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,4 +1,7 @@
"use client";
import { statusMessage } from "@/shared/i18n";
import { useI18n } from "@/components/I18nProvider";
import { useEffect, useRef, useState } from "react";
import Image from "next/image";
import { api } from "./StartGame";
@@ -31,6 +34,7 @@ type Draft = {
ban_reason: string | null;
};
export function DraftEditor({ id }: { id: string }) {
const { t } = useI18n();
const dirty = useRef(false);
const [draft, setDraft] = useState<Draft | null>(null),
[name, setName] = useState(""),
@@ -123,7 +127,9 @@ export function DraftEditor({ id }: { id: string }) {
}
if (!draft)
return (
<p role={error ? "alert" : undefined}>{error || "Loading your draft…"}</p>
<p role={error ? "alert" : undefined}>
{error ? t(error) : t("Loading your draft…")}
</p>
);
const editable =
(["draft", "rejected"].includes(draft.state) ||
@@ -139,11 +145,11 @@ export function DraftEditor({ id }: { id: string }) {
return (
<>
<div className="editor-navigation">
<Link href="/dashboard">← Your maps</Link>
<a href="#locations">Choose locations</a>
{!draft.parent_dataset_id && <a href="#map-tools">Map tools</a>}
<Link href="/dashboard">{t("← Your maps")}</Link>
<a href="#locations">{t("Choose locations")}</a>
{!draft.parent_dataset_id && <a href="#map-tools">{t("Map tools")}</a>}
</div>
<span className="badge">{draft.state}</span>
<span className="badge">{t(statusMessage(draft.state))}</span>
{!draft.parent_dataset_id &&
(draft.report_strikes > 0 || draft.banned_at) && (
<MapAppeal
@@ -155,47 +161,63 @@ export function DraftEditor({ id }: { id: string }) {
/>
)}
{draft.update_state && (
<span className="badge">Update: {draft.update_state}</span>
<span className="badge">
{t("Update:")} {t(statusMessage(draft.update_state))}
</span>
)}
{draft.contribution_status && (
<p role="status">
Contribution: {draft.contribution_status}.{" "}
{t("Contribution:")} {t(statusMessage(draft.contribution_status))}.{" "}
{draft.state === "failed"
? "This contribution no longer reserves panorama slots. Upload a fresh capture to try again."
? t(
"This contribution no longer reserves panorama slots. Upload a fresh capture to try again.",
)
: draft.contribution_status === "accepted"
? "The owner included your panoramas in the map draft. Map updates become playable after moderator approval."
? t(
"The owner included your panoramas in the map draft. Map updates become playable after moderator approval.",
)
: draft.contribution_status === "submitted"
? "Waiting for the map owner's approval."
? t("Waiting for the map owner's approval.")
: draft.contribution_status === "rejected"
? "Upload a fresh capture if you want to try again."
: "Choose your locations, then submit them to the map owner."}
? t("Upload a fresh capture if you want to try again.")
: t(
"Choose your locations, then submit them to the map owner.",
)}
</p>
)}
{draft.state === "published" && (
<p className="muted">
The approved version stays playable while you edit and moderators
review your update.
{t(
"The approved version stays playable while you edit and moderators review your update.",
)}
</p>
)}
<ProcessingProgress job={draft.upload_jobs} />
{draft.upload_jobs?.kind === "map" &&
draft.upload_jobs.state === "failed" && (
<p role="alert" className="error-message">
Map refresh failed: {draft.upload_jobs.error}. Your previous map is
still available. Try refreshing again below.
{t("Map refresh failed:")} {draft.upload_jobs.error}
{t(
". Your previous map is still available. Try refreshing again below.",
)}
</p>
)}
{refreshing && (
<p role="status" className="success-message">
Refreshing your guessing map. This page updates automatically; the
published version stays playable.
{t(
"Refreshing your guessing map. This page updates automatically; the published version stays playable.",
)}
</p>
)}
{["uploading", "processing"].includes(draft.state) && (
<p role="status" className="success-message">
{draft.state === "uploading"
? "Waiting for the capture tool to finish uploading. Retry upload in the tool if the connection was interrupted."
: "Your map and panoramas are being rendered. This page updates automatically."}
? t(
"Waiting for the capture tool to finish uploading. Retry upload in the tool if the connection was interrupted.",
)
: t(
"Your map and panoramas are being rendered. This page updates automatically.",
)}
</p>
)}
{draft.rejection_reason && (
@@ -205,8 +227,12 @@ export function DraftEditor({ id }: { id: string }) {
)}
<p className="muted">
{draft.source === "plugin"
? "Server-generated views use vanilla terrain and simplified lighting. Entities, shaders, custom packs and dynamic sign text are omitted."
: "Player captures show the terrain received by the client. Unexplored map coverage remains blank."}
? t(
"Server-generated views use vanilla terrain and simplified lighting. Entities, shaders, custom packs and dynamic sign text are omitted.",
)
: t(
"Player captures show the terrain received by the client. Unexplored map coverage remains blank.",
)}
</p>
{draft.map && (
<div className="detail-map" style={{ maxWidth: 600 }}>
@@ -215,15 +241,15 @@ export function DraftEditor({ id }: { id: string }) {
width={1024}
height={1024}
loading="eager"
alt="Draft surface map"
alt={t("Draft surface map")}
unoptimized
/>
</div>
)}
<section className="editor-panel">
<h2>Map details</h2>
<h2>{t("Map details")}</h2>
<label className="field">
Map name
{t("Map name")}
<input
value={name}
onChange={(e) => {
@@ -234,7 +260,7 @@ export function DraftEditor({ id }: { id: string }) {
/>
</label>
<label className="field">
Description
{t("Description")}
<textarea
value={description}
onChange={(e) => {
@@ -245,10 +271,12 @@ export function DraftEditor({ id }: { id: string }) {
/>
</label>
</section>
<h2 id="locations">Choose your locations.</h2>
<h2 id="locations">{t("Choose your locations.")}</h2>
<p className="muted">
Keep at least {minimum} clear, useful panorama{minimum === 1 ? "" : "s"}
. Selected: {selected.length} / 24
{t("Minimum panoramas: {minimum}. Selected: {selected} / 24", {
minimum,
selected: selected.length,
})}
</p>
<div className="preview-grid">
{draft.previews.map((p) => (
@@ -258,7 +286,7 @@ export function DraftEditor({ id }: { id: string }) {
type="checkbox"
checked={selected.includes(p.id)}
disabled={!editable || busy}
aria-label={`Keep ${p.label}`}
aria-label={t("Keep {location}", { location: p.label })}
onChange={(e) => {
dirty.current = true;
setSelected((s) =>
@@ -281,7 +309,7 @@ export function DraftEditor({ id }: { id: string }) {
</label>
{p.image && (
<button className="button full" onClick={() => setPreview(p.id)}>
Look around
{t("Look around")}
</button>
)}
</div>
@@ -301,7 +329,7 @@ export function DraftEditor({ id }: { id: string }) {
disabled={busy || refreshing || selected.length < minimum}
onClick={() => save()}
>
Save draft
{t("Save draft")}
</button>
<button
className="button primary"
@@ -309,8 +337,8 @@ export function DraftEditor({ id }: { id: string }) {
onClick={() => save(true)}
>
{draft.parent_dataset_id
? "Submit to map owner →"
: "Submit for review →"}
? t("Submit to map owner →")
: t("Submit for review →")}
</button>
</div>
)}
@@ -346,18 +374,18 @@ export function DraftEditor({ id }: { id: string }) {
))) && (
<button className="button" disabled={busy} onClick={cancel}>
{draft.parent_dataset_id
? "Withdraw contribution"
: "Cancel processing"}
? t("Withdraw contribution")
: t("Cancel processing")}
</button>
)}
{error && (
<p role="alert" className="error-message">
{error}
{t(error)}
</p>
)}
{message && (
<p role="status" className="success-message">
{message}
{t(message)}
</p>
)}
</>