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>
246 lines
8.7 KiB
TypeScript
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>
|
|
);
|
|
}
|