"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, StartGame } from "./StartGame"; import type { DatasetManifest } from "@/shared/protocol"; import type { ProcessingJob } from "@/shared/processing-progress"; import { ProcessingProgress } from "./ProcessingProgress"; import { PanoramaPreview } from "./PanoramaPreview"; import { OwnerContributions } from "./OwnerContributions"; import Link from "next/link"; import { MapRefresh } from "./MapRefresh"; import { MapAppeal } from "./MapAppeal"; import { ExportMap } from "./MapTransfer"; type Draft = { imported_from_id: string | null; imported_owner_name: string | null; ownsPublishedParent: boolean; id: string; name: string; description: string; state: string; source: string; rejection_reason: string | null; map: string | null; manifest: DatasetManifest; previews: { id: string; label: string; image: string | null }[]; upload_jobs: ProcessingJob | null; parent_dataset_id: string | null; contribution_status: string | null; allow_contributions: boolean; contribution_server_address: string | null; update_state: string | null; report_strikes: number; banned_at: string | null; ban_reason: string | null; }; export function DraftEditor({ id, hostName, }: { id: string; hostName: string; }) { const { t } = useI18n(); const dirty = useRef(false); const [draft, setDraft] = useState(null), [name, setName] = useState(""), [description, setDescription] = useState(""), [selected, setSelected] = useState([]), [error, setError] = useState(""), [message, setMessage] = useState(""), [busy, setBusy] = useState(false); const [preview, setPreview] = useState(null); const [version, setVersion] = useState(0); useEffect(() => { let disposed = false; let timer: ReturnType; const load = () => api("datasets/" + id) .then((d) => { if (disposed) return; setError(""); setDraft(d); if (!dirty.current) { setName(d.name); setDescription(d.description); setSelected(d.manifest.viewpoints.map((p) => p.id)); } if ( d.banned_at || ["uploading", "processing"].includes(d.state) || ["queued", "running"].includes(d.upload_jobs?.state ?? "") ) timer = setTimeout(load, 5000); }) .catch((e) => { if (!disposed) { setError(e.message); timer = setTimeout(load, 5000); } }); load(); return () => { disposed = true; clearTimeout(timer); }; }, [id, version]); async function save(submit = false, publishOwnContribution = false) { setBusy(true); setError(""); setMessage(""); try { await api( "datasets/" + id, { name, description, selectedIds: selected }, "PATCH", ); dirty.current = false; if (submit) { const publishUpdate = draft?.state === "published" && !draft.parent_dataset_id; await api( "datasets/" + id + (publishOwnContribution ? "/publish-own-contribution" : publishUpdate ? "/publish-update" : "/submit"), {}, ); setVersion((v) => v + 1); setMessage( publishUpdate || publishOwnContribution ? "Map update published." : draft?.parent_dataset_id ? "Submitted for map owner approval." : "Submitted for moderator review.", ); } else setMessage("Draft saved."); } catch (e) { setError((e as Error).message); } finally { setBusy(false); } } async function cancel() { setBusy(true); try { await api("datasets/" + id + "/cancel", {}); if (draft?.parent_dataset_id) { setVersion((v) => v + 1); setMessage("Contribution withdrawn. Its pending quota is released."); return; } setDraft((d) => d ? { ...d, state: "failed", rejection_reason: "Processing cancelled" } : d, ); setMessage("Processing cancelled."); } catch (e) { setError((e as Error).message); } finally { setBusy(false); } } if (!draft) return (

{error ? t(error) : t("Loading your draft…")}

); const editable = (["draft", "rejected"].includes(draft.state) || draft.state === "published") && draft.update_state !== "review" && !["submitted", "accepted", "rejected"].includes( draft.contribution_status ?? "", ); const refreshing = draft.upload_jobs?.kind === "map" && ["queued", "running"].includes(draft.upload_jobs.state); const minimum = draft.parent_dataset_id ? 1 : 5; return ( <>
{t("← Your maps")} {t("Choose locations")} {!draft.imported_from_id && !draft.parent_dataset_id && ( {t("Map tools")} )}
{t(statusMessage(draft.state))} {!draft.imported_from_id && !draft.parent_dataset_id && draft.map && !draft.banned_at && ( )} {draft.imported_from_id && (

{t("Owned by {owner}", { owner: draft.imported_owner_name ?? "Explorer", })}

{t( "This shared map is private and playable without moderator review. Only the original owner can submit it for public review.", )}

{draft.state === "draft" && ( )}
)} {!draft.parent_dataset_id && (draft.report_strikes > 0 || draft.banned_at) && ( setVersion((v) => v + 1)} /> )} {draft.update_state && ( {t("Update:")} {t(statusMessage(draft.update_state))} )} {draft.contribution_status && (

{t("Contribution:")} {t(statusMessage(draft.contribution_status))}.{" "} {draft.state === "failed" ? t( "This contribution no longer reserves panorama slots. Upload a fresh capture to try again.", ) : draft.contribution_status === "accepted" ? t( "The owner included your panoramas in the map draft. Map updates become playable after moderator approval.", ) : draft.contribution_status === "submitted" ? t("Waiting for the map owner's approval.") : draft.contribution_status === "rejected" ? t("Upload a fresh capture if you want to try again.") : t( "Choose your locations, then submit them to the map owner.", )}

)} {draft.state === "published" && (

{t( "The approved version stays playable while you edit and moderators review your update.", )}

)} {draft.upload_jobs?.kind === "map" && draft.upload_jobs.state === "failed" && (

{t("Map refresh failed:")} {draft.upload_jobs.error} {t( ". Your previous map is still available. Try refreshing again below.", )}

)} {refreshing && (

{t( "Refreshing your guessing map. This page updates automatically; the published version stays playable.", )}

)} {["uploading", "processing"].includes(draft.state) && (

{draft.state === "uploading" ? 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.", )}

)} {draft.rejection_reason && (

{draft.rejection_reason}

)}

{draft.source === "plugin" ? 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.", )}

{draft.map && (
{t("Draft
)}

{t("Map details")}