Improve MineGeo exploration and mapping features

This commit is contained in:
2026-10-05 13:41:36 +05:00
parent aec0641f87
commit c5368fa8cc
69 changed files with 3581 additions and 333 deletions

View File

@@ -0,0 +1,236 @@
"use client";
import { useCallback, useEffect, useState } from "react";
import { api } from "./StartGame";
import { PanoramaPreview } from "./PanoramaPreview";
import { ContributionInstructions } from "./ContributionInstructions";
type Contribution = {
id: string;
name: string;
owner_id: string;
contributorName: string;
state: string;
contribution_status: string;
panorama_count: number;
contribution_count: number;
rejection_reason: string | null;
};
export function OwnerContributions({
id,
allowed,
serverAddress,
bounds,
locked,
onChange,
}: {
id: string;
allowed: boolean;
serverAddress: string | null;
bounds: { minX: number; minZ: number; maxX: number; maxZ: number };
locked: boolean;
onChange: () => void;
}) {
const [items, setItems] = useState<Contribution[]>([]),
[address, setAddress] = useState(serverAddress ?? ""),
[saved, setSaved] = useState(""),
[error, setError] = useState(""),
[busy, setBusy] = useState(false),
[preview, setPreview] = useState<string | null>(null),
[reasons, setReasons] = useState<Record<string, string>>({});
useEffect(() => {
setAddress(serverAddress ?? "");
}, [serverAddress]);
async function saveAddress(value: string) {
setBusy(true);
setError("");
setSaved("");
try {
await api(
`datasets/${id}/settings`,
{ contributionServerAddress: value.trim() || null },
"PATCH",
);
setAddress(value);
setSaved(
value.trim()
? "Server address saved."
: "Server check turned off. Submissions still need your approval.",
);
onChange();
} catch (e) {
setError((e as Error).message);
} finally {
setBusy(false);
}
}
const load = useCallback(async () => {
try {
const result = await api<{ contributions: Contribution[] }>(
`contributions/${id}/inbox`,
);
setItems(result.contributions);
} catch (e) {
setError((e as Error).message);
}
}, [id]);
useEffect(() => {
load();
const timer = setInterval(load, 10000);
return () => clearInterval(timer);
}, [load]);
async function toggle(value: boolean) {
setBusy(true);
setError("");
try {
await api(
`datasets/${id}/settings`,
{ allowContributions: value },
"PATCH",
);
onChange();
} catch (e) {
setError((e as Error).message);
} finally {
setBusy(false);
}
}
async function decide(child: string, decision: string) {
setBusy(true);
setError("");
try {
await api(`contributions/${id}/inbox/${child}`, {
decision,
reason: reasons[child],
});
await load();
onChange();
} catch (e) {
setError((e as Error).message);
} finally {
setBusy(false);
}
}
return (
<section className="token-box">
<h2>Panorama settings</h2>
<label>
<input
type="checkbox"
checked={allowed}
disabled={busy}
onChange={(e) => toggle(e.target.checked)}
/>{" "}
Allow players to add panoramas with you
</label>
<p className="muted">
Five random locations are picked for each game. Add up to 24 locations
to make repeat games more varied. You can use the map ID below to add
your own panoramas too.
</p>
<label className="field">
Contribution server address
<input
value={address}
placeholder="play.example.com:25565"
maxLength={255}
onChange={(e) => {
setAddress(e.target.value);
setSaved("");
}}
/>
</label>
<p className="muted">
Use the Minecraft hostname or IP players connect to. This address is
shown publicly when player contributions are enabled. The mod records
the connected server; plugin admins set server-address before exporting.
You can override the captured address here, or clear it to allow
unchecked submissions.
</p>
<div className="inline-actions">
<button
className="button"
disabled={busy}
onClick={() => saveAddress(address)}
>
Save server address
</button>
<button
className="button"
disabled={busy || !serverAddress}
onClick={() => saveAddress("")}
>
Clear address and turn off check
</button>
</div>
{saved && <p role="status">{saved}</p>}
<ContributionInstructions
id={id}
bounds={bounds}
serverAddress={serverAddress}
/>
<h3>Player submissions</h3>
{locked && (
<p className="muted">
Wait for the current moderator review before accepting another
submission.
</p>
)}
{!items.length && <p className="muted">No contributions yet.</p>}
{items.map((c) => (
<article className="tool-card" key={c.id}>
<h3>{c.name}</h3>
<span className="badge">
{c.state === "failed" ? "failed" : c.contribution_status}
</span>
<p>
{c.panorama_count || c.contribution_count} panoramas · {c.state}
</p>
<p className="muted">Contributor: {c.contributorName}</p>
{c.rejection_reason && <p>{c.rejection_reason}</p>}
{c.contribution_status === "submitted" && (
<>
<button className="button" onClick={() => setPreview(c.id)}>
Inspect submitted panoramas
</button>
<label className="field">
Feedback
<textarea
value={reasons[c.id] ?? ""}
onChange={(e) =>
setReasons((r) => ({ ...r, [c.id]: e.target.value }))
}
/>
</label>
<div className="inline-actions">
<button
className="button primary"
disabled={busy || locked}
onClick={() => decide(c.id, "accept")}
>
Accept into map draft
</button>
<button
className="button"
disabled={busy || locked || !reasons[c.id]?.trim()}
onClick={() => decide(c.id, "reject")}
>
Reject with feedback
</button>
</div>
</>
)}
</article>
))}
{preview && (
<PanoramaPreview
endpoint={`contributions/${id}/inbox/${preview}/panoramas`}
onClose={() => setPreview(null)}
/>
)}
{error && (
<p className="error-message" role="alert">
{error}
</p>
)}
</section>
);
}