'use client'; import { useEffect, useRef, useState } from 'react'; import { Button } from './ui/button'; import { Input } from './ui/input'; import { FACTIONS, type FactionId } from '@/game/catalog'; import { ClientRequestError, requestJson, requestMayHaveCompleted } from '@/lib/client-request'; import { retainAdminRoomRequest } from '@/lib/admin-room-control-client'; import { clearAdminRoomCreation, completeAdminRoomCreation, newAdminRoomCreationRequest, readAdminRoomCreation, saveAdminRoomCreation, validAdminRoomCreationResult, type AdminRoomCreationRecord } from '@/lib/admin-room-creation-client'; import type { AdminRoomCreationInput, AdminRoomCreationResult } from '@/lib/admin-room-creation'; type Props = { accountId: string; onClose: () => void; onCreated: (code: string) => void; onDenied: () => void; onBusyChange: (busy: boolean) => void }; const factions = FACTIONS.filter(faction => faction.expansion === 'base'); const difficulties = ['Easy', 'Medium', 'Hard', 'Brutal'] as const; const message = (error: unknown) => error instanceof Error ? error.message : 'The room could not be created.'; export function AdminRoomCreation({ accountId, onClose, onCreated, onDenied, onBusyChange }: Props) { const [name, setName] = useState(''); const [faction, setFaction] = useState('atreides'); const [advanced, setAdvanced] = useState(false); const [techTokens, setTechTokens] = useState(false); const [strongholdCards, setStrongholdCards] = useState(false); const [bots, setBots] = useState([]); const [reason, setReason] = useState(''); const [confirmed, setConfirmed] = useState(false); const [record, setRecord] = useState(null); const [storageProblem, setStorageProblem] = useState(false); const [notice, setNotice] = useState(''); const [loading, setLoading] = useState(true); const [busy, setBusy] = useState(false); const [initialRequestPending, setInitialRequestPending] = useState(false); const alive = useRef(false), inFlight = useRef(false); const heading = useRef(null); useEffect(() => { alive.current = true; heading.current?.focus(); try { const saved = readAdminRoomCreation(window.sessionStorage, accountId); // oxlint-disable-next-line react/react-compiler -- Restore tab-scoped creation proof after SSR. setRecord(saved); if (saved?.kind === 'pending') setNotice('This tab has an unconfirmed room request. Retry it to find the original room.'); } catch (error) { setStorageProblem(true); setNotice(message(error)); } setLoading(false); return () => { alive.current = false; }; }, [accountId]); async function submit(saved?: AdminRoomCreationInput) { if (inFlight.current || storageProblem || (!saved && (!confirmed || record))) return; let input: AdminRoomCreationInput; try { input = saved ?? newAdminRoomCreationRequest({ name, faction, advanced, techTokens, strongholdCards, bots, reason }); saveAdminRoomCreation(window.sessionStorage, accountId, input); } catch (error) { setNotice(message(error)); return; } inFlight.current = true; setInitialRequestPending(!saved); setBusy(true); onBusyChange(true); setRecord({ kind: 'pending', input }); setNotice('Creating your room…'); try { const result = await requestJson('/api/admin/rooms', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Dune-Admin-Id': accountId }, body: JSON.stringify(input), }); if (validAdminRoomCreationResult(result) && result.operationId === input.operationId && result.roomRemoved) { if (alive.current) { setNotice(`Room ${result.code} exists but has been removed. Its exact creation proof is still saved in this tab. Restore the room in Administration, then retry this same request to confirm host access.`); setConfirmed(false); onCreated(result.code); } return; } completeAdminRoomCreation(window.sessionStorage, accountId, input, result); if (alive.current) { setRecord({ kind: 'completed', result }); setConfirmed(false); setNotice(result.replayed ? 'The original room has been found. No duplicate room was created.' : 'Room created. The lobby is ready to configure and invite players.'); onCreated(result.code); } } catch (error) { let cleared = false; if (!retainAdminRoomRequest(error)) { try { clearAdminRoomCreation(window.sessionStorage, accountId); cleared = true; } catch { /* Preserve exact retry if storage is unavailable. */ } } if (alive.current) { if (cleared) setRecord(null); setConfirmed(false); setNotice(requestMayHaveCompleted(error) ? `${message(error)} The room may already exist. Retry the saved request to confirm it.` : message(error)); if (error instanceof ClientRequestError && [401, 403].includes(error.status ?? 0)) onDenied(); } } finally { inFlight.current = false; onBusyChange(false); if (alive.current) { setInitialRequestPending(false); setBusy(false); } } } function clearRecord() { if (busy || (storageProblem && !confirmed)) return; try { clearAdminRoomCreation(window.sessionStorage, accountId); setRecord(null); setStorageProblem(false); setConfirmed(false); setNotice('Local creation record cleared. Existing rooms and saved seats are unchanged.'); } catch (error) { setNotice(message(error)); } } function addBot() { const next = factions.find(item => item.id !== faction && !bots.some(bot => bot.faction === item.id)); if (next) { setBots([...bots, { faction: next.id, difficulty: 'Medium' }]); setConfirmed(false); } } const result = record?.kind === 'completed' ? record.result : null; return

Create a room

{notice && {notice}} {loading ? Checking this tab’s saved creation request… : storageProblem ?

A room may already have been created. Check the directory before removing this unreadable record. Removing it cannot cancel creation and may lose this tab’s host-seat retry proof.

: initialRequestPending ? null : record?.kind === 'pending' ?

Unconfirmed room: host {record.input.name} · {record.input.advanced ? 'Advanced preview' : 'Basic'} · {record.input.bots.length} AI players.

The exact request is kept privately in this tab across refresh. Keep the tab and its storage until the result is confirmed. Retrying finds the same room.

: result ?

Room {result.code}

Share invitation code {result.code}. Players can enter it on the home page to join an available faction.

{result.hostAccess ? <>

When creation was confirmed, this browser received the host seat. Open the lobby to change settings, invite human players, or use Pass your seat to another player to give someone else the host seat.

Open your host seat :

The original host seat has since been transferred or its access revoked. This confirmation does not restore old seat access.

}
:
{ event.preventDefault(); void submit(); }}>

Create a lobby with a new human host seat in this browser. Players join by invitation; AI seats can be included below. Nothing starts until the host and players complete normal readiness.

{advanced &&

Advanced preview is unfinished. Only the six classic factions are available, and starting requires the existing preview confirmation.

}
Initial AI players

The other seats remain available for human players. AI strategy tuning is still in development.

{bots.map((bot, index) =>
)}

Use an operational reason; leave out credentials and private game information.

}
; }