* Show manager username instead of team name on draft board Both the read-only draft board and the active draft room now display the manager's username (falling back to displayName if no username is set, and to team name if the team is unassigned) in the column headers of the draft grid. https://claude.ai/code/session_01C97GauJaAB83NVWxdpNVh1 * Address code review: extract ownerMap helper, fix TeamsDraftedGrid - Extract ownerMap building into app/lib/owner-map.ts to eliminate duplication across the two loaders - Guard against null username AND displayName so the map only contains real string values - Apply username display to TeamsDraftedGrid (the "Teams" tab in the active draft room), which was missed in the initial change https://claude.ai/code/session_01C97GauJaAB83NVWxdpNVh1 --------- Co-authored-by: Claude <noreply@anthropic.com>
267 lines
9.6 KiB
TypeScript
267 lines
9.6 KiB
TypeScript
import { useMemo } from "react";
|
|
import {
|
|
ContextMenu,
|
|
ContextMenuContent,
|
|
ContextMenuItem,
|
|
ContextMenuTrigger,
|
|
} from "~/components/ui/context-menu";
|
|
import { formatClockTime, getTimerColorClass } from "~/lib/draft-timer";
|
|
|
|
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 | undefined>;
|
|
autodraftStatus: Record<string, boolean>;
|
|
connectedTeams: Set<string>;
|
|
isCommissioner: boolean;
|
|
onAdjustTimeBankOpen?: (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 function DraftGridSection({
|
|
draftSlots,
|
|
draftGrid,
|
|
currentPick,
|
|
teamTimers,
|
|
autodraftStatus,
|
|
connectedTeams,
|
|
isCommissioner,
|
|
onAdjustTimeBankOpen,
|
|
onForceAutopick,
|
|
onForceManualPickOpen,
|
|
onReplacePick,
|
|
onRollbackToPick,
|
|
ownerMap = {},
|
|
}: DraftGridSectionProps) {
|
|
const currentTeamId = useMemo(
|
|
() => draftGrid.flat().find((c) => c.pickNumber === currentPick)?.teamId ?? null,
|
|
[draftGrid, currentPick]
|
|
);
|
|
|
|
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);
|
|
|
|
const headerInner = (
|
|
<>
|
|
<div
|
|
className={`font-semibold text-sm truncate px-2 ${
|
|
slot.team.id === currentTeamId
|
|
? "text-electric font-bold"
|
|
: !isConnected
|
|
? "italic text-muted-foreground"
|
|
: ""
|
|
}`}
|
|
>
|
|
{ownerMap[slot.team.id] || slot.team.name}
|
|
</div>
|
|
<div
|
|
className={`text-xs font-mono ${getTimerColorClass(teamTime)}`}
|
|
>
|
|
{formatClockTime(teamTime)}
|
|
{isAutodraft && (
|
|
<span className="ml-1 text-muted-foreground">
|
|
(auto)
|
|
</span>
|
|
)}
|
|
</div>
|
|
</>
|
|
);
|
|
|
|
if (isCommissioner && onAdjustTimeBankOpen) {
|
|
return (
|
|
<ContextMenu key={slot.id}>
|
|
<ContextMenuTrigger asChild>
|
|
<div className="flex-1 min-w-32 text-center cursor-context-menu">
|
|
{headerInner}
|
|
</div>
|
|
</ContextMenuTrigger>
|
|
<ContextMenuContent>
|
|
<ContextMenuItem
|
|
onClick={() => onAdjustTimeBankOpen(slot.team.id)}
|
|
>
|
|
Adjust Time Bank...
|
|
</ContextMenuItem>
|
|
</ContextMenuContent>
|
|
</ContextMenu>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div key={slot.id} className="flex-1 min-w-32 text-center">
|
|
{headerInner}
|
|
</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/20 shadow-lg shadow-electric/25 ring-1 ring-electric/40"
|
|
: 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-sm font-bold text-electric animate-pulse">
|
|
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>
|
|
);
|
|
}
|