brackt/app/components/draft/DraftGridSection.tsx
Chris Parsons ea125e0d5e
Fix draft room Summary tab layout and board scroll clipping (#452)
- Add display:contents to rowgroup divs in DraftSummaryView so cells
  flow correctly into the CSS grid (sports as rows, teams as columns)
- Use opaque bg-muted on team column headers to prevent content
  bleeding through during sticky scroll
- Remove whitespace-nowrap and add break-words on sport row headers
  so long names like "FIFA Men's World Cup" wrap within the column
- Add pb-4 to DraftGridSection scroll wrapper so the last pick row
  isn't flush against the scroll boundary

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 23:31:40 -07:00

420 lines
17 KiB
TypeScript

import { memo, useMemo, useState } from "react";
import { TeamAvatar } from "~/components/TeamAvatar";
import type { AvatarData, RawFlagConfig } from "~/lib/flag-types";
import {
ContextMenu,
ContextMenuContent,
ContextMenuItem,
ContextMenuTrigger,
} from "~/components/ui/context-menu";
import {
Sheet,
SheetContent,
SheetHeader,
SheetTitle,
} from "~/components/ui/sheet";
import { Button } from "~/components/ui/button";
import { MoreVertical } from "lucide-react";
import { formatClockTime, getTimerColorClass } from "~/lib/draft-timer";
import { DraftPickCell } from "~/components/draft/DraftPickCell";
import type { SeasonStatus } from "~/models/season";
type MobileSheetData =
| { type: "team"; teamId: string }
| { type: "current-cell"; pickNumber: number; teamId: string }
| { type: "picked-cell"; pickNumber: number; teamId: string };
interface DraftGridSectionProps {
draftSlots: Array<{
id: string;
draftOrder: number;
team: {
id: string;
name: string;
logoUrl?: string | null;
flagConfig?: RawFlagConfig | null;
avatarType?: string | null;
ownerAvatarData?: AvatarData | null;
};
}>;
draftGrid: Array<
Array<{
pickNumber: number;
round: number;
pickInRound: number;
teamId: string;
pick?: {
participant: {
name: string;
};
sport: {
name: string;
};
};
}>
>;
currentPick: number;
teamTimers: Record<string, number | undefined>;
autodraftStatus: Record<string, { isEnabled: boolean; mode: "next_pick" | "while_on"; queueOnly: boolean }>;
connectedTeams: Set<string>;
isCommissioner: boolean;
seasonStatus?: SeasonStatus;
draftPaused?: boolean;
overnightPauseInfo?: { isActive: boolean; resumesAt: Date | null; pausedTeamIds?: Set<string> };
onAdjustTimeBankOpen?: (teamId: string) => void;
onSetAutodraftOpen?: (teamId: string) => void;
onForceAutopick?: (pickNumber: number, teamId: string) => void;
onForceManualPickOpen?: (pickNumber: number, teamId: string) => void;
onReplacePick?: (pickNumber: number, teamId: string) => void;
onRollbackToPick?: (pickNumber: number) => void;
ownerMap?: Record<string, string>;
}
export const DraftGridSection = memo(function DraftGridSection({
draftSlots,
draftGrid,
currentPick,
teamTimers,
autodraftStatus,
connectedTeams,
isCommissioner,
seasonStatus,
draftPaused,
overnightPauseInfo,
onAdjustTimeBankOpen,
onSetAutodraftOpen,
onForceAutopick,
onForceManualPickOpen,
onReplacePick,
onRollbackToPick,
ownerMap = {},
}: DraftGridSectionProps) {
const [mobileSheet, setMobileSheet] = useState<MobileSheetData | null>(null);
const currentTeamId = useMemo(
() => draftGrid.flat().find((c) => c.pickNumber === currentPick)?.teamId ?? null,
[draftGrid, currentPick]
);
const isOvernightActive = overnightPauseInfo?.isActive ?? false;
return (
<div className="h-full flex flex-col p-4">
<div className="flex-1 overflow-auto">
<div className="inline-block min-w-full min-h-full pb-4">
{/* Team Headers */}
<div className="flex gap-1.5 mb-1.5 sticky top-0 z-10 bg-background/95 backdrop-blur-sm py-1">
{draftSlots.map((slot) => {
const teamTime = teamTimers[slot.team.id];
const isAutodraft = autodraftStatus[slot.team.id]?.isEnabled || false;
const isConnected = connectedTeams.has(slot.team.id);
const isCurrentTeamOvernight = slot.team.id === currentTeamId
? isOvernightActive
: (overnightPauseInfo?.pausedTeamIds?.has(slot.team.id) ?? false);
const headerInner = (
<div className="relative">
<div className="flex justify-center mb-1">
<TeamAvatar
teamId={slot.team.id}
teamName={ownerMap[slot.team.id] || slot.team.name}
logoUrl={slot.team.logoUrl}
flagConfig={slot.team.flagConfig}
avatarType={slot.team.avatarType}
ownerAvatarData={slot.team.ownerAvatarData}
size="md"
/>
</div>
<div
className={`font-semibold text-xs px-1 flex items-center justify-center ${
slot.team.id === currentTeamId
? "text-electric font-bold"
: !isConnected
? "italic text-muted-foreground"
: ""
}`}
>
{isAutodraft && (
<span className="mr-1 inline-flex items-center justify-center h-4 w-4 text-[10px] font-bold text-white bg-black shrink-0">A</span>
)}
<span className="truncate">{ownerMap[slot.team.id] || slot.team.name}</span>
</div>
<div
className={`text-xs font-mono px-1 ${isCurrentTeamOvernight ? "text-muted-foreground" : getTimerColorClass(teamTime)}`}
>
{isCurrentTeamOvernight ? `🌙 ${formatClockTime(teamTime)}` : formatClockTime(teamTime)}
</div>
{isCommissioner && (onAdjustTimeBankOpen || onSetAutodraftOpen) && (
<button
className="absolute top-0 right-0 md:hidden p-1 min-w-[32px] min-h-[32px] flex items-center justify-center rounded hover:bg-muted"
onClick={() => setMobileSheet({ type: "team", teamId: slot.team.id })}
>
<MoreVertical className="h-3.5 w-3.5" />
</button>
)}
</div>
);
if (isCommissioner && (onAdjustTimeBankOpen || onSetAutodraftOpen)) {
return (
<ContextMenu key={slot.id}>
<ContextMenuTrigger asChild>
<div className="flex-1 min-w-20 text-center cursor-context-menu">
{headerInner}
</div>
</ContextMenuTrigger>
<ContextMenuContent>
{onAdjustTimeBankOpen && (
<ContextMenuItem onClick={() => onAdjustTimeBankOpen(slot.team.id)}>
Adjust Time Bank...
</ContextMenuItem>
)}
{onSetAutodraftOpen && (
<ContextMenuItem onClick={() => onSetAutodraftOpen(slot.team.id)}>
Set Autodraft...
</ContextMenuItem>
)}
</ContextMenuContent>
</ContextMenu>
);
}
return (
<div key={slot.id} className="flex-1 min-w-20 text-center">
{headerInner}
</div>
);
})}
</div>
{/* Draft Grid Rows */}
<div className="space-y-1.5">
{draftGrid.map((roundPicks, roundIndex) => {
const round = roundIndex + 1;
const isEvenRound = round % 2 === 0;
const displayPicks = isEvenRound
? [...roundPicks].toReversed()
: roundPicks;
return (
<div key={round} className="flex gap-1.5 items-stretch">
{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;
const pendingCorona = cellState === "upcoming" ? { type: "pending" as const } : undefined;
const mobileButtons = isCommissioner && (
<>
{!isPicked && isCurrent && (onForceAutopick || onForceManualPickOpen) && (
<button
className="absolute top-1 right-1 md:hidden p-1 min-w-[32px] min-h-[32px] flex items-center justify-center rounded hover:bg-muted"
onClick={() => setMobileSheet({ type: "current-cell", pickNumber: cell.pickNumber, teamId: cell.teamId })}
>
<MoreVertical className="h-3.5 w-3.5" />
</button>
)}
{isPicked && (onReplacePick || onRollbackToPick) && (
<button
className="absolute top-1 right-1 md:hidden p-1 min-w-[32px] min-h-[32px] flex items-center justify-center rounded hover:bg-muted"
onClick={() => setMobileSheet({ type: "picked-cell", pickNumber: cell.pickNumber, teamId: cell.teamId })}
>
<MoreVertical className="h-3.5 w-3.5" />
</button>
)}
</>
);
if (isCommissioner && !isPicked && isCurrent && (onForceAutopick || onForceManualPickOpen)) {
return (
<ContextMenu key={cell.pickNumber}>
<ContextMenuTrigger asChild>
<DraftPickCell
pickNumber={cell.pickNumber}
round={cell.round}
pickInRound={cell.pickInRound}
state={cellState}
pick={pickData}
coronaState={pendingCorona}
seasonStatus={seasonStatus}
draftPaused={draftPaused}
isOvernightPause={isCurrent && isOvernightActive}
resumesAt={isCurrent && isOvernightActive && overnightPauseInfo?.resumesAt ? overnightPauseInfo.resumesAt : undefined}
className="cursor-context-menu"
>
{mobileButtons}
</DraftPickCell>
</ContextMenuTrigger>
<ContextMenuContent>
{onForceAutopick && (
<ContextMenuItem onClick={() => onForceAutopick(cell.pickNumber, cell.teamId)}>
Force Auto Pick
</ContextMenuItem>
)}
{onForceManualPickOpen && (
<ContextMenuItem onClick={() => onForceManualPickOpen(cell.pickNumber, cell.teamId)}>
Force Manual Pick
</ContextMenuItem>
)}
</ContextMenuContent>
</ContextMenu>
);
}
if (isCommissioner && isPicked && (onReplacePick || onRollbackToPick)) {
return (
<ContextMenu key={cell.pickNumber}>
<ContextMenuTrigger asChild>
<DraftPickCell
pickNumber={cell.pickNumber}
round={cell.round}
pickInRound={cell.pickInRound}
state={cellState}
pick={pickData}
coronaState={pendingCorona}
seasonStatus={seasonStatus}
draftPaused={draftPaused}
className="cursor-context-menu"
>
{mobileButtons}
</DraftPickCell>
</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 (
<DraftPickCell
key={cell.pickNumber}
pickNumber={cell.pickNumber}
round={cell.round}
pickInRound={cell.pickInRound}
state={cellState}
pick={pickData}
coronaState={pendingCorona}
seasonStatus={seasonStatus}
draftPaused={draftPaused}
isOvernightPause={isCurrent && isOvernightActive}
resumesAt={isCurrent && isOvernightActive && overnightPauseInfo?.resumesAt ? overnightPauseInfo.resumesAt : undefined}
>
{mobileButtons}
</DraftPickCell>
);
})}
</div>
);
})}
</div>
</div>
</div>
{/* Mobile Commissioner Sheet */}
<Sheet open={!!mobileSheet} onOpenChange={(open) => !open && setMobileSheet(null)}>
<SheetContent side="bottom" className="pb-8">
<SheetHeader>
<SheetTitle>Commissioner Actions</SheetTitle>
</SheetHeader>
<div className="flex flex-col gap-3 p-4">
{mobileSheet?.type === "team" && (
<>
{onAdjustTimeBankOpen && (
<Button
onClick={() => {
onAdjustTimeBankOpen(mobileSheet.teamId);
setMobileSheet(null);
}}
>
Adjust Time Bank...
</Button>
)}
{onSetAutodraftOpen && (
<Button
variant="outline"
onClick={() => {
onSetAutodraftOpen(mobileSheet.teamId);
setMobileSheet(null);
}}
>
Set Autodraft...
</Button>
)}
</>
)}
{mobileSheet?.type === "current-cell" && (
<>
{onForceAutopick && (
<Button
onClick={() => {
onForceAutopick(mobileSheet.pickNumber, mobileSheet.teamId);
setMobileSheet(null);
}}
>
Force Auto Pick
</Button>
)}
{onForceManualPickOpen && (
<Button
variant="outline"
onClick={() => {
onForceManualPickOpen(mobileSheet.pickNumber, mobileSheet.teamId);
setMobileSheet(null);
}}
>
Force Manual Pick
</Button>
)}
</>
)}
{mobileSheet?.type === "picked-cell" && (
<>
{onReplacePick && (
<Button
onClick={() => {
onReplacePick(mobileSheet.pickNumber, mobileSheet.teamId);
setMobileSheet(null);
}}
>
Replace Pick
</Button>
)}
{onRollbackToPick && (
<Button
variant="destructive"
onClick={() => {
onRollbackToPick(mobileSheet.pickNumber);
setMobileSheet(null);
}}
>
Roll Back to This Pick
</Button>
)}
</>
)}
</div>
</SheetContent>
</Sheet>
</div>
);
});