import { memo, useState, useEffect, useRef } from "react"; import { DraftPickCell } from "~/components/draft/DraftPickCell"; import type { SeasonStatus } from "~/models/season"; import { ContextMenu, ContextMenuContent, ContextMenuItem, ContextMenuTrigger, } from "~/components/ui/context-menu"; import { formatClockTime, getTimerColorClass } from "~/lib/draft-timer"; const ROW_GAP = 6; // gap-1.5 const ANIMATION_MS = 300; function getRoundIndices(round: number): [number, number] { if (round <= 2) return [0, 1]; return [round - 2, round - 1]; } export interface MiniDraftGridProps { draftSlots: Array<{ id: string; draftOrder: number; team: { id: string; name: string; logoUrl?: string | null; }; }>; draftGrid: Array< Array<{ pickNumber: number; round: number; pickInRound: number; teamId: string; pick?: { participant: { name: string; }; sport: { name: string; }; }; }> >; currentPick: number; currentRound: number; ownerMap?: Record; teamTimers?: Record; autodraftStatus?: Record; seasonStatus?: SeasonStatus; draftPaused?: boolean; onForceAutopick?: (pickNumber: number, teamId: string) => void; onForceManualPickOpen?: (pickNumber: number, teamId: string) => void; onReplacePick?: (pickNumber: number, teamId: string) => void; onRollbackToPick?: (pickNumber: number) => void; } export const MiniDraftGrid = memo(function MiniDraftGrid({ draftSlots, draftGrid, currentPick, currentRound, ownerMap = {}, teamTimers = {}, autodraftStatus = {}, seasonStatus, draftPaused, onForceAutopick, onForceManualPickOpen, onReplacePick, onRollbackToPick, }: MiniDraftGridProps) { const [displayedIndices, setDisplayedIndices] = useState<[number, number]>(() => getRoundIndices(currentRound)); const [extraIndex, setExtraIndex] = useState(null); const [sliding, setSliding] = useState(false); const animGenRef = useRef(0); const prevRoundRef = useRef(currentRound); const firstRowRef = useRef(null); const [rowHeight, setRowHeight] = useState(0); const scrollContainerRef = useRef(null); const currentCellRef = useRef(null); useEffect(() => { if (firstRowRef.current) { setRowHeight(firstRowRef.current.getBoundingClientRect().height); } }, [draftGrid]); useEffect(() => { const container = scrollContainerRef.current; const cell = currentCellRef.current; if (!container || !cell) return; const containerRect = container.getBoundingClientRect(); const cellRect = cell.getBoundingClientRect(); const scrollLeft = container.scrollLeft + cellRect.left - containerRect.left - (containerRect.width - cellRect.width) / 2; container.scrollTo({ left: Math.max(0, scrollLeft), behavior: "smooth" }); }, [currentPick]); useEffect(() => { const prev = prevRoundRef.current; prevRoundRef.current = currentRound; const newIndices = getRoundIndices(currentRound); const prevIndices = getRoundIndices(prev); if (newIndices[0] === prevIndices[0]) return; // Non-sequential jump or animation already running: snap and invalidate any in-flight animation if (newIndices[0] !== prevIndices[0] + 1 || animGenRef.current > 0) { animGenRef.current++; // invalidate any in-flight animation closure setDisplayedIndices(newIndices); setExtraIndex(null); setSliding(false); return; } const gen = ++animGenRef.current; setExtraIndex(newIndices[1]); // Two rAFs: first lets React flush the extra row to DOM, second starts the CSS transition requestAnimationFrame(() => { requestAnimationFrame(() => { setSliding(true); setTimeout(() => { if (animGenRef.current !== gen) return; // interrupted by snap setDisplayedIndices(newIndices); setExtraIndex(null); setSliding(false); animGenRef.current = 0; }, ANIMATION_MS + 20); }); }); }, [currentRound]); if (draftGrid.length === 0) return null; // h-14 (56px) + gap-1.5 (6px) fallback until first measurement const effectiveRowHeight = rowHeight > 0 ? rowHeight : 56; const roundIndicesToRender: number[] = extraIndex !== null ? [displayedIndices[0], displayedIndices[1], extraIndex] : [displayedIndices[0], displayedIndices[1]]; return (
{/* Team header */}
{draftSlots.map((slot) => { const teamTime = teamTimers[slot.team.id]; const isAutodraft = autodraftStatus[slot.team.id]?.isEnabled ?? false; return (
{isAutodraft && ( A )} {ownerMap[slot.team.id] || slot.team.name}
{formatClockTime(teamTime)}
); })}
{/* Rows — clipped to 2-row height, slides to reveal new round */}
{roundIndicesToRender.map((roundIndex) => { if (roundIndex >= draftGrid.length) return null; const roundPicks = draftGrid[roundIndex]; const round = roundIndex + 1; const isEvenRound = round % 2 === 0; const displayPicks = isEvenRound ? [...roundPicks].toReversed() : roundPicks; return (
{displayPicks.map((cell) => { const isCurrent = cell.pickNumber === currentPick; const isPicked = !!cell.pick; const cellState = isPicked ? "picked" : isCurrent ? "current" : "upcoming"; const pickData = cell.pick ? { participant: { name: cell.pick.participant.name }, sport: { name: cell.pick.sport.name }, } : undefined; if (!isPicked && isCurrent && (onForceAutopick || onForceManualPickOpen)) { return ( {onForceAutopick && ( onForceAutopick(cell.pickNumber, cell.teamId)}> Force Auto Pick )} {onForceManualPickOpen && ( onForceManualPickOpen(cell.pickNumber, cell.teamId)}> Force Manual Pick )} ); } if (isPicked && (onReplacePick || onRollbackToPick)) { return ( {onReplacePick && ( onReplacePick(cell.pickNumber, cell.teamId)}> Replace Pick )} {onRollbackToPick && ( onRollbackToPick(cell.pickNumber)} className="text-destructive focus:text-destructive" > Roll Back to This Pick )} ); } return ( ); })}
); })}
); });