Add account restrictions, appeals, and admin moderation tools

This commit is contained in:
2026-10-13 12:32:37 +05:00
parent 018be5049e
commit cfe5820802
28 changed files with 1936 additions and 125 deletions

View File

@@ -12,6 +12,7 @@ import { ModeratorSettings } from "./ModeratorSettings";
import { ModeratorPrefixes } from "./ModeratorPrefixes";
import { isAdministrator, type StaffRole } from "@/shared/permissions";
import { DiscordIntegration } from "./DiscordIntegration";
import { AccountControls } from "./AccountControls";
import { PrometheusIntegration } from "./PrometheusIntegration";
type ReviewMap = {
id: string;
@@ -26,7 +27,11 @@ type ReviewMap = {
report_strikes: number;
banned_at: string | null;
};
export function Moderation() {
export function Moderation({
mode = "moderation",
}: {
mode?: "moderation" | "admin";
}) {
const { t, locale } = useI18n();
const [maps, setMaps] = useState<ReviewMap[]>([]),
[reports, setReports] = useState<
@@ -49,7 +54,7 @@ export function Moderation() {
const [preview, setPreview] = useState<string | null>(null),
[loading, setLoading] = useState(true);
const [role, setRole] = useState<StaffRole | null>(null);
const administrator = isAdministrator(role ?? undefined);
const administrator = mode === "admin" && isAdministrator(role ?? undefined);
const [reportPreview, setReportPreview] = useState<{
id: string;
panorama: string | null;
@@ -61,7 +66,7 @@ export function Moderation() {
maps: ReviewMap[];
reports: typeof reports;
appeals: Appeal[];
}>("moderation");
}>(mode === "admin" ? "moderation?view=admin" : "moderation");
setMaps(d.maps);
setRole(d.role);
setReports(d.reports);
@@ -103,7 +108,7 @@ export function Moderation() {
}
useEffect(() => {
load();
}, []);
}, [mode]);
async function decide(datasetId: string, decision: string) {
setBusy(true);
setError("");
@@ -122,6 +127,12 @@ export function Moderation() {
}
return (
<>
{role && (
<AccountControls
administrator={administrator}
showAppeals={mode === "moderation"}
/>
)}
{administrator && <ModeratorSettings />}
{administrator && <ModeratorPrefixes />}
{administrator && <DiscordIntegration />}
@@ -129,7 +140,7 @@ export function Moderation() {
{role === "moderator" && (
<p className="muted">
{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.",
)}
</p>
)}
@@ -142,78 +153,81 @@ export function Moderation() {
{!loading && !maps.length && !error && (
<p className="muted">{t("The review queue is clear.")}</p>
)}
{maps.map((m) => (
<article className="tool-card" key={m.id}>
<span className="badge">{t(statusMessage(m.state))}</span>
{m.update_state === "review" && (
<span className="badge">{t("Map update awaiting review")}</span>
)}
<h2>{m.name}</h2>
{m.report_strikes > 0 && (
<p className={m.banned_at ? "error-message" : "muted"}>
{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) => (
<article className="tool-card" key={m.id}>
<span className="badge">{t(statusMessage(m.state))}</span>
{m.update_state === "review" && (
<span className="badge">{t("Map update awaiting review")}</span>
)}
<h2>{m.name}</h2>
{m.report_strikes > 0 && (
<p className={m.banned_at ? "error-message" : "muted"}>
{m.report_strikes}
{t("/3 confirmed reports")}
{m.banned_at ? t(" · Banned; owner appeal required") : ""}
</p>
)}
<p className="muted">
{m.source} · {m.panorama_count} {t("locations ·")} {m.description}
</p>
)}
<p className="muted">
{m.source} · {m.panorama_count} {t("locations ·")} {m.description}
</p>
{m.map && (
<Image
src={m.map}
width={512}
height={512}
alt={t("Map for moderator review")}
unoptimized
style={{ maxWidth: "100%", height: "auto" }}
/>
)}
<label className="field">
{t("Review reason")}
<textarea
value={reasons[m.id] || ""}
onChange={(e) =>
setReasons((s) => ({ ...s, [m.id]: e.target.value }))
}
/>
</label>
<div className="inline-actions">
<button className="button" onClick={() => setPreview(m.id)}>
{t("Inspect 360° locations")}
</button>
{m.state === "review" || m.update_state === "review" ? (
<>
<button
className="button primary"
disabled={busy}
onClick={() => decide(m.id, "approve")}
>
{m.update_state === "review"
? t("Approve update")
: t("Approve map")}
</button>
{m.map && (
<Image
src={m.map}
width={512}
height={512}
alt={t("Map for moderator review")}
unoptimized
style={{ maxWidth: "100%", height: "auto" }}
/>
)}
<label className="field">
{t("Review reason")}
<textarea
value={reasons[m.id] || ""}
onChange={(e) =>
setReasons((s) => ({ ...s, [m.id]: e.target.value }))
}
/>
</label>
<div className="inline-actions">
<button className="button" onClick={() => setPreview(m.id)}>
{t("Inspect 360° locations")}
</button>
{mode === "moderation" &&
(m.state === "review" || m.update_state === "review") ? (
<>
<button
className="button primary"
disabled={busy}
onClick={() => decide(m.id, "approve")}
>
{m.update_state === "review"
? t("Approve update")
: t("Approve map")}
</button>
<button
className="button"
disabled={busy || !reasons[m.id]?.trim()}
onClick={() => decide(m.id, "reject")}
>
{t("Return with feedback")}
</button>
</>
) : null}
{administrator && m.state === "published" && (
<button
className="button"
disabled={busy || !reasons[m.id]?.trim()}
onClick={() => decide(m.id, "reject")}
disabled={busy}
onClick={() => decide(m.id, "unpublish")}
>
{t("Return with feedback")}
{t("Unpublish")}
</button>
</>
) : null}
{administrator && m.state === "published" && (
<button
className="button"
disabled={busy}
onClick={() => decide(m.id, "unpublish")}
>
{t("Unpublish")}
</button>
)}
</div>
</article>
))}
)}
</div>
</article>
))}
{administrator && reports.length > 0 && (
<section className="token-box">
<h2>{t("Community reports")}</h2>