245 lines
7.0 KiB
TypeScript
245 lines
7.0 KiB
TypeScript
"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,
|
|
count,
|
|
}: {
|
|
id: string;
|
|
allowed: boolean;
|
|
serverAddress: string | null;
|
|
bounds: { minX: number; minZ: number; maxX: number; maxZ: number };
|
|
locked: boolean;
|
|
onChange: () => void;
|
|
count: number;
|
|
}) {
|
|
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>Add panoramas · {count}/24</h2>
|
|
<p className="muted">
|
|
Owners can fill all 24 slots, even with player contributions turned off.
|
|
Export a fresh capture, then use the command below to add it to this
|
|
map.
|
|
</p>
|
|
<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}
|
|
isOwner
|
|
/>
|
|
<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>
|
|
);
|
|
}
|