Improve MineGeo interface and functionality

This commit is contained in:
2026-10-06 19:24:41 +05:00
parent 7404f1dc1c
commit 233e4d57f0
67 changed files with 3053 additions and 643 deletions

View File

@@ -1,10 +1,14 @@
"use client";
import { statusMessage } from "@/shared/i18n";
import { useI18n } from "@/components/I18nProvider";
import { useEffect, useState } from "react";
import Image from "next/image";
import { api } from "./StartGame";
import { PanoramaPreview } from "./PanoramaPreview";
import { reportCategories } from "@/shared/moderation";
import type { Appeal } from "./MapAppeal";
import { ModeratorSettings } from "./ModeratorSettings";
type ReviewMap = {
id: string;
name: string;
@@ -19,6 +23,7 @@ type ReviewMap = {
banned_at: string | null;
};
export function Moderation() {
const { t, locale } = useI18n();
const [maps, setMaps] = useState<ReviewMap[]>([]),
[reports, setReports] = useState<
{
@@ -39,6 +44,7 @@ export function Moderation() {
[reasons, setReasons] = useState<Record<string, string>>({});
const [preview, setPreview] = useState<string | null>(null),
[loading, setLoading] = useState(true);
const [authorized, setAuthorized] = useState(false);
const [reportPreview, setReportPreview] = useState<{
id: string;
panorama: string | null;
@@ -51,10 +57,12 @@ export function Moderation() {
appeals: Appeal[];
}>("moderation");
setMaps(d.maps);
setAuthorized(true);
setReports(d.reports);
setAppeals(d.appeals);
setError("");
} catch (e) {
setAuthorized(false);
setError((e as Error).message);
} finally {
setLoading(false);
@@ -105,43 +113,45 @@ export function Moderation() {
}
return (
<>
{authorized && <ModeratorSettings />}
{error && (
<p role="alert" className="error-message">
{error}
{t(error)}
</p>
)}
{loading && <p role="status">Loading review queue…</p>}
{loading && <p role="status">{t("Loading review queue…")}</p>}
{!loading && !maps.length && !error && (
<p className="muted">The review queue is clear.</p>
<p className="muted">{t("The review queue is clear.")}</p>
)}
{maps.map((m) => (
<article className="tool-card" key={m.id}>
<span className="badge">{m.state}</span>
<span className="badge">{t(statusMessage(m.state))}</span>
{m.update_state === "review" && (
<span className="badge">Map update awaiting review</span>
<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}/3 confirmed reports
{m.banned_at ? " · Banned; owner appeal required" : ""}
{m.report_strikes}
{t("/3 confirmed reports")}
{m.banned_at ? t(" · Banned; owner appeal required") : ""}
</p>
)}
<p className="muted">
{m.source} · {m.panorama_count} locations · {m.description}
{m.source} · {m.panorama_count} {t("locations ·")} {m.description}
</p>
{m.map && (
<Image
src={m.map}
width={512}
height={512}
alt="Map for moderator review"
alt={t("Map for moderator review")}
unoptimized
style={{ maxWidth: "100%", height: "auto" }}
/>
)}
<label className="field">
Review reason
{t("Review reason")}
<textarea
value={reasons[m.id] || ""}
onChange={(e) =>
@@ -151,7 +161,7 @@ export function Moderation() {
</label>
<div className="inline-actions">
<button className="button" onClick={() => setPreview(m.id)}>
Inspect 360° locations
{t("Inspect 360° locations")}
</button>
{m.state === "review" || m.update_state === "review" ? (
<>
@@ -161,15 +171,15 @@ export function Moderation() {
onClick={() => decide(m.id, "approve")}
>
{m.update_state === "review"
? "Approve update"
: "Approve map"}
? t("Approve update")
: t("Approve map")}
</button>
<button
className="button"
disabled={busy || !reasons[m.id]?.trim()}
onClick={() => decide(m.id, "reject")}
>
Return with feedback
{t("Return with feedback")}
</button>
</>
) : null}
@@ -179,7 +189,7 @@ export function Moderation() {
disabled={busy}
onClick={() => decide(m.id, "unpublish")}
>
Unpublish
{t("Unpublish")}
</button>
)}
</div>
@@ -187,10 +197,11 @@ export function Moderation() {
))}
{reports.length > 0 && (
<section className="token-box">
<h2>Community reports</h2>
<h2>{t("Community reports")}</h2>
<p className="muted">
Confirming a valid report adds one strike to the map. Three strikes
automatically unpublish it. Dismissed reports add no strikes.
{t(
"Confirming a valid report adds one strike to the map. Three strikes automatically unpublish it. Dismissed reports add no strikes.",
)}
</p>
{reports.map((r) => (
<article className="draft-row" key={r.id}>
@@ -198,9 +209,13 @@ export function Moderation() {
<h3>{r.mapName}</h3>
<code>{r.dataset_id}</code>
<p>
{r.panorama_id ? `Panorama: ${r.panorama_id}` : "Entire map"}{" "}
{r.panorama_id
? `Panorama: ${r.panorama_id}`
: t("Entire map")}{" "}
· {reportCategories[r.category]} ·{" "}
{new Date(r.created_at).toLocaleString()}
{new Date(r.created_at).toLocaleString(
locale === "ru" ? "ru-RU" : "en-GB",
)}
</p>
<p>{r.reason}</p>
{r.evidence_url && (
@@ -210,7 +225,7 @@ export function Moderation() {
target="_blank"
rel="noopener noreferrer"
>
Open evidence
{t("Open evidence")}
</a>
</p>
)}
@@ -219,7 +234,7 @@ export function Moderation() {
src={r.map}
width={320}
height={320}
alt="Reported map version"
alt={t("Reported map version")}
unoptimized
style={{ maxWidth: "100%", height: "auto" }}
/>
@@ -230,10 +245,10 @@ export function Moderation() {
setReportPreview({ id: r.id, panorama: r.panorama_id })
}
>
Inspect reported content
{t("Inspect reported content")}
</button>
<label className="field">
Decision reason
{t("Decision reason")}
<textarea
maxLength={2000}
value={reasons[r.id] ?? ""}
@@ -252,14 +267,14 @@ export function Moderation() {
}
onClick={() => resolveReport(r.id, "confirm")}
>
Confirm report · add strike
{t("Confirm report · add strike")}
</button>
<button
className="button"
disabled={busy || (reasons[r.id]?.trim().length ?? 0) < 10}
onClick={() => resolveReport(r.id, "dismiss")}
>
Dismiss report
{t("Dismiss report")}
</button>
</div>
</div>
@@ -269,7 +284,7 @@ export function Moderation() {
)}
{appeals.length > 0 && (
<section className="token-box">
<h2>Map ban appeals</h2>
<h2>{t("Map ban appeals")}</h2>
{appeals.map((a) => (
<article className="tool-card" key={a.id}>
<h3>
@@ -280,10 +295,10 @@ export function Moderation() {
className="button"
onClick={() => setPreview(a.dataset_id)}
>
Inspect map panoramas
{t("Inspect map panoramas")}
</button>
<label className="field">
Appeal decision reason
{t("Appeal decision reason")}
<textarea
maxLength={2000}
value={reasons[a.id] ?? ""}
@@ -298,14 +313,14 @@ export function Moderation() {
disabled={busy || (reasons[a.id]?.trim().length ?? 0) < 10}
onClick={() => reviewAppeal(a.id, "approve")}
>
Approve appeal · restore map
{t("Approve appeal · restore map")}
</button>
<button
className="button"
disabled={busy || (reasons[a.id]?.trim().length ?? 0) < 10}
onClick={() => reviewAppeal(a.id, "reject")}
>
Reject appeal · keep ban
{t("Reject appeal · keep ban")}
</button>
</div>
</article>