"use client"; import { useCallback, useEffect, useRef, useState } from "react"; import { fetchPokerTable, joinPokerTable, leavePokerTable, startPokerGame, type PokerSnapshot, } from "@/app/(frontend)/minigames/poker/actions"; import type { PokerPhase } from "@/lib/minigames/poker"; import { useRealtimePresence } from "@/hooks/useRealtimePresence"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { PokerGuide } from "./PokerGuide"; import { POKER_STATE_EVENT, POKER_TABLES_EVENT, POKER_TERMINATED_EVENT, readPokerStateEvent, readPokerTerminatedEvent, } from "./events"; import { PokerLeaderboard } from "./PokerLeaderboard"; import { PokerLobby } from "./PokerLobby"; import { PokerTable } from "./PokerTable"; import type { PokerStats } from "./types"; interface PokerArenaProps { readonly initialStats: PokerStats; readonly currentUserId: number | string; readonly initialTableId?: string; readonly isSuperuser: boolean; } type ArenaView = "lobby" | "table"; const STAT_ITEMS = [ ["Games", "gamesPlayed"], ["Wins", "wins"], ["Hands won", "handsWon"], ["Best chips", "bestScore"], ["XP earned", "xpEarned"], ] as const satisfies readonly (readonly [string, keyof PokerStats])[]; export function PokerArena({ initialStats, currentUserId, initialTableId, isSuperuser, }: PokerArenaProps) { const [view, setView] = useState(initialTableId ? "table" : "lobby"); const [snapshot, setSnapshot] = useState(null); const [activeTableId, setActiveTableId] = useState(initialTableId ?? null); const [tableError, setTableError] = useState(null); const [loading, setLoading] = useState(Boolean(initialTableId)); const [terminated, setTerminated] = useState(null); const [leaderboardRefreshKey, setLeaderboardRefreshKey] = useState(0); const [lobbyRefreshKey, setLobbyRefreshKey] = useState(0); const lastPhaseRef = useRef(null); const syncedTableIdRef = useRef(null); const applySnapshot = useCallback((next: PokerSnapshot) => { setSnapshot((current) => { if (current && next.version <= current.version) return current; return next; }); setActiveTableId(next.tableId); setTerminated((current) => (current !== next.tableId ? null : current)); if (syncedTableIdRef.current !== next.tableId) { syncedTableIdRef.current = next.tableId; window.history.replaceState( null, "", `/minigames/poker?table=${encodeURIComponent(next.tableId)}`, ); } setView("table"); setTableError(null); setLoading(false); const nextPhase = next.you?.phase ?? null; if (nextPhase === "game-complete" && lastPhaseRef.current !== "game-complete") { setLeaderboardRefreshKey((key) => key + 1); } lastPhaseRef.current = nextPhase; }, []); useEffect(() => { if (!initialTableId) { setLoading(false); return; } let cancelled = false; setLoading(true); void fetchPokerTable(initialTableId) .then((result) => { if (cancelled) return; if (result.success && result.data) { applySnapshot(result.data); } else { setTableError(result.error ?? "This poker table was not found."); setLoading(false); } }) .catch((caught: unknown) => { if (cancelled) return; setTableError( caught instanceof Error ? caught.message : "This poker table could not be loaded.", ); setLoading(false); }); return () => { cancelled = true; }; }, [applySnapshot, initialTableId]); useEffect(() => { const handleState = (event: Event) => { const detail = readPokerStateEvent(event); if (!detail || detail.tableId !== activeTableId) return; applySnapshot(detail.snapshot); }; const handleTables = () => setLobbyRefreshKey((key) => key + 1); const handleTerminated = (event: Event) => { const detail = readPokerTerminatedEvent(event); if (!detail || detail.tableId !== activeTableId) return; setTerminated(detail.tableId); }; window.addEventListener(POKER_STATE_EVENT, handleState); window.addEventListener(POKER_TABLES_EVENT, handleTables); window.addEventListener(POKER_TERMINATED_EVENT, handleTerminated); return () => { window.removeEventListener(POKER_STATE_EVENT, handleState); window.removeEventListener(POKER_TABLES_EVENT, handleTables); window.removeEventListener(POKER_TERMINATED_EVENT, handleTerminated); }; }, [activeTableId, applySnapshot]); // Polls are the correctness path: the in-memory SSE bus cannot reliably // deliver server-action pushes (dev per-bundle module duplication). The // SSE push stays as an instant bonus; applySnapshot dedupes by version. useEffect(() => { if (view !== "table" || activeTableId === null || terminated === activeTableId) return; let cancelled = false; const poll = window.setInterval(() => { void fetchPokerTable(activeTableId) .then((result) => { if (cancelled) return; if (result.success && result.data) applySnapshot(result.data); }) .catch(() => undefined); }, 1250); return () => { cancelled = true; window.clearInterval(poll); }; }, [activeTableId, terminated, view, applySnapshot]); const resetToLobby = useCallback(() => { setView("lobby"); setSnapshot(null); setActiveTableId(null); setTableError(null); setLoading(false); setTerminated(null); lastPhaseRef.current = null; if (syncedTableIdRef.current !== null) { syncedTableIdRef.current = null; window.history.replaceState(null, "", "/minigames/poker"); } }, []); const leaveTable = useCallback(async () => { const seated = snapshot?.seats.some((id) => String(id) === String(currentUserId)) ?? false; if (activeTableId && seated) { try { const result = await leavePokerTable(activeTableId); if (!result.success) { setTableError(result.error ?? "Could not leave the poker table."); return; } } catch (caught) { setTableError( caught instanceof Error ? caught.message : "Could not leave the poker table.", ); return; } } resetToLobby(); }, [activeTableId, currentUserId, resetToLobby, snapshot]); const joinCurrentTable = useCallback(async () => { if (!activeTableId) return; setLoading(true); setTableError(null); try { const result = await joinPokerTable(activeTableId); if (result.success && result.data) { applySnapshot(result.data); } else { setTableError(result.error ?? "Could not join the poker table."); setLoading(false); } } catch (caught) { setTableError(caught instanceof Error ? caught.message : "Could not join the poker table."); setLoading(false); } }, [activeTableId, applySnapshot]); const startCurrentTable = useCallback(async () => { if (!activeTableId) return; try { const result = await startPokerGame(activeTableId); if (result.success && result.data) { applySnapshot(result.data); } else { setTableError(result.error ?? "Could not start the poker game."); } } catch (caught) { setTableError(caught instanceof Error ? caught.message : "Could not start the poker game."); } }, [activeTableId, applySnapshot]); const activePlayers = useRealtimePresence( "poker", view === "table" && activeTableId !== null, true, ); return (
Your poker record

Results are recorded when a sit-and-go ends.

{STAT_ITEMS.map(([label, key]) => (
{label} {initialStats[key].toLocaleString()}
))}
{tableError && snapshot && (

{tableError}

)}
{view === "lobby" ? ( ) : terminated === activeTableId ? ( Game ended by an administrator

An administrator forcefully ended this poker table.

) : ( )}
); }