Improve MineGeo exploration and mapping features
This commit is contained in:
236
components/OwnerContributions.tsx
Normal file
236
components/OwnerContributions.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user