brackt/app/components/draft/DraftGridSection.tsx
Chris Parsons 41f3654956
Add commissioner replace pick and draft rollback features (#18)
Commissioners can now right-click any picked cell in the draft grid to
replace the drafted participant (available during and after draft) or
roll back the draft to that pick number (available during draft only).

Replace pick enforces sport eligibility with the replaced slot treated
as free, updates the pick in-place, and broadcasts a pick-replaced
socket event so all clients update in real time. Rollback deletes all
picks from the selected pick onward, resets the season pick number,
clears all timers, and creates a fresh timer for the team now on the
clock.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-20 22:47:29 -08:00

246 lines
8.7 KiB
TypeScript

import {
ContextMenu,
ContextMenuContent,
ContextMenuItem,
ContextMenuTrigger,
} from "~/components/ui/context-menu";
interface DraftGridSectionProps {
draftSlots: Array<{
id: string;
draftOrder: number;
team: {
id: string;
name: string;
};
}>;
draftGrid: Array<
Array<{
pickNumber: number;
round: number;
pickInRound: number;
teamId: string;
pick?: {
participant: {
name: string;
};
sport: {
name: string;
};
};
}>
>;
currentPick: number;
teamTimers: Record<string, number>;
autodraftStatus: Record<string, boolean>;
connectedTeams: Set<string>;
isCommissioner: boolean;
onForceAutopick: (pickNumber: number, teamId: string) => void;
onForceManualPickOpen: (pickNumber: number, teamId: string) => void;
onReplacePick?: (pickNumber: number, teamId: string) => void;
onRollbackToPick?: (pickNumber: number) => void;
}
export function DraftGridSection({
draftSlots,
draftGrid,
currentPick,
teamTimers,
autodraftStatus,
connectedTeams,
isCommissioner,
onForceAutopick,
onForceManualPickOpen,
onReplacePick,
onRollbackToPick,
}: DraftGridSectionProps) {
const formatTime = (seconds: number | null) => {
if (seconds === null) return "--:--";
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
const secs = seconds % 60;
if (hours > 0) {
return `${hours}:${String(minutes).padStart(2, "0")}:${String(secs).padStart(2, "0")}`;
}
return `${minutes}:${String(secs).padStart(2, "0")}`;
};
const getTimerColor = (seconds: number | null) => {
if (seconds === null) return "text-muted-foreground";
if (seconds > 60) return "text-emerald-400";
if (seconds > 30) return "text-amber-accent";
if (seconds > 10) return "text-coral-accent";
return "text-coral-accent animate-pulse";
};
return (
<div className="h-full overflow-auto p-4">
<h2 className="text-xl font-semibold mb-4">Draft Grid</h2>
<div className="overflow-x-auto">
<div className="w-full">
{/* Team Headers */}
<div className="flex gap-2 mb-2">
{draftSlots.map((slot) => {
const teamTime = teamTimers[slot.team.id];
const isAutodraft = autodraftStatus[slot.team.id] || false;
const isConnected = connectedTeams.has(slot.team.id);
return (
<div key={slot.id} className="flex-1 min-w-32 text-center">
<div
className={`font-semibold text-sm truncate px-2 ${!isConnected ? "italic text-muted-foreground" : ""}`}
>
{slot.team.name}
</div>
<div
className={`text-xs font-mono ${getTimerColor(teamTime)}`}
>
{formatTime(teamTime)}
{isAutodraft && (
<span className="ml-1 text-muted-foreground">
(auto)
</span>
)}
</div>
</div>
);
})}
</div>
{/* Draft Grid Rows */}
<div className="space-y-2">
{draftGrid.map((roundPicks, roundIndex) => {
const round = roundIndex + 1;
const isEvenRound = round % 2 === 0;
const displayPicks = isEvenRound
? [...roundPicks].reverse()
: roundPicks;
return (
<div key={roundIndex} className="flex gap-2">
{displayPicks.map((cell) => {
const isCurrent = cell.pickNumber === currentPick;
const isPicked = !!cell.pick;
const cellClassName = `flex-1 min-w-32 h-20 border-2 rounded-lg p-2 transition-all ${
isCurrent
? "border-electric bg-electric/15 shadow-lg shadow-electric/10"
: isPicked
? "bg-emerald-500/10 border-emerald-500/30"
: "border-border bg-card"
}`;
const cellInner = (
<>
<div className="text-xs font-mono text-muted-foreground mb-1">
{cell.round}.
{String(cell.pickInRound).padStart(2, "0")}
</div>
{isPicked ? (
<div className="text-xs">
<div className="font-semibold truncate">
{cell.pick?.participant.name}
</div>
<div className="text-muted-foreground truncate">
{cell.pick?.sport.name}
</div>
</div>
) : isCurrent ? (
<div className="text-xs font-semibold text-electric">
On Clock
</div>
) : null}
</>
);
// Commissioner context menu on the current unpicked cell
if (isCommissioner && !isPicked && isCurrent) {
return (
<ContextMenu key={cell.pickNumber}>
<ContextMenuTrigger asChild>
<div
className={cellClassName}
title={`Overall Pick #${cell.pickNumber}`}
>
{cellInner}
</div>
</ContextMenuTrigger>
<ContextMenuContent>
<ContextMenuItem
onClick={() =>
onForceAutopick(cell.pickNumber, cell.teamId)
}
>
Force Auto Pick
</ContextMenuItem>
<ContextMenuItem
onClick={() =>
onForceManualPickOpen(
cell.pickNumber,
cell.teamId
)
}
>
Force Manual Pick
</ContextMenuItem>
</ContextMenuContent>
</ContextMenu>
);
}
// Commissioner context menu on already-picked cells
if (isCommissioner && isPicked && (onReplacePick || onRollbackToPick)) {
return (
<ContextMenu key={cell.pickNumber}>
<ContextMenuTrigger asChild>
<div
className={cellClassName}
title={`Overall Pick #${cell.pickNumber}`}
>
{cellInner}
</div>
</ContextMenuTrigger>
<ContextMenuContent>
{onReplacePick && (
<ContextMenuItem
onClick={() =>
onReplacePick(cell.pickNumber, cell.teamId)
}
>
Replace Pick
</ContextMenuItem>
)}
{onRollbackToPick && (
<ContextMenuItem
onClick={() => onRollbackToPick(cell.pickNumber)}
className="text-destructive focus:text-destructive"
>
Roll Back to This Pick
</ContextMenuItem>
)}
</ContextMenuContent>
</ContextMenu>
);
}
return (
<div
key={cell.pickNumber}
className={cellClassName}
title={`Overall Pick #${cell.pickNumber}`}
>
{cellInner}
</div>
);
})}
</div>
);
})}
</div>
</div>
</div>
</div>
);
}