'use client'; import { useEffect, useRef, useState } from 'react'; import { Button } from './ui/button'; import { Input } from './ui/input'; import { ClientRequestError, requestJson, requestMayHaveCompleted } from '@/lib/client-request'; import { retainAdminRoomRequest } from '@/lib/admin-room-control-client'; import { adminArchiveResponse, adminArchiveConfirmation, clearAdminArchiveRequest, newAdminArchiveRequest, readAdminArchiveRequest, saveAdminArchiveRequest } from '@/lib/admin-archive-client'; import type { AdminArchiveInput, AdminArchiveView } from '@/lib/admin-archive'; type Props = { accountId: string; code: string; canManage: boolean; onClose: () => void; onUpdated: () => void; onDenied: () => void; onBusyChange: (busy: boolean) => void; }; const message = (error: unknown) => error instanceof Error ? error.message : 'Room availability could not be loaded.'; export function AdminRoomArchive({ accountId, code, canManage, onClose, onUpdated, onDenied, onBusyChange }: Props) { const [room, setRoom] = useState(null); const [reason, setReason] = useState(''); const [confirmed, setConfirmed] = useState(false); const [confirmationCode, setConfirmationCode] = useState(''); const [pending, setPending] = useState(null); const [storageProblem, setStorageProblem] = useState(false); const [notice, setNotice] = useState(''); const [busy, setBusy] = useState(false); const [initialRequestPending, setInitialRequestPending] = useState(false); const [loading, setLoading] = useState(true); const alive = useRef(false), inFlight = useRef(false); const title = useRef(null); const url = `/api/admin/rooms/${code}/archive`; useEffect(() => { alive.current = true; title.current?.focus(); let canceled = false; try { const saved = readAdminArchiveRequest(window.sessionStorage, accountId, code); // oxlint-disable-next-line react/react-compiler -- Restore the account/room's tab-scoped request after SSR. setPending(saved); if (saved) setNotice('A saved archive or unarchive request needs confirmation. Review it below, then retry the same request.'); } catch (error) { setStorageProblem(true); setNotice(message(error)); } requestJson(url, { cache: 'no-store', headers: { 'X-Dune-Admin-Id': accountId } }) .then(result => { const current = adminArchiveResponse(result, code); if (!canceled) setRoom(current); }) .catch(error => { if (!canceled) { setNotice(message(error)); if (error instanceof ClientRequestError && [401, 403].includes(error.status ?? 0)) onDenied(); } }) .finally(() => { if (!canceled) setLoading(false); }); return () => { canceled = true; alive.current = false; }; }, [accountId, code, url, onDenied]); async function refresh() { if (inFlight.current) return; inFlight.current = true; setBusy(true); onBusyChange(true); try { const current = adminArchiveResponse(await requestJson(url, { cache: 'no-store', headers: { 'X-Dune-Admin-Id': accountId } }), code); if (alive.current) { setRoom(current); setConfirmed(false); setConfirmationCode(''); } } catch (error) { if (alive.current) { setNotice(message(error)); if (error instanceof ClientRequestError && [401, 403].includes(error.status ?? 0)) onDenied(); } } finally { inFlight.current = false; onBusyChange(false); if (alive.current) setBusy(false); } } async function submit(saved?: AdminArchiveInput) { if (inFlight.current || loading || !canManage || storageProblem || (!saved && (!room || room.removed || !room.closed || pending || !confirmed || (!room.archived && confirmationCode !== code)))) return; let input: AdminArchiveInput; try { input = saved ?? newAdminArchiveRequest(room!, !room!.archived, reason); saveAdminArchiveRequest(window.sessionStorage, accountId, code, input); } catch (error) { setNotice(message(error)); return; } inFlight.current = true; setInitialRequestPending(!saved); setBusy(true); onBusyChange(true); setPending(input); setNotice(input.archived ? 'Archiving room…' : 'Unarchiving room…'); try { const result = adminArchiveConfirmation(await requestJson(url, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Dune-Admin-Id': accountId }, body: JSON.stringify(input), }), code, input); clearAdminArchiveRequest(window.sessionStorage, accountId, code); if (alive.current) { setPending(null); setRoom(result.room); setConfirmed(false); setConfirmationCode(''); setReason(''); setNotice(result.replayed ? 'The original request was already recorded. Current room availability is shown below.' : input.archived ? 'Room archived. It remains closed and readable to existing players. Find it with the Archive filter.' : 'Room unarchived. It remains closed until an administrator separately reopens it.'); onUpdated(); } } catch (error) { let cleared = false; if (!retainAdminRoomRequest(error)) { try { clearAdminArchiveRequest(window.sessionStorage, accountId, code); cleared = true; } catch { /* Keep the exact retry after storage failure. */ } } if (alive.current) { if (cleared) setPending(null); setConfirmed(false); setConfirmationCode(''); setNotice(requestMayHaveCompleted(error) ? `${message(error)} The change may have been saved. Use Retry saved request to confirm.` : message(error)); if (error instanceof ClientRequestError && [401, 403].includes(error.status ?? 0)) onDenied(); else try { const current = adminArchiveResponse(await requestJson(url, { cache: 'no-store', headers: { 'X-Dune-Admin-Id': accountId } }), code); if (alive.current) setRoom(current); } catch { /* Preserve the original outcome and saved retry. */ } } } finally { inFlight.current = false; onBusyChange(false); if (alive.current) { setInitialRequestPending(false); setBusy(false); } } } function discardUnreadableRecord() { if (!canManage || !room || !confirmed || busy) return; try { clearAdminArchiveRequest(window.sessionStorage, accountId, code); setStorageProblem(false); setConfirmed(false); setNotice('Local retry record removed. This did not undo any server change. Review current room availability before continuing.'); } catch (error) { setNotice(message(error)); } } return

Archive or unarchive · {code}

{notice && {notice}} {loading ? Loading room archive… : <> {room ? <>

Archive status: {room.archived ? 'Archived' : 'Unarchived'}. Room availability: {room.removed ? 'Removed' : room.closed ? 'Closed' : 'Open'}.

Archiving organizes a closed room without changing its saved game, credentials or discussion. Existing players keep their readable table and history.

{room.archivedAt !== null &&

Archived at: .

} : } {!canManage ?

Your viewer role can inspect archive status. An owner or operator can archive or unarchive rooms.

: storageProblem ?

No new changes can be sent until this tab’s unreadable retry record is resolved. Discarding the record does not cancel a server change.

: initialRequestPending ? null : pending ?

Saved request: {pending.archived ? 'Archive' : 'Unarchive'} room {code}.

Reason: {pending.reason}

The exact request stays in this tab across refresh. Retrying confirms the original change without applying it again; the current archive status may differ after another administrator’s action.

After refresh or sign-in, choose All rooms in both the Directory and Archive filters to find the room and reopen these controls.

: room?.removed ?

Restore this removed room before changing its archive status. Any saved archive request can still be confirmed above.

: room && !room.closed ?

Use Close or reopen to close this room before archiving it. Archiving does not interrupt play or assign an outcome.

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

Use a short operational reason. Do not include credentials or private game information.

{room.archived ? <>

Return room {code} to the unarchived directory. Its game, seats, history, pause and joining settings remain saved.

Unarchiving leaves the room closed. Reopen it separately when play should resume.

: <>

Archive closed room {code}. It leaves the default administrator list and remains available under Archived rooms or All rooms in the Archive filter.

Players can still read their saved table and history. The room stays closed to further play; archiving does not delete it or change its winner.

}
} }
; }