All checks were successful
Deploy MineGeo to Dokploy / deploy (push) Successful in 6m26s
262 lines
7.5 KiB
TypeScript
262 lines
7.5 KiB
TypeScript
"use client";
|
|
import { statusMessage } from "@/shared/i18n";
|
|
import { useI18n } from "@/components/I18nProvider";
|
|
|
|
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 { t } = useI18n();
|
|
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>
|
|
{t("Add panoramas ·")} {count}/24
|
|
</h2>
|
|
<p className="muted">
|
|
{t(
|
|
"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)}
|
|
/>{" "}
|
|
{t("Allow players to add panoramas with you")}
|
|
</label>
|
|
<p className="muted">
|
|
{t(
|
|
"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">
|
|
{t("Contribution server addresses (separate with commas)")}
|
|
<input
|
|
value={address}
|
|
placeholder="play.example.com:25565"
|
|
maxLength={2559}
|
|
onChange={(e) => {
|
|
setAddress(e.target.value);
|
|
setSaved("");
|
|
}}
|
|
/>
|
|
</label>
|
|
<p className="muted">
|
|
{t(
|
|
"Enter up to 10 Minecraft hostnames or IPs separated by commas. These addresses are public when contributions are enabled. Clear them to turn off the server check.",
|
|
)}
|
|
</p>
|
|
<div className="inline-actions">
|
|
<button
|
|
className="button"
|
|
disabled={busy}
|
|
onClick={() => saveAddress(address)}
|
|
>
|
|
{t("Save server address")}
|
|
</button>
|
|
<button
|
|
className="button"
|
|
disabled={busy || !serverAddress}
|
|
onClick={() => saveAddress("")}
|
|
>
|
|
{t("Clear address and turn off check")}
|
|
</button>
|
|
</div>
|
|
{saved && <p role="status">{t(saved)}</p>}
|
|
<ContributionInstructions
|
|
id={id}
|
|
bounds={bounds}
|
|
serverAddress={serverAddress}
|
|
isOwner
|
|
/>
|
|
<h3>{t("Player submissions")}</h3>
|
|
<p className="muted">
|
|
{t(
|
|
"Submissions older than five days are hidden from this list. Accepted panoramas stay in your map.",
|
|
)}
|
|
</p>
|
|
{locked && (
|
|
<p className="muted">
|
|
{t(
|
|
"Wait for the current moderator review before accepting another submission.",
|
|
)}
|
|
</p>
|
|
)}
|
|
{!items.length && (
|
|
<p className="muted">
|
|
{t("No contributions from the last five days.")}
|
|
</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} {t("panoramas ·")}{" "}
|
|
{t(statusMessage(c.state))}
|
|
</p>
|
|
<p className="muted">
|
|
{t("Contributor:")} {c.contributorName}
|
|
</p>
|
|
{c.rejection_reason && <p>{c.rejection_reason}</p>}
|
|
{c.contribution_status === "submitted" && (
|
|
<>
|
|
<button className="button" onClick={() => setPreview(c.id)}>
|
|
{t("Inspect submitted panoramas")}
|
|
</button>
|
|
<label className="field">
|
|
{t("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")}
|
|
>
|
|
{t("Accept into map draft")}
|
|
</button>
|
|
<button
|
|
className="button"
|
|
disabled={busy || locked || !reasons[c.id]?.trim()}
|
|
onClick={() => decide(c.id, "reject")}
|
|
>
|
|
{t("Reject with feedback")}
|
|
</button>
|
|
</div>
|
|
</>
|
|
)}
|
|
</article>
|
|
))}
|
|
{preview && (
|
|
<PanoramaPreview
|
|
endpoint={`contributions/${id}/inbox/${preview}/panoramas`}
|
|
onClose={() => setPreview(null)}
|
|
/>
|
|
)}
|
|
{error && (
|
|
<p className="error-message" role="alert">
|
|
{t(error)}
|
|
</p>
|
|
)}
|
|
</section>
|
|
);
|
|
}
|