"use client"; import { useCallback, useEffect, useRef, useState } from "react"; import { toast } from "sonner"; import { fetchBlackjackTable, joinBlackjackTable, leaveBlackjackTable, spectateBlackjackTable, startBlackjackGame, type BlackjackSnapshot, } from "@/app/(frontend)/minigames/blackjack/actions"; import type { BlackjackPhase } from "@/lib/minigames/blackjack"; import { useRealtimePresence } from "@/hooks/useRealtimePresence"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { ChipBankCard } from "@/components/frontend/minigames/ChipBankCard"; import { BlackjackGuide } from "./BlackjackGuide"; import { BLACKJACK_STATE_EVENT, BLACKJACK_TABLES_EVENT, BLACKJACK_TERMINATED_EVENT, readBlackjackStateEvent, readBlackjackTerminatedEvent, } from "./events"; import { BlackjackLeaderboard } from "./BlackjackLeaderboard"; import { BlackjackLobby } from "./BlackjackLobby"; import { BlackjackTable } from "./BlackjackTable"; import type { BlackjackStats } from "./types"; interface BlackjackArenaProps { readonly initialStats: BlackjackStats; readonly currentUserId: number | string; readonly initialTableId?: string; readonly isSuperuser: boolean; readonly superuserIds: readonly number[]; readonly initialChipBank: number; readonly initialChipTopUpAt: string | null; } type ArenaView = "lobby" | "table"; const STAT_ITEMS = [ ["Sessions", "gamesPlayed"], ["Rounds won", "roundsWon"], ["Blackjacks", "blackjacksHit"], ["Best bankroll", "bestScore"], ["XP earned", "xpEarned"], ] as const satisfies readonly (readonly [string, keyof BlackjackStats])[]; export function BlackjackArena({ initialStats, currentUserId, initialTableId, isSuperuser, superuserIds, initialChipBank, initialChipTopUpAt, }: BlackjackArenaProps) { 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 [leaderboardRefreshKey, setLeaderboardRefreshKey] = useState(0); const [lobbyRefreshKey, setLobbyRefreshKey] = useState(0); const lastRoundRef = useRef(null); const syncedTableIdRef = useRef(null); const applySnapshot = useCallback((next: BlackjackSnapshot) => { setSnapshot((current) => { if (current && next.version <= current.version) return current; return next; }); setActiveTableId(next.tableId); if (syncedTableIdRef.current !== next.tableId) { syncedTableIdRef.current = next.tableId; window.history.replaceState( null, "", `/minigames/blackjack?table=${encodeURIComponent(next.tableId)}`, ); } setView("table"); setTableError(null); setLoading(false); const nextRound = next.you?.roundNumber ?? null; if (nextRound !== null && lastRoundRef.current !== null && nextRound !== lastRoundRef.current) { setLeaderboardRefreshKey((key) => key + 1); } lastRoundRef.current = nextRound; }, []); useEffect(() => { if (!initialTableId) { setLoading(false); return; } let cancelled = false; setLoading(true); void fetchBlackjackTable(initialTableId) .then((result) => { if (cancelled) return; if (result.success && result.data) { applySnapshot(result.data); } else { setTableError(result.error ?? "This blackjack table was not found."); setLoading(false); } }) .catch((caught: unknown) => { if (cancelled) return; setTableError( caught instanceof Error ? caught.message : "This blackjack table could not be loaded.", ); setLoading(false); }); return () => { cancelled = true; }; }, [applySnapshot, initialTableId]); useEffect(() => { const handleState = (event: Event) => { const detail = readBlackjackStateEvent(event); if (!detail || detail.tableId !== activeTableId) return; applySnapshot(detail.snapshot); }; const handleTables = () => setLobbyRefreshKey((key) => key + 1); const handleTerminated = (event: Event) => { const detail = readBlackjackTerminatedEvent(event); if (!detail || detail.tableId !== activeTableId) return; toast.info("An administrator ended your blackjack game."); resetToLobby(); }; window.addEventListener(BLACKJACK_STATE_EVENT, handleState); window.addEventListener(BLACKJACK_TABLES_EVENT, handleTables); window.addEventListener(BLACKJACK_TERMINATED_EVENT, handleTerminated); return () => { window.removeEventListener(BLACKJACK_STATE_EVENT, handleState); window.removeEventListener(BLACKJACK_TABLES_EVENT, handleTables); window.removeEventListener(BLACKJACK_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) return; let cancelled = false; const poll = window.setInterval(() => { void fetchBlackjackTable(activeTableId) .then((result) => { if (cancelled) return; if (result.success && result.data) { applySnapshot(result.data); } else if (result.error === "Table not found.") { toast.info("An administrator ended your blackjack game."); resetToLobby(); } }) .catch(() => undefined); }, 1250); return () => { cancelled = true; window.clearInterval(poll); }; }, [activeTableId, view, applySnapshot]); const resetToLobby = useCallback(() => { setView("lobby"); setSnapshot(null); setActiveTableId(null); setTableError(null); setLoading(false); lastRoundRef.current = null; if (syncedTableIdRef.current !== null) { syncedTableIdRef.current = null; window.history.replaceState(null, "", "/minigames/blackjack"); } }, []); const leaveTable = useCallback(async () => { const seated = snapshot?.seats.some((id) => String(id) === String(currentUserId)) ?? false; if (activeTableId && seated) { try { window.dispatchEvent(new Event("chipBank:showSpinner")); const result = await leaveBlackjackTable(activeTableId); if (!result.success) { setTableError(result.error ?? "Could not leave the blackjack table."); return; } } catch (caught) { setTableError( caught instanceof Error ? caught.message : "Could not leave the blackjack table.", ); return; } } resetToLobby(); }, [activeTableId, currentUserId, resetToLobby, snapshot]); const joinCurrentTable = useCallback(async () => { if (!activeTableId) return; setLoading(true); setTableError(null); try { window.dispatchEvent(new Event("chipBank:showSpinner")); const result = await joinBlackjackTable(activeTableId); if (result.success && result.data) { applySnapshot(result.data); } else { setTableError(result.error ?? "Could not join the blackjack table."); setLoading(false); } } catch (caught) { setTableError( caught instanceof Error ? caught.message : "Could not join the blackjack table.", ); setLoading(false); } }, [activeTableId, applySnapshot]); const spectateCurrentTable = useCallback(async () => { if (!activeTableId) return; setLoading(true); setTableError(null); try { const result = await spectateBlackjackTable(activeTableId); if (result.success && result.data) { applySnapshot(result.data); } else { setTableError(result.error ?? "Could not spectate this table."); setLoading(false); } } catch (caught: unknown) { setTableError(caught instanceof Error ? caught.message : "Could not spectate this table."); setLoading(false); } }, [activeTableId, applySnapshot]); const startCurrentTable = useCallback(async () => { if (!activeTableId) return; try { window.dispatchEvent(new Event("chipBank:showSpinner")); const result = await startBlackjackGame(activeTableId); if (result.success && result.data) { applySnapshot(result.data); } else { setTableError(result.error ?? "Could not start the blackjack game."); } } catch (caught) { setTableError( caught instanceof Error ? caught.message : "Could not start the blackjack game.", ); } }, [activeTableId, applySnapshot]); const activePlayers = useRealtimePresence( "blackjack", view === "table" && activeTableId !== null, true, ); return (
Your blackjack record

Results are recorded when a session ends.

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

{tableError}

)}
{view === "lobby" ? ( ) : ( )}
); }