Improve MineGeo interface and functionality
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user