Address all code review issues for draft room context menus

Layout bug: flex-1 min-w-20 was nested inside headerInner instead of the
direct flex-child wrapper in MiniDraftGrid, breaking equal column sizing.
Fixed by moving the classes to the outermost element in both the menu and
non-menu paths (matching DraftGridSection's pattern).

MiniDraftGrid improvements:
- Hoist hasHeaderMenu constant above the draftSlots.map() loop
- Add optional connectedTeams prop; disconnected teams render italic +
  muted-foreground, consistent with DraftGridSection
- connectedTeams now wired through the route's miniDraftGrid useMemo

DraftGridSection interface:
- Make onForceAutopick and onForceManualPickOpen optional (?) to match
  every other commissioner callback; add null checks at all three call
  sites (context menu, mobile MoreVertical button, mobile Sheet)
- Gate those two callbacks behind isCommissioner at the route level

Tests (new files):
- app/components/__tests__/MiniDraftGrid.test.tsx — covers layout classes,
  connected/disconnected styling, and all four context menu interactions
- app/components/__tests__/DraftGridSection.test.tsx — covers all three
  context menu surfaces for both commissioner and non-commissioner roles

Storybook: add CommissionerView and DisconnectedTeams stories to
MiniDraftGrid.stories.tsx

https://claude.ai/code/session_017JCShLVs9xZ6FZmyrUFaE1
This commit is contained in:
Claude 2026-04-25 07:11:13 +00:00
parent 3cfd6ca654
commit a2cbedbb82
No known key found for this signature in database
6 changed files with 555 additions and 81 deletions

View file

@ -0,0 +1,198 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { render, screen, fireEvent } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { DraftGridSection } from "~/components/draft/DraftGridSection";
const draftSlots = [
{ id: "slot-1", draftOrder: 1, team: { id: "team-1", name: "Alpha", logoUrl: null } },
{ id: "slot-2", draftOrder: 2, team: { id: "team-2", name: "Bravo", logoUrl: null } },
];
const ownerMap = { "team-1": "Alpha", "team-2": "Bravo" };
const draftGrid = [
[
{ pickNumber: 1, round: 1, pickInRound: 1, teamId: "team-1", pick: { participant: { name: "Player A" }, sport: { name: "NFL" } } },
{ pickNumber: 2, round: 1, pickInRound: 2, teamId: "team-2" },
],
];
const baseProps = {
draftSlots,
draftGrid,
currentPick: 2,
teamTimers: {} as Record<string, number | undefined>,
autodraftStatus: {} as Record<string, { isEnabled: boolean; mode: "next_pick" | "while_on"; queueOnly: boolean }>,
connectedTeams: new Set(["team-1", "team-2"]),
isCommissioner: false,
ownerMap,
seasonStatus: "draft" as const,
draftPaused: false,
};
describe("DraftGridSection", () => {
beforeEach(() => vi.clearAllMocks());
describe("Team header context menus", () => {
it("shows no header context menu for non-commissioner", () => {
render(<DraftGridSection {...baseProps} />);
fireEvent.contextMenu(screen.getAllByText("Alpha")[0]);
expect(screen.queryByText("Adjust Time Bank...")).not.toBeInTheDocument();
expect(screen.queryByText("Set Autodraft...")).not.toBeInTheDocument();
});
it("shows Adjust Time Bank and Set Autodraft for commissioner on right-click", () => {
render(
<DraftGridSection
{...baseProps}
isCommissioner
onAdjustTimeBankOpen={vi.fn()}
onSetAutodraftOpen={vi.fn()}
/>
);
fireEvent.contextMenu(screen.getAllByText("Alpha")[0]);
expect(screen.getByText("Adjust Time Bank...")).toBeInTheDocument();
expect(screen.getByText("Set Autodraft...")).toBeInTheDocument();
});
it("calls onAdjustTimeBankOpen with correct teamId", async () => {
const user = userEvent.setup();
const onAdjustTimeBankOpen = vi.fn();
render(
<DraftGridSection
{...baseProps}
isCommissioner
onAdjustTimeBankOpen={onAdjustTimeBankOpen}
/>
);
fireEvent.contextMenu(screen.getAllByText("Bravo")[0]);
await user.click(screen.getByText("Adjust Time Bank..."));
expect(onAdjustTimeBankOpen).toHaveBeenCalledWith("team-2");
});
it("calls onSetAutodraftOpen with correct teamId", async () => {
const user = userEvent.setup();
const onSetAutodraftOpen = vi.fn();
render(
<DraftGridSection
{...baseProps}
isCommissioner
onSetAutodraftOpen={onSetAutodraftOpen}
/>
);
fireEvent.contextMenu(screen.getAllByText("Alpha")[0]);
await user.click(screen.getByText("Set Autodraft..."));
expect(onSetAutodraftOpen).toHaveBeenCalledWith("team-1");
});
});
describe("Current cell context menu", () => {
it("shows no context menu for non-commissioner on current cell", () => {
render(
<DraftGridSection
{...baseProps}
onForceAutopick={vi.fn()}
onForceManualPickOpen={vi.fn()}
/>
);
fireEvent.contextMenu(screen.getByTitle("Overall Pick #2"));
expect(screen.queryByText("Force Auto Pick")).not.toBeInTheDocument();
});
it("shows Force Auto Pick and Force Manual Pick for commissioner", () => {
render(
<DraftGridSection
{...baseProps}
isCommissioner
onForceAutopick={vi.fn()}
onForceManualPickOpen={vi.fn()}
/>
);
fireEvent.contextMenu(screen.getByTitle("Overall Pick #2"));
expect(screen.getByText("Force Auto Pick")).toBeInTheDocument();
expect(screen.getByText("Force Manual Pick")).toBeInTheDocument();
});
it("shows no force-pick menu when callbacks absent even for commissioner", () => {
render(<DraftGridSection {...baseProps} isCommissioner />);
fireEvent.contextMenu(screen.getByTitle("Overall Pick #2"));
expect(screen.queryByText("Force Auto Pick")).not.toBeInTheDocument();
});
it("calls onForceAutopick with correct args", async () => {
const user = userEvent.setup();
const onForceAutopick = vi.fn();
render(
<DraftGridSection {...baseProps} isCommissioner onForceAutopick={onForceAutopick} />
);
fireEvent.contextMenu(screen.getByTitle("Overall Pick #2"));
await user.click(screen.getByText("Force Auto Pick"));
expect(onForceAutopick).toHaveBeenCalledWith(2, "team-2");
});
it("calls onForceManualPickOpen with correct args", async () => {
const user = userEvent.setup();
const onForceManualPickOpen = vi.fn();
render(
<DraftGridSection
{...baseProps}
isCommissioner
onForceManualPickOpen={onForceManualPickOpen}
/>
);
fireEvent.contextMenu(screen.getByTitle("Overall Pick #2"));
await user.click(screen.getByText("Force Manual Pick"));
expect(onForceManualPickOpen).toHaveBeenCalledWith(2, "team-2");
});
});
describe("Picked cell context menu", () => {
it("shows Replace Pick and Roll Back for commissioner on a picked cell", () => {
render(
<DraftGridSection
{...baseProps}
isCommissioner
onReplacePick={vi.fn()}
onRollbackToPick={vi.fn()}
/>
);
fireEvent.contextMenu(screen.getByTitle("Overall Pick #1"));
expect(screen.getByText("Replace Pick")).toBeInTheDocument();
expect(screen.getByText("Roll Back to This Pick")).toBeInTheDocument();
});
it("shows no context menu on picked cell for non-commissioner", () => {
render(
<DraftGridSection
{...baseProps}
onReplacePick={vi.fn()}
onRollbackToPick={vi.fn()}
/>
);
fireEvent.contextMenu(screen.getByTitle("Overall Pick #1"));
expect(screen.queryByText("Replace Pick")).not.toBeInTheDocument();
});
it("calls onReplacePick with correct args", async () => {
const user = userEvent.setup();
const onReplacePick = vi.fn();
render(
<DraftGridSection {...baseProps} isCommissioner onReplacePick={onReplacePick} />
);
fireEvent.contextMenu(screen.getByTitle("Overall Pick #1"));
await user.click(screen.getByText("Replace Pick"));
expect(onReplacePick).toHaveBeenCalledWith(1, "team-1");
});
it("calls onRollbackToPick with correct pickNumber", async () => {
const user = userEvent.setup();
const onRollbackToPick = vi.fn();
render(
<DraftGridSection {...baseProps} isCommissioner onRollbackToPick={onRollbackToPick} />
);
fireEvent.contextMenu(screen.getByTitle("Overall Pick #1"));
await user.click(screen.getByText("Roll Back to This Pick"));
expect(onRollbackToPick).toHaveBeenCalledWith(1);
});
});
});

View file

@ -0,0 +1,237 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { render, screen, fireEvent } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { MiniDraftGrid } from "~/components/draft/MiniDraftGrid";
const draftSlots = [
{ id: "slot-1", draftOrder: 1, team: { id: "team-1", name: "Alpha", logoUrl: null } },
{ id: "slot-2", draftOrder: 2, team: { id: "team-2", name: "Bravo", logoUrl: null } },
];
const ownerMap = { "team-1": "Alpha", "team-2": "Bravo" };
// Round 1: team-1 pick 1 (filled), team-2 pick 2 (current/empty)
// Round 2 (snake): team-2 pick 3, team-1 pick 4 — both empty
const draftGrid = [
[
{ pickNumber: 1, round: 1, pickInRound: 1, teamId: "team-1", pick: { participant: { name: "Player A" }, sport: { name: "NFL" } } },
{ pickNumber: 2, round: 1, pickInRound: 2, teamId: "team-2" },
],
[
{ pickNumber: 3, round: 2, pickInRound: 1, teamId: "team-2" },
{ pickNumber: 4, round: 2, pickInRound: 2, teamId: "team-1" },
],
];
const baseProps = {
draftSlots,
draftGrid,
currentPick: 2,
currentRound: 1,
ownerMap,
};
describe("MiniDraftGrid", () => {
beforeEach(() => vi.clearAllMocks());
describe("Team header layout", () => {
it("renders all team names", () => {
render(<MiniDraftGrid {...baseProps} />);
expect(screen.getAllByText("Alpha").length).toBeGreaterThan(0);
expect(screen.getAllByText("Bravo").length).toBeGreaterThan(0);
});
it("non-commissioner headers carry flex sizing classes", () => {
render(<MiniDraftGrid {...baseProps} />);
const alphaHeader = screen.getAllByText("Alpha")[0].closest(".flex-1");
expect(alphaHeader).toHaveClass("flex-1", "min-w-20");
});
it("commissioner headers carry flex sizing classes", () => {
render(
<MiniDraftGrid
{...baseProps}
onAdjustTimeBankOpen={vi.fn()}
onSetAutodraftOpen={vi.fn()}
/>
);
const alphaHeader = screen.getAllByText("Alpha")[0].closest(".flex-1");
expect(alphaHeader).toHaveClass("flex-1", "min-w-20");
});
});
describe("Disconnected team styling", () => {
it("applies italic + muted style to disconnected teams when connectedTeams provided", () => {
render(
<MiniDraftGrid
{...baseProps}
connectedTeams={new Set(["team-1"])}
/>
);
const bravoSpan = screen.getAllByText("Bravo")[0];
expect(bravoSpan).toHaveClass("italic", "text-muted-foreground");
const alphaSpan = screen.getAllByText("Alpha")[0];
expect(alphaSpan).not.toHaveClass("italic");
});
it("treats all teams as connected when connectedTeams is not provided", () => {
render(<MiniDraftGrid {...baseProps} />);
expect(screen.getAllByText("Alpha")[0]).not.toHaveClass("italic");
expect(screen.getAllByText("Bravo")[0]).not.toHaveClass("italic");
});
});
describe("Team header context menus (commissioner)", () => {
it("shows no context menu when callbacks are absent", () => {
render(<MiniDraftGrid {...baseProps} />);
const alphaHeader = screen.getAllByText("Alpha")[0];
fireEvent.contextMenu(alphaHeader);
expect(screen.queryByText("Adjust Time Bank...")).not.toBeInTheDocument();
expect(screen.queryByText("Set Autodraft...")).not.toBeInTheDocument();
});
it("shows Adjust Time Bank and Set Autodraft on right-click when both callbacks provided", () => {
render(
<MiniDraftGrid
{...baseProps}
onAdjustTimeBankOpen={vi.fn()}
onSetAutodraftOpen={vi.fn()}
/>
);
fireEvent.contextMenu(screen.getAllByText("Alpha")[0]);
expect(screen.getByText("Adjust Time Bank...")).toBeInTheDocument();
expect(screen.getByText("Set Autodraft...")).toBeInTheDocument();
});
it("shows only Adjust Time Bank when only that callback is provided", () => {
render(
<MiniDraftGrid {...baseProps} onAdjustTimeBankOpen={vi.fn()} />
);
fireEvent.contextMenu(screen.getAllByText("Alpha")[0]);
expect(screen.getByText("Adjust Time Bank...")).toBeInTheDocument();
expect(screen.queryByText("Set Autodraft...")).not.toBeInTheDocument();
});
it("calls onAdjustTimeBankOpen with the correct teamId", async () => {
const user = userEvent.setup();
const onAdjustTimeBankOpen = vi.fn();
render(
<MiniDraftGrid {...baseProps} onAdjustTimeBankOpen={onAdjustTimeBankOpen} />
);
fireEvent.contextMenu(screen.getAllByText("Bravo")[0]);
await user.click(screen.getByText("Adjust Time Bank..."));
expect(onAdjustTimeBankOpen).toHaveBeenCalledWith("team-2");
});
it("calls onSetAutodraftOpen with the correct teamId", async () => {
const user = userEvent.setup();
const onSetAutodraftOpen = vi.fn();
render(
<MiniDraftGrid {...baseProps} onSetAutodraftOpen={onSetAutodraftOpen} />
);
fireEvent.contextMenu(screen.getAllByText("Alpha")[0]);
await user.click(screen.getByText("Set Autodraft..."));
expect(onSetAutodraftOpen).toHaveBeenCalledWith("team-1");
});
});
describe("Current cell context menu (commissioner)", () => {
it("shows Force Auto Pick and Force Manual Pick on right-click of current cell", () => {
render(
<MiniDraftGrid
{...baseProps}
onForceAutopick={vi.fn()}
onForceManualPickOpen={vi.fn()}
/>
);
// Pick 2 is the current cell (team-2, round 1)
const currentCell = screen.getByTitle("Overall Pick #2");
fireEvent.contextMenu(currentCell);
expect(screen.getByText("Force Auto Pick")).toBeInTheDocument();
expect(screen.getByText("Force Manual Pick")).toBeInTheDocument();
});
it("calls onForceAutopick with correct args", async () => {
const user = userEvent.setup();
const onForceAutopick = vi.fn();
render(
<MiniDraftGrid {...baseProps} onForceAutopick={onForceAutopick} />
);
fireEvent.contextMenu(screen.getByTitle("Overall Pick #2"));
await user.click(screen.getByText("Force Auto Pick"));
expect(onForceAutopick).toHaveBeenCalledWith(2, "team-2");
});
it("calls onForceManualPickOpen with correct args", async () => {
const user = userEvent.setup();
const onForceManualPickOpen = vi.fn();
render(
<MiniDraftGrid {...baseProps} onForceManualPickOpen={onForceManualPickOpen} />
);
fireEvent.contextMenu(screen.getByTitle("Overall Pick #2"));
await user.click(screen.getByText("Force Manual Pick"));
expect(onForceManualPickOpen).toHaveBeenCalledWith(2, "team-2");
});
it("shows no force-pick menu when no callbacks provided", () => {
render(<MiniDraftGrid {...baseProps} />);
fireEvent.contextMenu(screen.getByTitle("Overall Pick #2"));
expect(screen.queryByText("Force Auto Pick")).not.toBeInTheDocument();
expect(screen.queryByText("Force Manual Pick")).not.toBeInTheDocument();
});
});
describe("Picked cell context menu (commissioner)", () => {
it("shows Replace Pick and Roll Back on right-click of a picked cell", () => {
render(
<MiniDraftGrid
{...baseProps}
onReplacePick={vi.fn()}
onRollbackToPick={vi.fn()}
/>
);
// Pick 1 is already picked (team-1)
const pickedCell = screen.getByTitle("Overall Pick #1");
fireEvent.contextMenu(pickedCell);
expect(screen.getByText("Replace Pick")).toBeInTheDocument();
expect(screen.getByText("Roll Back to This Pick")).toBeInTheDocument();
});
it("calls onReplacePick with correct args", async () => {
const user = userEvent.setup();
const onReplacePick = vi.fn();
render(
<MiniDraftGrid {...baseProps} onReplacePick={onReplacePick} />
);
fireEvent.contextMenu(screen.getByTitle("Overall Pick #1"));
await user.click(screen.getByText("Replace Pick"));
expect(onReplacePick).toHaveBeenCalledWith(1, "team-1");
});
it("calls onRollbackToPick with correct pickNumber", async () => {
const user = userEvent.setup();
const onRollbackToPick = vi.fn();
render(
<MiniDraftGrid {...baseProps} onRollbackToPick={onRollbackToPick} />
);
fireEvent.contextMenu(screen.getByTitle("Overall Pick #1"));
await user.click(screen.getByText("Roll Back to This Pick"));
expect(onRollbackToPick).toHaveBeenCalledWith(1);
});
it("shows no menu on picked cell when no callbacks provided", () => {
render(<MiniDraftGrid {...baseProps} />);
fireEvent.contextMenu(screen.getByTitle("Overall Pick #1"));
expect(screen.queryByText("Replace Pick")).not.toBeInTheDocument();
});
});
describe("Empty grid", () => {
it("renders nothing when draftGrid is empty", () => {
const { container } = render(
<MiniDraftGrid {...baseProps} draftGrid={[]} />
);
expect(container.firstChild).toBeNull();
});
});
});

View file

@ -58,8 +58,8 @@ interface DraftGridSectionProps {
draftPaused?: boolean; draftPaused?: boolean;
onAdjustTimeBankOpen?: (teamId: string) => void; onAdjustTimeBankOpen?: (teamId: string) => void;
onSetAutodraftOpen?: (teamId: string) => void; onSetAutodraftOpen?: (teamId: string) => void;
onForceAutopick: (pickNumber: number, teamId: string) => void; onForceAutopick?: (pickNumber: number, teamId: string) => void;
onForceManualPickOpen: (pickNumber: number, teamId: string) => void; onForceManualPickOpen?: (pickNumber: number, teamId: string) => void;
onReplacePick?: (pickNumber: number, teamId: string) => void; onReplacePick?: (pickNumber: number, teamId: string) => void;
onRollbackToPick?: (pickNumber: number) => void; onRollbackToPick?: (pickNumber: number) => void;
ownerMap?: Record<string, string>; ownerMap?: Record<string, string>;
@ -196,7 +196,7 @@ export const DraftGridSection = memo(function DraftGridSection({
const mobileButtons = isCommissioner && ( const mobileButtons = isCommissioner && (
<> <>
{!isPicked && isCurrent && ( {!isPicked && isCurrent && (onForceAutopick || onForceManualPickOpen) && (
<button <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" 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 })} onClick={() => setMobileSheet({ type: "current-cell", pickNumber: cell.pickNumber, teamId: cell.teamId })}
@ -215,7 +215,7 @@ export const DraftGridSection = memo(function DraftGridSection({
</> </>
); );
if (isCommissioner && !isPicked && isCurrent) { if (isCommissioner && !isPicked && isCurrent && (onForceAutopick || onForceManualPickOpen)) {
return ( return (
<ContextMenu key={cell.pickNumber}> <ContextMenu key={cell.pickNumber}>
<ContextMenuTrigger asChild> <ContextMenuTrigger asChild>
@ -234,23 +234,16 @@ export const DraftGridSection = memo(function DraftGridSection({
</DraftPickCell> </DraftPickCell>
</ContextMenuTrigger> </ContextMenuTrigger>
<ContextMenuContent> <ContextMenuContent>
<ContextMenuItem {onForceAutopick && (
onClick={() => <ContextMenuItem onClick={() => onForceAutopick(cell.pickNumber, cell.teamId)}>
onForceAutopick(cell.pickNumber, cell.teamId) Force Auto Pick
} </ContextMenuItem>
> )}
Force Auto Pick {onForceManualPickOpen && (
</ContextMenuItem> <ContextMenuItem onClick={() => onForceManualPickOpen(cell.pickNumber, cell.teamId)}>
<ContextMenuItem Force Manual Pick
onClick={() => </ContextMenuItem>
onForceManualPickOpen( )}
cell.pickNumber,
cell.teamId
)
}
>
Force Manual Pick
</ContextMenuItem>
</ContextMenuContent> </ContextMenuContent>
</ContextMenu> </ContextMenu>
); );
@ -354,23 +347,27 @@ export const DraftGridSection = memo(function DraftGridSection({
)} )}
{mobileSheet?.type === "current-cell" && ( {mobileSheet?.type === "current-cell" && (
<> <>
<Button {onForceAutopick && (
onClick={() => { <Button
onForceAutopick(mobileSheet.pickNumber, mobileSheet.teamId); onClick={() => {
setMobileSheet(null); onForceAutopick(mobileSheet.pickNumber, mobileSheet.teamId);
}} setMobileSheet(null);
> }}
Force Auto Pick >
</Button> Force Auto Pick
<Button </Button>
variant="outline" )}
onClick={() => { {onForceManualPickOpen && (
onForceManualPickOpen(mobileSheet.pickNumber, mobileSheet.teamId); <Button
setMobileSheet(null); variant="outline"
}} onClick={() => {
> onForceManualPickOpen(mobileSheet.pickNumber, mobileSheet.teamId);
Force Manual Pick setMobileSheet(null);
</Button> }}
>
Force Manual Pick
</Button>
)}
</> </>
)} )}
{mobileSheet?.type === "picked-cell" && ( {mobileSheet?.type === "picked-cell" && (

View file

@ -143,3 +143,33 @@ export const EmptyGrid: Story = {
ownerMap, ownerMap,
}, },
}; };
export const CommissionerView: Story = {
name: "Commissioner — All Actions Available (right-click headers and cells)",
args: {
draftSlots,
draftGrid: fullGrid,
currentPick: 6,
currentRound: 2,
ownerMap,
connectedTeams: new Set(["team-1", "team-2", "team-3"]), // team-4 disconnected
onAdjustTimeBankOpen: (teamId) => alert(`Adjust time bank for ${teamId}`),
onSetAutodraftOpen: (teamId) => alert(`Set autodraft for ${teamId}`),
onForceAutopick: (pickNumber, teamId) => alert(`Force autopick ${pickNumber} for ${teamId}`),
onForceManualPickOpen: (pickNumber, teamId) => alert(`Force manual pick ${pickNumber} for ${teamId}`),
onReplacePick: (pickNumber, teamId) => alert(`Replace pick ${pickNumber} for ${teamId}`),
onRollbackToPick: (pickNumber) => alert(`Roll back to pick ${pickNumber}`),
},
};
export const DisconnectedTeams: Story = {
name: "Disconnected Teams",
args: {
draftSlots,
draftGrid: fullGrid,
currentPick: 3,
currentRound: 1,
ownerMap,
connectedTeams: new Set(["team-1"]), // team-2 through team-4 disconnected
},
};

View file

@ -48,6 +48,7 @@ export interface MiniDraftGridProps {
ownerMap?: Record<string, string>; ownerMap?: Record<string, string>;
teamTimers?: Record<string, number | undefined>; teamTimers?: Record<string, number | undefined>;
autodraftStatus?: Record<string, { isEnabled: boolean }>; autodraftStatus?: Record<string, { isEnabled: boolean }>;
connectedTeams?: Set<string>;
seasonStatus?: SeasonStatus; seasonStatus?: SeasonStatus;
draftPaused?: boolean; draftPaused?: boolean;
onAdjustTimeBankOpen?: (teamId: string) => void; onAdjustTimeBankOpen?: (teamId: string) => void;
@ -66,6 +67,7 @@ export const MiniDraftGrid = memo(function MiniDraftGrid({
ownerMap = {}, ownerMap = {},
teamTimers = {}, teamTimers = {},
autodraftStatus = {}, autodraftStatus = {},
connectedTeams,
seasonStatus, seasonStatus,
draftPaused, draftPaused,
onAdjustTimeBankOpen, onAdjustTimeBankOpen,
@ -152,51 +154,60 @@ export const MiniDraftGrid = memo(function MiniDraftGrid({
<div className="inline-block min-w-full"> <div className="inline-block min-w-full">
{/* Team header */} {/* Team header */}
<div className="flex gap-1.5 mb-1.5"> <div className="flex gap-1.5 mb-1.5">
{draftSlots.map((slot) => { {(() => {
const teamTime = teamTimers[slot.team.id];
const isAutodraft = autodraftStatus[slot.team.id]?.isEnabled ?? false;
const hasHeaderMenu = !!(onAdjustTimeBankOpen || onSetAutodraftOpen); const hasHeaderMenu = !!(onAdjustTimeBankOpen || onSetAutodraftOpen);
return draftSlots.map((slot) => {
const teamTime = teamTimers[slot.team.id];
const isAutodraft = autodraftStatus[slot.team.id]?.isEnabled ?? false;
const isConnected = connectedTeams ? connectedTeams.has(slot.team.id) : true;
const headerInner = ( const headerContent = (
<div className="flex-1 min-w-20 text-center"> <>
<div className="text-xs font-medium truncate px-1 flex items-center justify-center gap-0.5"> <div className="text-xs font-medium truncate px-1 flex items-center justify-center gap-0.5">
{isAutodraft && ( {isAutodraft && (
<span className="inline-flex items-center justify-center h-3.5 w-3.5 text-[9px] font-bold text-white bg-black shrink-0">A</span> <span className="inline-flex items-center justify-center h-3.5 w-3.5 text-[9px] 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 ${getTimerColorClass(teamTime)}`}>
{formatClockTime(teamTime)}
</div>
</div>
);
if (hasHeaderMenu) {
return (
<ContextMenu key={slot.id}>
<ContextMenuTrigger asChild>
<div className="cursor-context-menu">
{headerInner}
</div>
</ContextMenuTrigger>
<ContextMenuContent>
{onAdjustTimeBankOpen && (
<ContextMenuItem onClick={() => onAdjustTimeBankOpen(slot.team.id)}>
Adjust Time Bank...
</ContextMenuItem>
)} )}
{onSetAutodraftOpen && ( <span className={`truncate${!isConnected ? " italic text-muted-foreground" : ""}`}>
<ContextMenuItem onClick={() => onSetAutodraftOpen(slot.team.id)}> {ownerMap[slot.team.id] || slot.team.name}
Set Autodraft... </span>
</ContextMenuItem> </div>
)} <div className={`text-xs font-mono px-1 ${getTimerColorClass(teamTime)}`}>
</ContextMenuContent> {formatClockTime(teamTime)}
</ContextMenu> </div>
</>
); );
}
return <div key={slot.id}>{headerInner}</div>; if (hasHeaderMenu) {
})} return (
<ContextMenu key={slot.id}>
<ContextMenuTrigger asChild>
<div className="flex-1 min-w-20 text-center cursor-context-menu">
{headerContent}
</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">
{headerContent}
</div>
);
});
})()}
</div> </div>
{/* Rows — clipped to 2-row height, slides to reveal new round */} {/* Rows — clipped to 2-row height, slides to reveal new round */}

View file

@ -953,6 +953,7 @@ export default function DraftRoom() {
ownerMap, ownerMap,
teamTimers, teamTimers,
autodraftStatus, autodraftStatus,
connectedTeams,
seasonStatus: season.status, seasonStatus: season.status,
draftPaused: isPaused, draftPaused: isPaused,
onAdjustTimeBankOpen: isCommissioner ? handleAdjustTimeBankOpen : undefined, onAdjustTimeBankOpen: isCommissioner ? handleAdjustTimeBankOpen : undefined,
@ -961,7 +962,7 @@ export default function DraftRoom() {
onForceManualPickOpen: isCommissioner ? handleForceManualPickOpen : undefined, onForceManualPickOpen: isCommissioner ? handleForceManualPickOpen : undefined,
onReplacePick: isCommissioner ? handleReplacePickOpen : undefined, onReplacePick: isCommissioner ? handleReplacePickOpen : undefined,
onRollbackToPick: isCommissioner && !isDraftComplete ? handleRollbackToPick : undefined, onRollbackToPick: isCommissioner && !isDraftComplete ? handleRollbackToPick : undefined,
}), [draftSlots, draftGrid, currentPick, currentRound, ownerMap, teamTimers, autodraftStatus, season.status, isPaused, isCommissioner, handleAdjustTimeBankOpen, handleSetAutodraftOpen, handleForceAutopick, handleForceManualPickOpen, handleReplacePickOpen, handleRollbackToPick, isDraftComplete]); }), [draftSlots, draftGrid, currentPick, currentRound, ownerMap, teamTimers, autodraftStatus, connectedTeams, season.status, isPaused, isCommissioner, handleAdjustTimeBankOpen, handleSetAutodraftOpen, handleForceAutopick, handleForceManualPickOpen, handleReplacePickOpen, handleRollbackToPick, isDraftComplete]);
const availableParticipantsSectionProps = useMemo(() => ({ const availableParticipantsSectionProps = useMemo(() => ({
participants: filteredParticipants, participants: filteredParticipants,
@ -1003,8 +1004,8 @@ export default function DraftRoom() {
draftPaused: isPaused, draftPaused: isPaused,
onAdjustTimeBankOpen: isCommissioner ? handleAdjustTimeBankOpen : undefined, onAdjustTimeBankOpen: isCommissioner ? handleAdjustTimeBankOpen : undefined,
onSetAutodraftOpen: isCommissioner ? handleSetAutodraftOpen : undefined, onSetAutodraftOpen: isCommissioner ? handleSetAutodraftOpen : undefined,
onForceAutopick: handleForceAutopick, onForceAutopick: isCommissioner ? handleForceAutopick : undefined,
onForceManualPickOpen: handleForceManualPickOpen, onForceManualPickOpen: isCommissioner ? handleForceManualPickOpen : undefined,
onReplacePick: isCommissioner ? handleReplacePickOpen : undefined, onReplacePick: isCommissioner ? handleReplacePickOpen : undefined,
onRollbackToPick: isCommissioner && !isDraftComplete ? handleRollbackToPick : undefined, onRollbackToPick: isCommissioner && !isDraftComplete ? handleRollbackToPick : undefined,
}; };