"use client"; import { useEffect, useRef, useState } from "react"; import Image from "next/image"; import { api } 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"; type Draft = { 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; }; export function DraftEditor({ id }: { id: string }) { 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 ( ["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) { setBusy(true); setError(""); setMessage(""); try { await api( "datasets/" + id, { name, description, selectedIds: selected }, "PATCH", ); dirty.current = false; if (submit) { await api("datasets/" + id + "/submit", {}); setVersion((v) => v + 1); setMessage( 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 || "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 ( <>
← Your maps Choose locations {!draft.parent_dataset_id && Map tools}
{draft.state} {draft.update_state && ( Update: {draft.update_state} )} {draft.contribution_status && (

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

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

The approved version stays playable while you edit and moderators review your update.

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

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

)} {refreshing && (

Refreshing your guessing map. This page updates automatically; the published version stays playable.

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

{draft.state === "uploading" ? "Waiting for the capture tool to finish uploading. Retry upload in the tool if the connection was interrupted." : "Your map and panoramas are being rendered. This page updates automatically."}

)} {draft.rejection_reason && (

{draft.rejection_reason}

)}

{draft.source === "plugin" ? "Server-generated views use vanilla terrain and simplified lighting. Entities, shaders, custom packs and dynamic sign text are omitted." : "Player captures show the terrain received by the client. Unexplored map coverage remains blank."}

{draft.map && (
Draft surface map
)}

Map details