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

193 lines
6.3 KiB
TypeScript

"use client";
import { useEffect, useState } from "react";
import type { PokerActionKind, PokerLegalActions } from "@/lib/minigames/poker";
import { pokerAction, type PokerSnapshot } from "@/app/(frontend)/minigames/poker/actions";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
interface BettingControlsProps {
readonly tableId: string;
readonly snapshotVersion: number;
readonly legalActions: PokerLegalActions;
readonly potTotal: number;
readonly yourTurn: boolean;
readonly onSnapshot: (snapshot: PokerSnapshot) => void;
}
function clampAmount(value: number, min: number, max: number): number {
if (!Number.isFinite(value)) return min;
return Math.min(max, Math.max(min, Math.round(value)));
}
function amountLabel(value: number): string {
return value.toLocaleString();
}
export function BettingControls({
tableId,
snapshotVersion,
legalActions,
potTotal,
yourTurn,
onSnapshot,
}: BettingControlsProps) {
const [pendingAction, setPendingAction] = useState<PokerActionKind | null>(null);
const [error, setError] = useState<string | null>(null);
const [amount, setAmount] = useState(legalActions.minBetTo);
const wagerKind: PokerActionKind | null = legalActions.canBet
? "bet"
: legalActions.canRaise
? "raise"
: null;
const wagerMin = legalActions.canBet ? legalActions.minBetTo : legalActions.minRaiseTo;
const wagerMax = legalActions.canBet ? legalActions.maxBetTo : legalActions.maxRaiseTo;
const canAct = yourTurn && pendingAction === null;
useEffect(() => {
setAmount((current) => clampAmount(current, wagerMin, wagerMax));
}, [wagerMax, wagerMin]);
useEffect(() => {
setPendingAction(null);
setError(null);
}, [snapshotVersion]);
const submit = async (kind: PokerActionKind, wager?: number) => {
setError(null);
setPendingAction(kind);
try {
const result = await pokerAction(tableId, kind, wager);
if (!result.success) {
setError(result.error ?? "That action was rejected by the table.");
setPendingAction(null);
return;
}
if (result.data) onSnapshot(result.data);
} catch (caught) {
setError(caught instanceof Error ? caught.message : "Could not send the table action.");
setPendingAction(null);
}
};
const chooseQuickAmount = (choice: "min" | "half" | "pot" | "all") => {
const nextAmount =
choice === "min"
? wagerMin
: choice === "half"
? Math.round(potTotal / 2)
: choice === "pot"
? potTotal
: wagerMax;
setAmount(clampAmount(nextAmount, wagerMin, wagerMax));
};
return (
<div className="flex flex-col gap-3">
<div className="flex flex-wrap gap-2">
<Button
type="button"
variant="destructive"
disabled={!canAct || !legalActions.canFold}
onClick={() => void submit("fold")}
>
Fold
</Button>
<Button
type="button"
variant="outline"
disabled={!canAct || !legalActions.canCheck}
onClick={() => void submit("check")}
>
Check
</Button>
<Button
type="button"
variant="secondary"
disabled={!canAct || !legalActions.canCall}
onClick={() => void submit("call")}
>
Call {amountLabel(legalActions.callAmount)}
</Button>
</div>
{wagerKind && wagerMax >= wagerMin && (
<Card className="rounded-none">
<CardHeader className="p-4 pb-2">
<CardTitle className="text-sm">
{wagerKind === "bet" ? "Bet size" : "Raise to"}
</CardTitle>
</CardHeader>
<CardContent className="flex flex-col gap-3 p-4 pt-0">
<div className="flex items-center gap-3">
<input
type="range"
min={wagerMin}
max={wagerMax}
step={1}
value={amount}
onChange={(event) =>
setAmount(clampAmount(Number(event.target.value), wagerMin, wagerMax))
}
disabled={!canAct}
className="h-2 min-w-0 flex-1 cursor-pointer accent-primary disabled:cursor-not-allowed"
aria-label={`${wagerKind === "bet" ? "Bet" : "Raise"} amount`}
/>
<Input
type="number"
min={wagerMin}
max={wagerMax}
step={1}
value={amount}
onChange={(event) =>
setAmount(clampAmount(Number(event.target.value), wagerMin, wagerMax))
}
disabled={!canAct}
className="w-24 font-mono tabular-nums"
aria-label="Wager amount"
/>
</div>
<div className="flex flex-wrap gap-2">
{(["min", "half", "pot", "all"] as const).map((choice) => (
<Button
key={choice}
type="button"
variant="outline"
size="sm"
disabled={!canAct}
onClick={() => chooseQuickAmount(choice)}
>
{choice === "min"
? "Min"
: choice === "half"
? "1/2 pot"
: choice === "pot"
? "Pot"
: "All-in"}
</Button>
))}
<Button
type="button"
className="ml-auto"
disabled={!canAct}
onClick={() => void submit(wagerKind, amount)}
>
{wagerKind === "bet" ? "Bet" : "Raise"} {amountLabel(amount)}
</Button>
</div>
</CardContent>
</Card>
)}
<div className="flex min-h-5 items-center justify-between gap-3 text-xs" aria-live="polite">
{!yourTurn && <span className="text-muted-foreground">Waiting for the table to act.</span>}
{pendingAction && (
<span className="text-muted-foreground">Waiting for the table update...</span>
)}
{error && <span className="text-destructive">{error}</span>}
</div>
</div>
);
}