1
0
Fork 0
polaris-task-force/src/components/frontend/minigames/poker/PokerTable.tsx
2026-09-11 03:25:43 -04:00

219 lines
6.7 KiB
TypeScript

"use client";
import { useState } from "react";
import { cn } from "@/lib/utils";
import type { PokerSnapshot } from "@/app/(frontend)/minigames/poker/actions";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
import { Skeleton } from "@/components/ui/skeleton";
import { PokerGameTable } from "./PokerGameTable";
import { RenameTableDialog } from "./RenameTableDialog";
import { seatPosition } from "./seatPosition";
interface PokerTableProps {
readonly snapshot: PokerSnapshot | null;
readonly currentUserId: number | string;
readonly loading: boolean;
readonly tableError: string | null;
readonly onSnapshot: (snapshot: PokerSnapshot) => void;
readonly onJoin: () => Promise<void>;
readonly onStart: () => Promise<void>;
readonly onLeave: () => Promise<void>;
readonly onBack: () => void;
}
type SeatedSnapshot = PokerSnapshot & {
readonly you: NonNullable<PokerSnapshot["you"]>;
};
function isSeatedSnapshot(snapshot: PokerSnapshot): snapshot is SeatedSnapshot {
return snapshot.you !== null;
}
function WaitingTable({
snapshot,
currentUserId,
onSnapshot,
onJoin,
onStart,
onLeave,
onBack,
}: {
readonly snapshot: PokerSnapshot;
readonly currentUserId: number | string;
readonly onSnapshot: (snapshot: PokerSnapshot) => void;
readonly onJoin: () => Promise<void>;
readonly onStart: () => Promise<void>;
readonly onLeave: () => Promise<void>;
readonly onBack: () => void;
}) {
const [starting, setStarting] = useState(false);
const [leaving, setLeaving] = useState(false);
const viewerSeat = snapshot.seats.findIndex((id) => String(id) === String(currentUserId));
const seated = viewerSeat >= 0;
const full = snapshot.seats.length > 0 && snapshot.seats.every((id) => id !== null);
const isHost = seated && snapshot.hostSeatId === viewerSeat;
const start = async () => {
setStarting(true);
await onStart();
setStarting(false);
};
const leave = async () => {
setLeaving(true);
await onLeave();
setLeaving(false);
};
return (
<Card className="rounded-none">
<CardHeader className="flex-row items-start justify-between gap-4 border-b p-4">
<div className="flex flex-col gap-1">
<div className="flex items-center gap-1">
<CardTitle className="text-base">
{snapshot.name ?? `Table ${snapshot.tableId}`}
</CardTitle>
{isHost && (
<RenameTableDialog
tableId={snapshot.tableId}
currentName={snapshot.name}
onSnapshot={onSnapshot}
/>
)}
</div>
<p className="text-sm text-muted-foreground">
{seated
? "You have a seat reserved."
: "Join an open seat to receive a private hand view."}
</p>
</div>
<Badge variant="outline">Waiting room</Badge>
</CardHeader>
<CardContent className="p-4">
<div className="poker-felt-table relative min-h-[28rem] overflow-hidden p-4 sm:min-h-[34rem]">
{snapshot.seats.map((userId, seatId) => (
<div
key={seatId}
className={cn(
"poker-waiting-seat absolute flex flex-col gap-1 border px-3 py-2 text-xs",
seatPosition(seatId),
userId !== null && "poker-waiting-seat-occupied",
)}
>
<span className="font-semibold">Seat {seatId + 1}</span>
<span className="text-white/70">
{userId === null
? "Open seat"
: String(userId) === String(currentUserId)
? "You"
: "Occupied"}
</span>
</div>
))}
<div className="absolute left-1/2 top-1/2 flex -translate-x-1/2 -translate-y-1/2 flex-col items-center gap-2 text-center text-white">
<span className="font-mono text-xs uppercase tracking-[0.2em] text-white/60">
Polaris Hold&apos;em
</span>
<span className="text-sm text-white/80">
{seated ? "Waiting for the host to start" : "Open seating"}
</span>
</div>
</div>
</CardContent>
<CardFooter className="flex flex-wrap justify-end gap-2 border-t p-4">
{seated ? (
<>
{isHost && (
<Button type="button" onClick={() => void start()} disabled={starting}>
{starting ? "Starting..." : "Start game"}
</Button>
)}
<Button type="button" variant="outline" onClick={() => void leave()} disabled={leaving}>
{leaving ? "Leaving..." : "Leave table"}
</Button>
</>
) : (
<>
<Button type="button" onClick={() => void onJoin()} disabled={full}>
{full ? "Table full" : "Join an open seat"}
</Button>
<Button type="button" variant="outline" onClick={onBack}>
Back to lobby
</Button>
</>
)}
</CardFooter>
</Card>
);
}
export function PokerTable({
snapshot,
currentUserId,
loading,
tableError,
onSnapshot,
onJoin,
onStart,
onLeave,
onBack,
}: PokerTableProps) {
if (loading && !snapshot) {
return (
<Card className="rounded-none">
<CardHeader className="gap-3 p-4">
<Skeleton className="h-5 w-40" />
<Skeleton className="h-4 w-64" />
</CardHeader>
<CardContent className="p-4 pt-0">
<Skeleton className="h-[28rem] w-full" />
</CardContent>
</Card>
);
}
if (!snapshot) {
return (
<Card className="rounded-none">
<CardHeader className="p-4">
<CardTitle className="text-base">Table unavailable</CardTitle>
</CardHeader>
<CardContent className="p-4 pt-0">
<p className="text-sm text-destructive">
{tableError ?? "This table could not be loaded."}
</p>
</CardContent>
<CardFooter className="border-t p-4">
<Button type="button" variant="outline" onClick={onBack}>
Back to lobby
</Button>
</CardFooter>
</Card>
);
}
if (!isSeatedSnapshot(snapshot)) {
return (
<WaitingTable
snapshot={snapshot}
currentUserId={currentUserId}
onSnapshot={onSnapshot}
onJoin={onJoin}
onStart={onStart}
onLeave={onLeave}
onBack={onBack}
/>
);
}
return (
<PokerGameTable
snapshot={snapshot}
onSnapshot={onSnapshot}
onStart={onStart}
onLeave={onLeave}
/>
);
}