1
0
Fork 0
polaris-task-force/src/components/frontend/minigames/poker/PokerGameTable.tsx
2026-09-14 19:24:59 -04:00

219 lines
7.5 KiB
TypeScript

"use client";
import { createPortal } from "react-dom";
import { useEffect, useMemo, useState } from "react";
import { Maximize2, Minimize2 } from "lucide-react";
import type { PokerPhase } from "@/lib/minigames/poker";
import { describeHandStrength } from "@/lib/minigames/poker/handHint";
import type { PokerSnapshot } from "@/app/(frontend)/minigames/poker/actions";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Card as UiCard, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { PokerCardStyleToggle } from "./PokerCardStyleToggle";
import { PokerDevPanel } from "./PokerDevPanel";
import { PokerFeltCanvas, type SeatedSnapshot } from "./PokerFeltCanvas";
import { PokerSoundToggle } from "./PokerSoundToggle";
import type { PokerSoundControls } from "./usePokerSounds";
import type { ActivePokerPopup } from "./usePokerPopups";
import type { PokerActionLogEntry } from "./pokerPopups";
import { RenameDialogGuard } from "./RenameDialogGuard";
interface PokerGameTableProps {
readonly snapshot: SeatedSnapshot;
readonly onSnapshot: (snapshot: PokerSnapshot) => void;
readonly onStart: () => Promise<void>;
readonly onLeave: () => Promise<void>;
readonly onJoin: () => Promise<void>;
readonly sound: PokerSoundControls;
readonly popups: readonly ActivePokerPopup[];
readonly actionLog: readonly PokerActionLogEntry[];
readonly isSuperuser: boolean;
readonly superuserIds: readonly number[];
}
const PHASE_LABELS: Record<PokerPhase, string> = {
betting: "Betting",
showdown: "Showdown",
"hand-complete": "Hand complete",
"game-complete": "Game complete",
};
export function PokerGameTable({
snapshot,
onSnapshot,
onStart,
onLeave,
onJoin,
sound,
popups,
actionLog,
isSuperuser,
superuserIds,
}: PokerGameTableProps) {
const [starting, setStarting] = useState(false);
const [leaving, setLeaving] = useState(false);
const [fullscreen, setFullscreen] = useState(false);
const [mounted, setMounted] = useState(false);
const view = snapshot.you;
const handHint = useMemo(
() =>
view.phase === "game-complete"
? null
: describeHandStrength(view.yourHoleCards, view.communityCards),
[view.communityCards, view.phase, view.yourHoleCards],
);
const bestCardKeys = useMemo(() => new Set(handHint?.bestCardKeys ?? []), [handHint]);
const handTier = handHint?.tier ?? null;
useEffect(() => {
setMounted(true);
}, []);
useEffect(() => {
if (!fullscreen) return;
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === "Escape") setFullscreen(false);
};
window.addEventListener("keydown", handleKeyDown);
return () => window.removeEventListener("keydown", handleKeyDown);
}, [fullscreen]);
const start = async () => {
setStarting(true);
await onStart();
setStarting(false);
};
const leave = async () => {
setLeaving(true);
await onLeave();
setLeaving(false);
};
const tableName = snapshot.name ?? `Table ${snapshot.tableId}`;
const spectator = Boolean(snapshot.spectator);
const feltCanvas = (
<PokerFeltCanvas
snapshot={snapshot}
onSnapshot={onSnapshot}
handLabel={handHint?.label ?? null}
handTier={handTier}
bestCardKeys={bestCardKeys}
actionProps={{
starting,
leaving,
onStart: () => void start(),
onLeave: () => void leave(),
}}
onActionFeedback={sound.playUiClick}
popups={popups}
actionLog={actionLog}
spectator={spectator}
onJoin={onJoin}
superuserIds={superuserIds}
fullscreen={fullscreen}
/>
);
if (fullscreen) {
if (!mounted) return null;
return createPortal(
<div className="fixed inset-0 z-50 flex flex-col gap-3 overflow-hidden bg-black/92 p-4 backdrop-blur-sm">
<div className="flex shrink-0 flex-wrap items-center justify-between gap-3 border border-white/15 bg-black/60 p-3 backdrop-blur-sm">
<div className="flex min-w-0 items-center gap-2">
<span className="truncate font-semibold text-white">{tableName}</span>
{spectator && <Badge variant="secondary">Spectating</Badge>}
<Badge variant="outline">{PHASE_LABELS[view.phase]}</Badge>
</div>
<div className="flex flex-wrap items-center justify-end gap-2">
{isSuperuser && <PokerDevPanel tableId={snapshot.tableId} view={view} />}
<PokerCardStyleToggle />
<PokerSoundToggle
muted={sound.muted}
volume={sound.volume}
onToggleMuted={sound.toggleMuted}
onVolumeChange={sound.setVolume}
/>
<Button
type="button"
variant="outline"
size="icon"
onClick={() => setFullscreen(false)}
aria-label="Exit fullscreen table"
title="Exit fullscreen table"
>
<Minimize2 aria-hidden="true" />
</Button>
{view.phase !== "game-complete" && (
<Button
type="button"
variant="outline"
onClick={() => void leave()}
disabled={leaving}
>
{leaving ? "Leaving..." : spectator ? "Stop watching" : "Leave table"}
</Button>
)}
</div>
</div>
<div className="relative min-h-0 flex-1">{feltCanvas}</div>
</div>,
document.body,
);
}
return (
<UiCard className="rounded-none">
<CardHeader className="flex-row items-start justify-between gap-4 border-b p-4">
<div className="flex min-w-0 flex-col gap-1">
<div className="flex flex-wrap items-center gap-2">
<CardTitle className="text-base">{tableName}</CardTitle>
<RenameDialogGuard
canRename={view.viewerSeat === snapshot.hostSeatId}
tableId={snapshot.tableId}
currentName={snapshot.name}
onSnapshot={onSnapshot}
/>
{spectator && <Badge variant="secondary">Spectating</Badge>}
<Badge variant="outline">{PHASE_LABELS[view.phase]}</Badge>
</div>
<p className="font-mono text-xs tabular-nums text-muted-foreground">
Hand {view.handNumber} · Blinds {view.config.smallBlind}/{view.config.bigBlind}
</p>
</div>
<div className="flex shrink-0 items-center gap-2">
<span className="font-mono text-xs tabular-nums text-muted-foreground">
v{snapshot.version}
</span>
{isSuperuser && <PokerDevPanel tableId={snapshot.tableId} view={view} />}
<PokerCardStyleToggle />
<PokerSoundToggle
muted={sound.muted}
volume={sound.volume}
onToggleMuted={sound.toggleMuted}
onVolumeChange={sound.setVolume}
/>
<Button
type="button"
variant="outline"
size="icon"
onClick={() => setFullscreen(true)}
aria-label="Enter fullscreen table"
title="Enter fullscreen table"
>
<Maximize2 aria-hidden="true" />
</Button>
{view.phase !== "game-complete" && (
<Button type="button" variant="outline" onClick={() => void leave()} disabled={leaving}>
{leaving ? "Leaving..." : spectator ? "Stop watching" : "Leave table"}
</Button>
)}
</div>
</CardHeader>
<CardContent className="p-4">{feltCanvas}</CardContent>
</UiCard>
);
}