+ {new Date(appeal.created_at).toLocaleString(locale)} +
+{appeal.reason}
+ {appeal.review_reason && ( + <> + {l("Staff decision", "Решение команды")} +{appeal.review_reason}
+ > + )} ++ {t( + "Manage accounts, site settings, integrations and community reports.", + )} +
++ + {t("Open Moderation Tools")} + +
++ {ru + ? "Войдите в заблокированный аккаунт, чтобы прочитать причину и подать апелляцию." + : "Log in to the affected account to see the reason and submit a ban appeal."} +
+ + {ru ? "Войти" : "Log in"} + ++ + {ru ? "Как работают апелляции" : "How appeals work"} + +
+ > + )} +- {t( - "Review map quality and coverage before a world enters the public catalog.", - )} + {t("Review pending maps, map updates and account ban appeals.")}
+ {isAdministrator(staff.role) && ( ++ + {t("Open Admin Tools")} + +
+ )}+ {error} +
+ )} + {message && ( ++ {message} +
+ )} + {loading && !error && ( ++ {l("Loading account status…", "Загрузка состояния аккаунта…")} +
+ )} +{data.account.account_reason}
+ > + )} + {data.account.account_status === "active" ? ( + + {l("Continue playing", "Продолжить играть")} + + ) : ( ++ {l( + "Normal account access, uploads and API keys are blocked. You can still read the docs, view this page and sign out.", + "Обычный доступ к аккаунту, загрузки и ключи API заблокированы. Вы можете читать документацию, открывать эту страницу и выходить из аккаунта.", + )} +
+ )} + {data.account.account_status === "deactivated" && ( ++ {l( + "An Admin must restore a deactivated account. Ban appeals are available only for banned accounts. Contact the team through the community links below.", + "Деактивированный аккаунт может восстановить только администратор. Апелляции доступны только при блокировке. Свяжитесь с командой через ссылки сообщества внизу.", + )} +
+ )} + {data.account.account_status === "deleting" && ( ++ {l( + "An Admin must finish this action. A deleted account cannot be appealed or recovered here.", + "Администратор должен завершить это действие. Удалённый аккаунт нельзя восстановить или обжаловать здесь.", + )} +
+ )} ++ {new Date(appeal.created_at).toLocaleString(locale)} +
+{appeal.reason}
+ {appeal.review_reason && ( + <> + {l("Staff decision", "Решение команды")} +{appeal.review_reason}
+ > + )} ++ {error} +
+ )} + {message && ( ++ {message} +
+ )} + {administrator && ( ++ {l( + "Deactivate access temporarily, ban with a reason, restore access, or permanently delete an account. Admin accounts are protected.", + "Временно отключайте доступ, блокируйте с указанием причины, восстанавливайте доступ или удаляйте аккаунты навсегда. Аккаунты администраторов защищены.", + )} +
+ + {searched && users.length === 0 && ( +{l("No accounts found.", "Аккаунты не найдены.")}
+ )} +
+ {account.id} · {account.role}
+
{account.account_reason}
} + {account.role === "admin" || account.id === viewer ? ( ++ {l("This account is protected.", "Этот аккаунт защищён.")} +
+ ) : ( ++ {l( + "Moderators and Admins can review appeals. Approving an appeal restores access for that ban. Deactivation and deletion remain Admin controls.", + "Модераторы и администраторы могут рассматривать апелляции. Одобрение восстанавливает доступ после этой блокировки. Деактивация и удаление доступны только администраторам.", + )} +
+ + {loading && ( +{l("Loading appeals…", "Загрузка апелляций…")}
+ )} + {!loading && !error && appeals.length === 0 && ( ++ {l( + "No appeals awaiting review.", + "Нет апелляций, ожидающих рассмотрения.", + )} +
+ )} +
+ {appeal.owner_id} ·{" "}
+ {new Date(appeal.created_at).toLocaleString(locale)}
+
+ {l("Ban reason: ", "Причина блокировки: ")} + {appeal.ban_reason} +
+ )} +{appeal.reason}
+ +{t( - "Moderators can inspect and review pending maps and updates. Other controls require Admin permission.", + "Moderators can review pending maps, updates and account ban appeals. Other controls require Admin permission.", )}
)} @@ -142,78 +153,81 @@ export function Moderation() { {!loading && !maps.length && !error && ({t("The review queue is clear.")}
)} - {maps.map((m) => ( -
- {m.report_strikes}
- {t("/3 confirmed reports")}
- {m.banned_at ? t(" · Banned; owner appeal required") : ""}
+ {maps
+ .filter((m) => mode !== "admin" || m.state !== "review")
+ .map((m) => (
+
+ {m.report_strikes}
+ {t("/3 confirmed reports")}
+ {m.banned_at ? t(" · Banned; owner appeal required") : ""}
+
+ {m.source} · {m.panorama_count} {t("locations ·")} {m.description}
- {m.source} · {m.panorama_count} {t("locations ·")} {m.description}
- {m.name}
+ {m.report_strikes > 0 && (
+ {t("Community reports")}
diff --git a/components/Shell.tsx b/components/Shell.tsx
index c50b54f..8db1843 100644
--- a/components/Shell.tsx
+++ b/components/Shell.tsx
@@ -3,7 +3,8 @@ import "server-only";
import Link from "next/link";
import { Compass, ArrowUpRight } from "lucide-react";
import { LanguageSwitcher } from "./LanguageSwitcher";
-import { user } from "@/lib/supabase";
+import { rawUser, admin, required } from "@/lib/supabase";
+import { canReviewMaps, isAdministrator } from "@/shared/permissions";
import { Brand } from "./Brand";
import { SiteNav } from "./SiteNav";
import { ReportBugButton } from "./ReportBugButton";
@@ -12,7 +13,7 @@ import { CommunityLinks } from "./CommunityLinks";
import { forumUrl } from "@/lib/forum";
export async function Header() {
const { t } = await getI18n();
- const account = await user();
+ const account = await rawUser();
return (