Files
MineGeo/components/AccountAppeal.tsx

244 lines
9.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import Link from "next/link";
import { useEffect, useRef, useState } from "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";
export function AccountAppeal() {
const { locale } = useI18n();
const l = (en: string, ru: string) => (locale === "ru" ? ru : en);
const [data, setData] = useState<{
account: ManagedAccount;
appeals: AccountAppealRecord[];
} | null>(null);
const [error, setError] = useState("");
const [message, setMessage] = useState("");
const [loading, setLoading] = useState(true);
const [reason, setReason] = useState("");
const [busy, setBusy] = useState(false);
const lock = useRef(false);
useEffect(() => {
let active = true;
api<{ account: ManagedAccount; appeals: AccountAppealRecord[] }>(
"ban/appeal",
)
.then((result) => {
if (active) setData(result);
})
.catch((e) => {
if (active) setError((e as Error).message);
})
.finally(() => {
if (active) setLoading(false);
});
return () => {
active = false;
};
}, []);
async function run(operation: () => Promise<void>) {
if (lock.current) return;
lock.current = true;
setBusy(true);
setError("");
try {
await operation();
} catch (e) {
setError((e as Error).message);
} finally {
lock.current = false;
setBusy(false);
}
}
async function reload() {
setData(await api("ban/appeal"));
setLoading(false);
}
const current = data?.appeals.find(
(a) => a.status_version === data.account.status_version,
);
return (
<section
aria-label={l(
"Account access and appeals",
"Доступ к аккаунту и апелляции",
)}
>
{error && (
<p className="error-message" role="alert">
{error}
</p>
)}
{message && (
<p className="muted" role="status">
{message}
</p>
)}
{loading && !error && (
<p role="status">
{l("Loading account status…", "Загрузка состояния аккаунта…")}
</p>
)}
<div className={styles.actions}>
<button
className="button"
disabled={busy}
onClick={() => void run(reload)}
>
{l("Refresh status", "Обновить статус")}
</button>
<button
className="button"
disabled={busy}
onClick={() =>
void run(async () => {
await api("auth/logout", {});
window.location.assign("/log-in");
})
}
>
{l("Log out", "Выйти")}
</button>
<Link className="button" href="/docs?page=account-bans">
{l("Read the guide", "Открыть руководство")}
</Link>
</div>
{data && (
<>
<div className={styles.record} style={{ marginTop: 32 }}>
<h2>
{data.account.account_status === "active"
? l("Your account is active", "Ваш аккаунт активен")
: data.account.account_status === "banned"
? l("Your account is banned", "Ваш аккаунт заблокирован")
: data.account.account_status === "deleting"
? l(
"Account deletion is pending",
"Ожидается удаление аккаунта",
)
: l(
"Your account is deactivated",
"Ваш аккаунт деактивирован",
)}
</h2>
{data.account.account_reason && (
<>
<strong>{l("Reason", "Причина")}</strong>
<p className={styles.reason}>{data.account.account_reason}</p>
</>
)}
{data.account.account_status === "active" ? (
<Link className="button primary" href="/servers">
{l("Continue playing", "Продолжить играть")}
</Link>
) : (
<p>
{l(
"Normal account access, uploads and API keys are blocked. You can still read the docs, view this page and sign out.",
"Обычный доступ к аккаунту, загрузки и ключи API заблокированы. Вы можете читать документацию, открывать эту страницу и выходить из аккаунта.",
)}
</p>
)}
{data.account.account_status === "deactivated" && (
<p>
{l(
"An Admin must restore a deactivated account. Ban appeals are available only for banned accounts. Contact the team through the community links below.",
"Деактивированный аккаунт может восстановить только администратор. Апелляции доступны только при блокировке. Свяжитесь с командой через ссылки сообщества внизу.",
)}
</p>
)}
{data.account.account_status === "deleting" && (
<p>
{l(
"An Admin must finish this action. A deleted account cannot be appealed or recovered here.",
"Администратор должен завершить это действие. Удалённый аккаунт нельзя восстановить или обжаловать здесь.",
)}
</p>
)}
</div>
{data.account.account_status === "banned" && !current && (
<form
className={styles.confirm}
onSubmit={(e) => {
e.preventDefault();
void run(async () => {
await api("ban/appeal", { reason });
setReason("");
setMessage(
l(
"Appeal sent. A Moderator or Admin will review it.",
"Апелляция отправлена. Её рассмотрит модератор или администратор.",
),
);
await reload();
});
}}
>
<h2>{l("Appeal this ban", "Обжаловать блокировку")}</h2>
<p>
{l(
"Explain what happened and why the ban should be reconsidered. You can submit one appeal for each ban. Staff will leave their decision here.",
"Расскажите, что произошло и почему блокировку стоит пересмотреть. Для каждой блокировки можно подать одну апелляцию. Решение команды появится здесь.",
)}
</p>
<label className="field">
{l(
"Your explanation (10–2000 characters)",
"Ваше объяснение (10–2000 символов)",
)}
<textarea
required
minLength={10}
maxLength={2000}
rows={6}
disabled={busy}
value={reason}
onChange={(e) => setReason(e.target.value)}
/>
</label>
<button
className="button primary"
disabled={busy || reason.trim().length < 10}
>
{busy
? l("Sending…", "Отправка…")
: l("Send appeal", "Отправить апелляцию")}
</button>
</form>
)}
{data.appeals.length > 0 && (
<div className={styles.list}>
<h2>{l("Your appeals", "Ваши апелляции")}</h2>
{data.appeals.map((appeal) => (
<article className={styles.record} key={appeal.id}>
<span className="badge">
{appeal.state === "open"
? l("Awaiting review", "Ожидает рассмотрения")
: appeal.state === "approved"
? l("Approved", "Одобрена")
: l("Rejected", "Отклонена")}
</span>
<p className="muted">
{new Date(appeal.created_at).toLocaleString(locale)}
</p>
<p className={styles.reason}>{appeal.reason}</p>
{appeal.review_reason && (
<>
<strong>{l("Staff decision", "Решение команды")}</strong>
<p className={styles.reason}>{appeal.review_reason}</p>
</>
)}
</article>
))}
</div>
)}
</>
)}
</section>
);
}