From 32bfbf2564a97e0b0a5d7f45e977cc792aa7c7c5 Mon Sep 17 00:00:00 2001 From: Chris Parsons Date: Fri, 17 Apr 2026 08:47:53 -0700 Subject: [PATCH] Draft room improvements. --- app/components/NotificationSettings.tsx | 36 +++++ .../AvailableParticipantsSection.test.tsx | 5 + .../draft/AvailableParticipantsSection.tsx | 44 +++++- .../draft/MiniDraftGrid.stories.tsx | 145 ++++++++++++++++++ app/components/draft/MiniDraftGrid.tsx | 112 ++++++++++++++ .../leagues/$leagueId.draft.$seasonId.tsx | 102 +++++------- 6 files changed, 377 insertions(+), 67 deletions(-) create mode 100644 app/components/draft/MiniDraftGrid.stories.tsx create mode 100644 app/components/draft/MiniDraftGrid.tsx diff --git a/app/components/NotificationSettings.tsx b/app/components/NotificationSettings.tsx index 0d48152..51d9225 100644 --- a/app/components/NotificationSettings.tsx +++ b/app/components/NotificationSettings.tsx @@ -9,6 +9,7 @@ interface NotificationSettingsProps { mode: NotificationMode; onModeChange: (mode: NotificationMode) => void; permissionState: NotificationPermission | "unsupported"; + switchOnly?: boolean; } export function NotificationSettings({ @@ -17,11 +18,46 @@ export function NotificationSettings({ mode, onModeChange, permissionState, + switchOnly = false, }: NotificationSettingsProps) { if (permissionState === "unsupported") { return null; } + if (switchOnly) { + return ( + <> + + {enabled && ( +
+ onModeChange(value as NotificationMode)} + className="space-y-2" + > +
+ + +
+
+ + +
+
+
+ )} + + ); + } + return (
diff --git a/app/components/__tests__/AvailableParticipantsSection.test.tsx b/app/components/__tests__/AvailableParticipantsSection.test.tsx index 3d28666..76ccd15 100644 --- a/app/components/__tests__/AvailableParticipantsSection.test.tsx +++ b/app/components/__tests__/AvailableParticipantsSection.test.tsx @@ -29,6 +29,11 @@ const defaultProps = { onMakePick: vi.fn(), onAddToQueue: vi.fn(), onRemoveFromQueue: vi.fn(), + participantRanks: new Map([ + ["1", { overallRank: 1, sportRank: 1 }], + ["2", { overallRank: 2, sportRank: 1 }], + ["3", { overallRank: 3, sportRank: 2 }], + ]), }; // Both the mobile Sheet trigger and desktop Popover trigger are rendered in jsdom diff --git a/app/components/draft/AvailableParticipantsSection.tsx b/app/components/draft/AvailableParticipantsSection.tsx index 4733bce..8d4f6aa 100644 --- a/app/components/draft/AvailableParticipantsSection.tsx +++ b/app/components/draft/AvailableParticipantsSection.tsx @@ -2,6 +2,7 @@ import { memo, useCallback, useMemo } from "react"; import { Button } from "~/components/ui/button"; import { Badge } from "~/components/ui/badge"; import { Checkbox } from "~/components/ui/checkbox"; +import { MiniDraftGrid } from "~/components/draft/MiniDraftGrid"; import { Popover, PopoverTrigger, @@ -130,6 +131,29 @@ interface AvailableParticipantsSectionProps { name: string; }; }>; + participantRanks: Map; + miniDraftGrid?: { + 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; + }; searchQuery: string; sportFilters: string[]; hideDrafted: boolean; @@ -157,6 +181,8 @@ interface AvailableParticipantsSectionProps { export const AvailableParticipantsSection = memo(function AvailableParticipantsSection({ participants, + participantRanks, + miniDraftGrid, searchQuery, sportFilters, hideDrafted, @@ -243,6 +269,11 @@ export const AvailableParticipantsSection = memo(function AvailableParticipantsS return (
+ {miniDraftGrid && ( +
+ +
+ )}
{/* Search Input */} @@ -395,6 +426,9 @@ export const AvailableParticipantsSection = memo(function AvailableParticipantsS {participant.sport.name} + + OVR {participantRanks.get(participant.id)?.overallRank} · SPR {participantRanks.get(participant.id)?.sportRank} + {isDrafted && ( Drafted @@ -473,6 +507,8 @@ export const AvailableParticipantsSection = memo(function AvailableParticipantsS + + {hasTeam && ( @@ -484,7 +520,7 @@ export const AvailableParticipantsSection = memo(function AvailableParticipantsS {participants.length === 0 ? ( +
OVRSPR Participant Sport
{emptyMessage} @@ -509,6 +545,12 @@ export const AvailableParticipantsSection = memo(function AvailableParticipantsS !isEligible && !isDrafted ? ineligibleReason : undefined } > + + {participantRanks.get(participant.id)?.overallRank} + + {participantRanks.get(participant.id)?.sportRank} +
= { + title: "Draft/MiniDraftGrid", + component: MiniDraftGrid, + parameters: { + layout: "padded", + }, +}; + +export default meta; +type Story = StoryObj; + +const teamNames = ["Alpha", "Bravo", "Charlie", "Delta"]; +const ownerMap: Record = {}; +const draftSlots = teamNames.map((name, i) => { + const id = `team-${i + 1}`; + ownerMap[id] = name; + return { id, draftOrder: i + 1, team: { id, name, logoUrl: null } }; +}); + +function buildRound(round: number, teamCount: number, picks: Array) { + const isEvenRound = round % 2 === 0; + return Array.from({ length: teamCount }, (_, i) => { + const teamIndex = isEvenRound ? teamCount - 1 - i : i; + const pickNumber = (round - 1) * teamCount + i + 1; + const pick = picks[i]; + return { + pickNumber, + round, + pickInRound: i + 1, + teamId: draftSlots[teamIndex].id, + ...(pick !== null + ? { + pick: { + participant: { name: `Participant ${pick}` }, + sport: { name: ["NFL", "NBA", "NHL", "MLB"][pick % 4] }, + }, + } + : {}), + }; + }); +} + +const fullGrid = [ + buildRound(1, 4, [1, 2, 3, 4]), + buildRound(2, 4, [5, 6, null, null]), + buildRound(3, 4, [null, null, null, null]), + buildRound(4, 4, [null, null, null, null]), +]; + +export const Round1ShowsFirstTwo: Story = { + name: "Round 1 — Shows Rounds 1 & 2", + args: { + draftSlots, + draftGrid: fullGrid, + currentPick: 1, + currentRound: 1, + ownerMap, + }, +}; + +export const Round2: Story = { + name: "Round 2 — Shows Rounds 1 & 2", + args: { + draftSlots, + draftGrid: fullGrid, + currentPick: 5, + currentRound: 2, + ownerMap, + }, +}; + +export const Round3: Story = { + name: "Round 3 — Shows Rounds 2 & 3", + args: { + draftSlots, + draftGrid: fullGrid, + currentPick: 9, + currentRound: 3, + ownerMap, + }, +}; + +export const Round4: Story = { + name: "Round 4 — Shows Rounds 3 & 4", + args: { + draftSlots, + draftGrid: fullGrid, + currentPick: 13, + currentRound: 4, + ownerMap, + }, +}; + +export const AllPicked: Story = { + name: "Both Rounds Fully Picked", + args: { + draftSlots, + draftGrid: [ + buildRound(1, 4, [1, 2, 3, 4]), + buildRound(2, 4, [5, 6, 7, 8]), + buildRound(3, 4, [null, null, null, null]), + ], + currentPick: 9, + currentRound: 3, + ownerMap, + }, +}; + +export const SixTeams: Story = { + name: "6 Teams — Snake Draft", + args: (() => { + const names6 = ["Alpha", "Bravo", "Charlie", "Delta", "Echo", "Foxtrot"]; + const ownerMap6: Record = {}; + const slots6 = names6.map((name, i) => { + const id = `team-${i + 1}`; + ownerMap6[id] = name; + return { id, draftOrder: i + 1, team: { id, name, logoUrl: null } }; + }); + const grid6 = [ + buildRound(1, 6, [1, 2, 3, 4, 5, 6]), + buildRound(2, 6, [7, 8, null, null, null, null]), + ]; + return { + draftSlots: slots6, + draftGrid: grid6, + currentPick: 9, + currentRound: 2, + ownerMap: ownerMap6, + }; + })(), +}; + +export const EmptyGrid: Story = { + name: "Empty Grid", + args: { + draftSlots, + draftGrid: [], + currentPick: 1, + currentRound: 1, + ownerMap, + }, +}; diff --git a/app/components/draft/MiniDraftGrid.tsx b/app/components/draft/MiniDraftGrid.tsx new file mode 100644 index 0000000..50e7356 --- /dev/null +++ b/app/components/draft/MiniDraftGrid.tsx @@ -0,0 +1,112 @@ +import { memo, useMemo } from "react"; +import { DraftPickCell } from "~/components/draft/DraftPickCell"; + +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; +} + +export const MiniDraftGrid = memo(function MiniDraftGrid({ + draftSlots, + draftGrid, + currentPick, + currentRound, + ownerMap = {}, +}: MiniDraftGridProps) { + const roundsToShow = useMemo(() => { + if (currentRound <= 1) return [0, 1]; + return [currentRound - 2, currentRound - 1]; + }, [currentRound]); + + if (draftGrid.length === 0) return null; + + return ( +
+
+
+
+ {draftSlots.map((slot) => ( +
+
+ {ownerMap[slot.team.id] || slot.team.name} +
+
+ ))} +
+
+ {roundsToShow.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 ( +
+
+ R{round} +
+ {displayPicks.map((cell) => { + const isCurrent = cell.pickNumber === currentPick; + const isPicked = !!cell.pick; + const cellState = isPicked + ? "picked" + : isCurrent + ? "current" + : "upcoming"; + + return ( + + ); + })} +
+ ); + })} +
+
+
+ ); +}); diff --git a/app/routes/leagues/$leagueId.draft.$seasonId.tsx b/app/routes/leagues/$leagueId.draft.$seasonId.tsx index 6eb8ca7..c89cfd3 100644 --- a/app/routes/leagues/$leagueId.draft.$seasonId.tsx +++ b/app/routes/leagues/$leagueId.draft.$seasonId.tsx @@ -643,6 +643,17 @@ export default function DraftRoom() { ); }, [availableParticipants]); + const participantRanks = useMemo(() => { + const ranks = new Map(); + const sportCounters = new Map(); + availableParticipants.forEach((p, i) => { + const sportIdx = (sportCounters.get(p.sport.id) ?? 0) + 1; + sportCounters.set(p.sport.id, sportIdx); + ranks.set(p.id, { overallRank: i + 1, sportRank: sportIdx }); + }); + return ranks; + }, [availableParticipants]); + // Calculate draft eligibility for current user's team const eligibility = useMemo(() => { if (!userTeam) return null; @@ -1493,6 +1504,14 @@ export default function DraftRoom() { const availableParticipantsSectionProps = { participants: filteredParticipants, + participantRanks, + miniDraftGrid: { + draftSlots, + draftGrid, + currentPick, + currentRound, + ownerMap, + }, searchQuery, sportFilters, hideDrafted, @@ -1626,20 +1645,25 @@ export default function DraftRoom() { )} -
-

- {season.league.name} - {season.year} Draft -

-
- Round: {currentRound} - Pick: {currentPick} - - {season.status.replace("_", " ")} - -
-
+ + Brackt + +

+ {season.league.name} Draft Room +

+
+ Pick Notifications + +
{/* Commissioner Controls - hidden on mobile (shown in Controls tab) */} {isCommissioner && season.status === "pre_draft" && ( @@ -1659,16 +1683,6 @@ export default function DraftRoom() {
)} -
-
- - {isConnected ? "Connected" : "Disconnected"} - -
@@ -1783,14 +1797,6 @@ export default function DraftRoom() { - - @@ -1805,15 +1811,6 @@ export default function DraftRoom() { onCollapsedChange={setSidebarCollapsed} queueSection={} recentPicksSection={} - settingsSection={ - - } /> )} @@ -1837,33 +1834,6 @@ export default function DraftRoom() { Rosters Summary - - {season.status === "draft" && !isDraftComplete && currentDraftSlot && ( -
-
-
- {isMyTurn ? "Your Turn!" : "On the Clock"} -
-
- {currentDraftSlotOwnerName ?? currentDraftSlot.team.name} -
- {currentDraftSlotOwnerName && ( -
{currentDraftSlot.team.name}
- )} -
- {isPaused ? ( - Paused - ) : ( - - {formatClockTime(currentClockTime)} - - )} -
- )}