"use client"; import { useEffect, useRef, useState } from "react"; import { ShieldCheck, Users, RefreshCw } from "lucide-react"; import { api } from "@/lib/client-api"; import { useI18n } from "./I18nProvider"; import type { ManagedAccount, AccountAppealRecord, } from "@/shared/account-controls"; import styles from "@/app/ban/appeal/page.module.css"; import { useAppealRealtime } from "./useAppealRealtime"; export function AccountControls({ administrator, showAppeals = true, }: { administrator: boolean; showAppeals?: boolean; }) { const { locale } = useI18n(); const l = (en: string, ru: string) => (locale === "ru" ? ru : en); const [query, setQuery] = useState(""); const [users, setUsers] = useState([]); const [viewer, setViewer] = useState(""); const [searched, setSearched] = useState(false); const [appeals, setAppeals] = useState([]); const [loading, setLoading] = useState(showAppeals); const [error, setError] = useState(""); const [message, setMessage] = useState(""); const [busy, setBusy] = useState(false); const lock = useRef(false); const appealReads = useRef(0); const [selected, setSelected] = useState<{ account: ManagedAccount; action: "deactivate" | "ban" | "restore" | "delete"; } | null>(null); const [reason, setReason] = useState(""); const [confirmation, setConfirmation] = useState(""); const [reviewReasons, setReviewReasons] = useState>( {}, ); const realtime = useAppealRealtime({ enabled: showAppeals, refresh: async () => { const version = ++appealReads.current; const result = await api<{ appeals: AccountAppealRecord[] }>( "moderation/account-appeals", ); if (version === appealReads.current) { setAppeals(result.appeals); setLoading(false); } }, onError: (error) => { setError((error as Error).message); setLoading(false); }, }); useEffect(() => { if (!showAppeals) return; let active = true; const version = ++appealReads.current; api<{ appeals: AccountAppealRecord[] }>("moderation/account-appeals") .then((data) => { if (active && version === appealReads.current) setAppeals(data.appeals); }) .catch((e) => { if (active && version === appealReads.current) setError((e as Error).message); }) .finally(() => { if (active && version === appealReads.current) setLoading(false); }); return () => { active = false; }; }, [showAppeals]); async function run(operation: () => Promise) { if (lock.current) return; lock.current = true; setBusy(true); setError(""); setMessage(""); try { await operation(); } catch (e) { setError((e as Error).message); } finally { lock.current = false; setBusy(false); } } async function find() { const result = await api<{ users: ManagedAccount[]; viewerId: string }>( "moderation/accounts?q=" + encodeURIComponent(query), ); setUsers(result.users); setViewer(result.viewerId); setSearched(true); } async function refreshAppeals() { if (!showAppeals) return; setLoading(true); const version = ++appealReads.current; try { const result = await api<{ appeals: AccountAppealRecord[] }>( "moderation/account-appeals", ); if (version === appealReads.current) setAppeals(result.appeals); } finally { if (version === appealReads.current) setLoading(false); } } async function save(event: React.FormEvent) { event.preventDefault(); if (!selected) return; const chosen = selected; await run(async () => { await api("moderation/accounts/" + chosen.account.id, { action: chosen.action, reason, ...(chosen.action === "delete" ? { confirmation } : {}), }); setSelected(null); setReason(""); setConfirmation(""); await find(); await refreshAppeals(); setMessage( l("Account action completed.", "Действие с аккаунтом выполнено."), ); }); } const stateLabel = (status: string) => ({ active: l("Active", "Активен"), deactivated: l("Deactivated", "Деактивирован"), banned: l("Banned", "Заблокирован"), deleting: l("Deletion pending", "Ожидает удаления"), })[status] ?? status; const actionLabel = (action: string) => ({ deactivate: l("Deactivate", "Деактивировать"), ban: l("Ban", "Заблокировать"), restore: l("Restore access", "Восстановить доступ"), delete: l("Delete permanently", "Удалить навсегда"), })[action] ?? action; return ( <> {error && (

{error}

)} {message && (

{message}

)} {administrator && (

{l( "Deactivate access temporarily, ban with a reason, restore access, or permanently delete an account. Admin accounts are protected.", "Временно отключайте доступ, блокируйте с указанием причины, восстанавливайте доступ или удаляйте аккаунты навсегда. Аккаунты администраторов защищены.", )}

{ e.preventDefault(); void run(find); }} >
{searched && users.length === 0 && (

{l("No accounts found.", "Аккаунты не найдены.")}

)}
{users.map((account) => (

{account.display_name}

{stateLabel(account.account_status)}

{account.id} · {account.role}

{account.account_reason &&

{account.account_reason}

} {account.role === "admin" || account.id === viewer ? (

{l("This account is protected.", "Этот аккаунт защищён.")}

) : (
{(["deactivate", "ban", "restore", "delete"] as const).map( (action) => ( ), )}
)} {selected?.account.id === account.id && (
void save(e)} >

{actionLabel(selected.action)}: {account.display_name}

{selected.action === "delete" ? l( "Permanent deletion removes sign-in, profile, credentials and account appeals. Shared maps and community history remain without an account owner. This cannot be undone.", "Удаление навсегда убирает вход, профиль, ключи и апелляции аккаунта. Общие карты и история сообщества сохраняются без владельца аккаунта. Отменить удаление нельзя.", ) : l( "The player will see your reason. Restricting access revokes existing keys and ends existing games and multiplayer participation.", "Игрок увидит указанную причину. Ограничение доступа отзывает ключи и завершает текущие игры и участие в мультиплеере.", )}