diff --git a/.opencode/plugins/oxlint.ts b/.opencode/plugins/oxlint.ts new file mode 100644 index 0000000..365eaa9 --- /dev/null +++ b/.opencode/plugins/oxlint.ts @@ -0,0 +1,20 @@ +export const OxlintPlugin = async ({ $ }: { $: any }) => { + return { + "file.edited": async ({ filePath }: { filePath: string }) => { + if ( + !filePath.endsWith(".ts") && + !filePath.endsWith(".tsx") && + !filePath.endsWith(".js") && + !filePath.endsWith(".jsx") + ) { + return + } + + try { + await $`npm run lint:path -- ${filePath}`.quiet() + } catch (e: any) { + throw new Error(`oxlint found issues in ${filePath}:\n${e}`) + } + }, + } +} diff --git a/app/components/__tests__/AvailableParticipantsSection.test.tsx b/app/components/__tests__/AvailableParticipantsSection.test.tsx index 5c6c433..a3b02a1 100644 --- a/app/components/__tests__/AvailableParticipantsSection.test.tsx +++ b/app/components/__tests__/AvailableParticipantsSection.test.tsx @@ -9,9 +9,9 @@ vi.mock("@tanstack/react-virtual", () => ({ estimateSize, }: { count: number; - estimateSize: () => number; + estimateSize: (index: number) => number; }) => { - const size = estimateSize(); + const size = estimateSize(0); const items = Array.from({ length: count }, (_, i) => ({ index: i, start: i * size, @@ -171,6 +171,154 @@ describe("AvailableParticipantsSection", () => { }); }); + describe("Projected Pick Dividers", () => { + const manyParticipants = Array.from({ length: 30 }, (_, i) => ({ + id: String(i + 1), + name: `Player ${i + 1}`, + sport: { id: "s1", name: "NFL" }, + })); + + const manyRanks = new Map( + Array.from({ length: 30 }, (_, i) => [ + String(i + 1), + { overallRank: i + 1, sportRank: i + 1 }, + ]) + ); + + it("renders projected pick divider at correct position", () => { + render( + + ); + + expect(screen.getByText("Projected Round 3 Pick")).toBeInTheDocument(); + expect(screen.getByText("Projected Round 4 Pick")).toBeInTheDocument(); + }); + + it("does not render dividers when projectedPicks is undefined", () => { + render( + + ); + + expect(screen.queryByText(/Projected Round \d+ Pick/)).not.toBeInTheDocument(); + }); + + it("does not render dividers when projectedPicks is empty", () => { + render( + + ); + + expect(screen.queryByText(/Projected Round \d+ Pick/)).not.toBeInTheDocument(); + }); + + it("skips dividers where picksFromNow exceeds list length", () => { + render( + + ); + + expect(screen.getByText("Projected Round 3 Pick")).toBeInTheDocument(); + expect(screen.queryByText("Projected Round 10 Pick")).not.toBeInTheDocument(); + }); + + it("skips dividers where picksFromNow is zero or negative", () => { + render( + + ); + + expect(screen.queryByText("Projected Round 1 Pick")).not.toBeInTheDocument(); + expect(screen.getByText("Projected Round 2 Pick")).toBeInTheDocument(); + }); + + it("still renders all participants alongside dividers", () => { + render( + + ); + + expect(screen.getAllByText("Player 1").length).toBeGreaterThan(0); + expect(screen.getAllByText("Player 10").length).toBeGreaterThan(0); + expect(screen.getAllByText("Player 30").length).toBeGreaterThan(0); + }); + + it("suppresses dividers when search query is active", () => { + render( + + ); + + expect(screen.queryByText(/Projected Round \d+ Pick/)).not.toBeInTheDocument(); + }); + + it("suppresses dividers when sport filter is active", () => { + render( + + ); + + expect(screen.queryByText(/Projected Round \d+ Pick/)).not.toBeInTheDocument(); + }); + + it("suppresses dividers when hideDrafted is active", () => { + render( + + ); + + expect(screen.queryByText(/Projected Round \d+ Pick/)).not.toBeInTheDocument(); + }); + }); + describe("Sport Filter - sheet/popover", () => { it("opens and shows checkboxes for each sport", async () => { render(); diff --git a/app/components/draft/AvailableParticipantsSection.tsx b/app/components/draft/AvailableParticipantsSection.tsx index 94e5781..b0fac0c 100644 --- a/app/components/draft/AvailableParticipantsSection.tsx +++ b/app/components/draft/AvailableParticipantsSection.tsx @@ -19,6 +19,10 @@ import { } from "~/components/ui/sheet"; import { ListPlus, ListX, ChevronDown } from "lucide-react"; +type ListItem = + | { type: "participant"; index: number; key: string } + | { type: "divider"; round: number; key: string }; + function getParticipantState( participant: { id: string; sport: { id: string } }, draftedParticipantIds: Set, @@ -157,6 +161,7 @@ interface AvailableParticipantsSectionProps { onMakePick: (participantId: string) => void; onAddToQueue: (participantId: string) => void; onRemoveFromQueue: (queueId: string) => void; + projectedPicks?: Array<{ round: number; picksFromNow: number }>; } export const AvailableParticipantsSection = memo(function AvailableParticipantsSection({ @@ -183,6 +188,7 @@ export const AvailableParticipantsSection = memo(function AvailableParticipantsS onMakePick, onAddToQueue, onRemoveFromQueue, + projectedPicks, }: AvailableParticipantsSectionProps) { const queueMap = useMemo( () => new Map(queue.map((item) => [item.participantId, item.id])), @@ -247,13 +253,41 @@ export const AvailableParticipantsSection = memo(function AvailableParticipantsS const hasActiveFilters = sportFilters.length > 0 || hideCompletedSports; + const anyFilterActive = searchQuery !== "" || sportFilters.length > 0 || hideDrafted || hideIneligible || hideCompletedSports; + + const listItems = useMemo(() => { + const items: ListItem[] = []; + if (!projectedPicks || projectedPicks.length === 0 || anyFilterActive) { + for (let i = 0; i < participants.length; i++) { + items.push({ type: "participant", index: i, key: participants[i].id }); + } + return items; + } + const dividersByAfterIndex = new Map(); + for (const pp of projectedPicks) { + if (pp.picksFromNow <= 0) continue; + const afterIndex = pp.picksFromNow - 1; + if (afterIndex >= 0 && afterIndex < participants.length - 1) { + dividersByAfterIndex.set(afterIndex, pp.round); + } + } + for (let i = 0; i < participants.length; i++) { + items.push({ type: "participant", index: i, key: participants[i].id }); + const dividerRound = dividersByAfterIndex.get(i); + if (dividerRound !== undefined) { + items.push({ type: "divider", round: dividerRound, key: `divider-rd${dividerRound}` }); + } + } + return items; + }, [participants, projectedPicks, anyFilterActive]); + const parentRef = useRef(null); const virtualizer = useVirtualizer({ - count: participants.length, + count: listItems.length, getScrollElement: () => parentRef.current, - estimateSize: () => 72, + estimateSize: (index) => listItems[index]?.type === "divider" ? 36 : 72, overscan: 5, - getItemKey: (index) => participants[index]?.id ?? index, + getItemKey: (index) => listItems[index].key, }); const desktopGridClass = hasTeam @@ -400,10 +434,37 @@ export const AvailableParticipantsSection = memo(function AvailableParticipantsS }} > {virtualizer.getVirtualItems().map((virtualItem) => { - const participant = participants[virtualItem.index]; + const listItem = listItems[virtualItem.index]; + + if (listItem.type === "divider") { + return ( +
+
+
+ + Projected Round {listItem.round} Pick + +
+
+
+ ); + } + + const participant = participants[listItem.index]; const { isDrafted, isInQueue, isEligible, ineligibleReason } = getParticipantState(participant, draftedParticipantIds, queueMap, eligibility); const rank = participantRanks.get(participant.id); + const followsDivider = listItems[virtualItem.index - 1]?.type === "divider"; return (
(picks.at(-1)?.id); const [animatingId, setAnimatingId] = useState(undefined); diff --git a/app/components/league/LeagueRow.tsx b/app/components/league/LeagueRow.tsx index a0414b6..0784d69 100644 --- a/app/components/league/LeagueRow.tsx +++ b/app/components/league/LeagueRow.tsx @@ -2,7 +2,7 @@ import { formatDistanceToNow } from "date-fns"; import { Link } from "react-router"; import { Button } from "~/components/ui/button"; import { LeagueAvatar } from "./LeagueAvatar"; -import { StatColumn, StatDivider, RankChangeIndicator } from "./StatHelpers"; +import { StatColumn, StatDivider, RankingDisplay } from "./StatHelpers"; export interface LeagueRowProps { leagueId: string; @@ -10,6 +10,7 @@ export interface LeagueRowProps { numSports: number; status: "draft" | "active" | "pre_draft" | "completed"; seasonId?: string; + displayRank?: string | number; currentRank?: number; totalPoints?: number; previousRank?: number; @@ -93,6 +94,7 @@ function DraftRow({ leagueId, leagueName, seasonId, picksUntilMyTurn }: LeagueRo function ActiveRow({ leagueId, leagueName, + displayRank, currentRank, totalPoints, previousRank, @@ -116,13 +118,11 @@ function ActiveRow({ {/* Stats — second row on mobile, right side on desktop */} {showStats && (
- {currentRank !== undefined && ( - - #{currentRank} - {previousRank !== undefined && previousRank !== currentRank && ( - - )} - + {displayRank !== undefined && ( + )} {currentRank !== undefined && totalPoints !== undefined && } {totalPoints !== undefined && ( diff --git a/app/components/league/StandingsPreview.tsx b/app/components/league/StandingsPreview.tsx index 1d313ee..c6b5df2 100644 --- a/app/components/league/StandingsPreview.tsx +++ b/app/components/league/StandingsPreview.tsx @@ -4,7 +4,7 @@ import { Button } from "~/components/ui/button"; import { Card, CardContent, CardHeader } from "~/components/ui/card"; import { GradientIcon } from "~/components/ui/GradientIcon"; import { TeamAvatar } from "~/components/TeamAvatar"; -import { StatColumn, StatDivider, RankChangeIndicator, PointChangeIndicator } from "./StatHelpers"; +import { StatColumn, StatDivider, PointChangeIndicator, RankingDisplay } from "./StatHelpers"; // ─── Row styles ─────────────────────────────────────────────────────────────── @@ -38,12 +38,7 @@ function RowContent({ entry }: { entry: StandingsPreviewEntry }) { {/* Right: stats — second row on mobile */}
- - #{String(entry.displayRank).replace(/^T/, "")} - {entry.rankChange !== undefined && entry.rankChange !== 0 && ( - - )} - + diff --git a/app/components/league/StatHelpers.tsx b/app/components/league/StatHelpers.tsx index e1db42a..a21525c 100644 --- a/app/components/league/StatHelpers.tsx +++ b/app/components/league/StatHelpers.tsx @@ -39,6 +39,23 @@ export function RankChangeIndicator({ delta }: { delta: number }) { ); } +export function RankingDisplay({ + displayRank, + rankChange, +}: { + displayRank: string | number; + rankChange?: number; +}) { + return ( + + {displayRank} + {rankChange !== undefined && rankChange !== 0 && ( + + )} + + ); +} + export function PointChangeIndicator({ delta }: { delta: number }) { if (delta >= 0) { return ( diff --git a/app/components/navbar.tsx b/app/components/navbar.tsx index cd544ad..9c940e6 100644 --- a/app/components/navbar.tsx +++ b/app/components/navbar.tsx @@ -1,5 +1,5 @@ import { useState } from "react"; -import { Link, useLocation } from "react-router"; +import { Link, NavLink, useLocation } from "react-router"; import { Sheet, SheetContent, @@ -11,7 +11,19 @@ import { Button } from "~/components/ui/button"; import { authClient } from "~/lib/auth-client"; import { UserMenu } from "~/components/UserMenu"; import { HelpCircle, MenuIcon, Settings } from "lucide-react"; -import logoUrl from "../../public/logo.svg?url"; + +const NAV_LINK_CLASS = + "relative text-sm font-semibold uppercase tracking-wide text-muted-foreground px-3 py-2 transition-colors " + + "after:content-[''] after:absolute after:bottom-0 after:left-3 after:right-3 after:h-[3px] " + + "after:scale-x-0 hover:after:scale-x-100 after:transition-transform after:[background:var(--brackt-gradient)]"; + +function navLinkClass({ isActive }: { isActive: boolean }) { + return `${NAV_LINK_CLASS}${isActive ? " !text-emerald-400" : ""}`; +} + +function mobileNavLinkClass({ isActive }: { isActive: boolean }) { + return `block px-4 py-2 text-lg font-medium rounded-md transition-colors${isActive ? " text-emerald-400" : " text-muted-foreground"}`; +} interface NavbarProps { isAdmin: boolean; @@ -42,11 +54,15 @@ export function Navbar({ isAdmin }: NavbarProps) { {/* Left: Logo + Nav links */}
- Brackt + Brackt
@@ -105,20 +121,28 @@ export function Navbar({ isAdmin }: NavbarProps) { Menu diff --git a/app/lib/__tests__/draft-order.test.ts b/app/lib/__tests__/draft-order.test.ts new file mode 100644 index 0000000..4e0c937 --- /dev/null +++ b/app/lib/__tests__/draft-order.test.ts @@ -0,0 +1,210 @@ +import { describe, it, expect } from "vitest"; +import { getTeamForPick, getProjectedPicks } from "~/lib/draft-order"; + +function makeSlots(teamCount: number) { + return Array.from({ length: teamCount }, (_, i) => ({ + draftOrder: i + 1, + teamId: `team-${i + 1}`, + })); +} + +describe("getTeamForPick", () => { + const slots = makeSlots(4); + + it("returns correct team for round 1 (forward)", () => { + expect(getTeamForPick(1, slots)?.teamId).toBe("team-1"); + expect(getTeamForPick(2, slots)?.teamId).toBe("team-2"); + expect(getTeamForPick(3, slots)?.teamId).toBe("team-3"); + expect(getTeamForPick(4, slots)?.teamId).toBe("team-4"); + }); + + it("returns correct team for round 2 (snake reversed)", () => { + expect(getTeamForPick(5, slots)?.teamId).toBe("team-4"); + expect(getTeamForPick(6, slots)?.teamId).toBe("team-3"); + expect(getTeamForPick(7, slots)?.teamId).toBe("team-2"); + expect(getTeamForPick(8, slots)?.teamId).toBe("team-1"); + }); + + it("returns correct team for round 3 (forward again)", () => { + expect(getTeamForPick(9, slots)?.teamId).toBe("team-1"); + expect(getTeamForPick(10, slots)?.teamId).toBe("team-2"); + expect(getTeamForPick(11, slots)?.teamId).toBe("team-3"); + expect(getTeamForPick(12, slots)?.teamId).toBe("team-4"); + }); + + it("returns undefined for empty slots", () => { + expect(getTeamForPick(1, [])).toBeUndefined(); + }); +}); + +describe("getProjectedPicks", () => { + it("returns empty array when no draft slots", () => { + const result = getProjectedPicks({ + draftSlots: [], + userTeamId: "team-1", + currentPick: 1, + totalRounds: 10, + existingPicks: [], + }); + expect(result).toEqual([]); + }); + + it("returns empty array when user has no team in slots", () => { + const result = getProjectedPicks({ + draftSlots: makeSlots(4), + userTeamId: "team-999", + currentPick: 1, + totalRounds: 10, + existingPicks: [], + }); + expect(result).toEqual([]); + }); + + describe("4-team snake draft, user at position 1", () => { + const slots = makeSlots(4); + + it("computes picks from start of draft", () => { + const result = getProjectedPicks({ + draftSlots: slots, + userTeamId: "team-1", + currentPick: 1, + totalRounds: 4, + existingPicks: [], + }); + + expect(result).toEqual([ + { round: 1, pickNumber: 1, picksFromNow: 0 }, + { round: 2, pickNumber: 8, picksFromNow: 7 }, + { round: 3, pickNumber: 9, picksFromNow: 8 }, + { round: 4, pickNumber: 16, picksFromNow: 15 }, + ]); + }); + + it("computes picks from mid-draft, skipping past picks", () => { + const result = getProjectedPicks({ + draftSlots: slots, + userTeamId: "team-1", + currentPick: 5, + totalRounds: 4, + existingPicks: [ + { pickNumber: 1, teamId: "team-1" }, + ], + }); + + expect(result).toEqual([ + { round: 2, pickNumber: 8, picksFromNow: 3 }, + { round: 3, pickNumber: 9, picksFromNow: 4 }, + { round: 4, pickNumber: 16, picksFromNow: 11 }, + ]); + }); + + it("skips rounds already picked even if current pick is before them", () => { + const result = getProjectedPicks({ + draftSlots: slots, + userTeamId: "team-1", + currentPick: 1, + totalRounds: 4, + existingPicks: [ + { pickNumber: 1, teamId: "team-1" }, + { pickNumber: 8, teamId: "team-1" }, + ], + }); + + expect(result).toEqual([ + { round: 3, pickNumber: 9, picksFromNow: 8 }, + { round: 4, pickNumber: 16, picksFromNow: 15 }, + ]); + }); + }); + + describe("4-team snake draft, user at position 3", () => { + const slots = makeSlots(4); + + it("computes correct snake positions", () => { + const result = getProjectedPicks({ + draftSlots: slots, + userTeamId: "team-3", + currentPick: 1, + totalRounds: 4, + existingPicks: [], + }); + + expect(result).toEqual([ + { round: 1, pickNumber: 3, picksFromNow: 2 }, + { round: 2, pickNumber: 6, picksFromNow: 5 }, + { round: 3, pickNumber: 11, picksFromNow: 10 }, + { round: 4, pickNumber: 14, picksFromNow: 13 }, + ]); + }); + }); + + describe("4-team snake draft, user at position 4 (last)", () => { + const slots = makeSlots(4); + + it("picks first in round 2 due to snake reversal", () => { + const result = getProjectedPicks({ + draftSlots: slots, + userTeamId: "team-4", + currentPick: 1, + totalRounds: 4, + existingPicks: [], + }); + + expect(result).toEqual([ + { round: 1, pickNumber: 4, picksFromNow: 3 }, + { round: 2, pickNumber: 5, picksFromNow: 4 }, + { round: 3, pickNumber: 12, picksFromNow: 11 }, + { round: 4, pickNumber: 13, picksFromNow: 12 }, + ]); + }); + }); + + it("returns empty when all picks are in the past", () => { + const result = getProjectedPicks({ + draftSlots: makeSlots(4), + userTeamId: "team-1", + currentPick: 17, + totalRounds: 4, + existingPicks: [ + { pickNumber: 1, teamId: "team-1" }, + { pickNumber: 8, teamId: "team-1" }, + { pickNumber: 9, teamId: "team-1" }, + { pickNumber: 16, teamId: "team-1" }, + ], + }); + expect(result).toEqual([]); + }); + + it("handles 8-team snake draft correctly", () => { + const slots = makeSlots(8); + + const result = getProjectedPicks({ + draftSlots: slots, + userTeamId: "team-5", + currentPick: 1, + totalRounds: 3, + existingPicks: [], + }); + + expect(result).toEqual([ + { round: 1, pickNumber: 5, picksFromNow: 4 }, + { round: 2, pickNumber: 12, picksFromNow: 11 }, + { round: 3, pickNumber: 21, picksFromNow: 20 }, + ]); + }); + + it("excludes picks before currentPick", () => { + const result = getProjectedPicks({ + draftSlots: makeSlots(4), + userTeamId: "team-1", + currentPick: 9, + totalRounds: 4, + existingPicks: [], + }); + + expect(result).toEqual([ + { round: 3, pickNumber: 9, picksFromNow: 0 }, + { round: 4, pickNumber: 16, picksFromNow: 7 }, + ]); + }); +}); diff --git a/app/lib/draft-order.ts b/app/lib/draft-order.ts index e474bca..7503245 100644 --- a/app/lib/draft-order.ts +++ b/app/lib/draft-order.ts @@ -16,3 +16,52 @@ export function getTeamForPick( } return draftSlots.find((slot) => slot.draftOrder === pickInRound); } + +export interface ProjectedPick { + round: number; + pickNumber: number; + picksFromNow: number; +} + +export function getProjectedPicks(options: { + draftSlots: Array<{ draftOrder: number; teamId: string }>; + userTeamId: string; + currentPick: number; + totalRounds: number; + existingPicks: Array<{ pickNumber: number; teamId: string }>; +}): ProjectedPick[] { + const { draftSlots, userTeamId, currentPick, totalRounds, existingPicks } = options; + const totalTeams = draftSlots.length; + if (totalTeams === 0) return []; + + const userSlot = draftSlots.find((slot) => slot.teamId === userTeamId); + if (!userSlot) return []; + + const userDraftOrder = userSlot.draftOrder; + const pickedNumbersForTeam = new Set( + existingPicks + .filter((p) => p.teamId === userTeamId) + .map((p) => p.pickNumber) + ); + + const result: ProjectedPick[] = []; + + for (let round = 1; round <= totalRounds; round++) { + const isEvenRound = round % 2 === 0; + const pickInRound = isEvenRound + ? totalTeams - userDraftOrder + 1 + : userDraftOrder; + const pickNumber = (round - 1) * totalTeams + pickInRound; + + if (pickNumber < currentPick) continue; + if (pickedNumbersForTeam.has(pickNumber)) continue; + + result.push({ + round, + pickNumber, + picksFromNow: pickNumber - currentPick, + }); + } + + return result; +} diff --git a/app/routes/home.tsx b/app/routes/home.tsx index a50dc04..27ed9ca 100644 --- a/app/routes/home.tsx +++ b/app/routes/home.tsx @@ -11,8 +11,9 @@ import { findSeasonById, findCurrentSeasonWithSports } from "~/models/season"; import { findTeamByOwnerAndSeason } from "~/models/team"; import { getDraftedParticipantsBySportsSeason } from "~/models/draft-pick"; import { getUpcomingEventsForDraftedParticipants } from "~/models/scoring-event"; -import { getTeamStanding } from "~/models/standings"; +import { getTeamStanding, getSeasonStandings } from "~/models/standings"; import { getSeasonCompletionPercentage } from "~/lib/season-helpers.server"; +import { buildTiedRankChecker, getDisplayRank } from "~/lib/standings-display"; import { findDraftSlotsBySeasonId } from "~/models/draft-slot"; import { getTeamForPick } from "~/lib/draft-order"; import type { CalendarPanelEvent } from "~/components/sport-season/UpcomingCalendarPanel"; @@ -58,6 +59,7 @@ export async function loader(args: Route.LoaderArgs) { let currentRank: number | undefined; let totalPoints: number | undefined; let previousRank: number | undefined; + let displayRank: string | number | undefined; let completionPercentage = 0; let picksUntilMyTurn: number | undefined; let draftPosition: number | undefined; @@ -101,6 +103,9 @@ export async function loader(args: Route.LoaderArgs) { currentRank = standing.currentRank ?? undefined; totalPoints = standing.totalPoints ?? undefined; previousRank = standing.previousRank ?? undefined; + const allStandings = await getSeasonStandings(league.currentSeasonId); + const isTied = buildTiedRankChecker(allStandings.map((s) => s.currentRank)); + displayRank = getDisplayRank(standing, allStandings.length, isTied(standing.currentRank)); } else if (myTeam && season?.status === "active") { // No standing row yet (no scoring events) — default to rank 1, 0 points currentRank = 1; @@ -139,6 +144,7 @@ export async function loader(args: Route.LoaderArgs) { ...league, currentSeason: season, numSports, + displayRank, currentRank, totalPoints, previousRank, @@ -196,6 +202,7 @@ export default function Home({ loaderData }: Route.ComponentProps) { status: isValidStatus(rawStatus) ? rawStatus : "pre_draft", seasonId: league.currentSeasonId ?? undefined, currentRank: league.currentRank, + displayRank: league.displayRank, totalPoints: league.totalPoints, previousRank: league.previousRank, completionPercentage: league.completionPercentage, diff --git a/app/routes/leagues/$leagueId.draft.$seasonId.tsx b/app/routes/leagues/$leagueId.draft.$seasonId.tsx index dffb407..0171c34 100644 --- a/app/routes/leagues/$leagueId.draft.$seasonId.tsx +++ b/app/routes/leagues/$leagueId.draft.$seasonId.tsx @@ -34,7 +34,7 @@ import { ConnectionOverlay } from "~/components/draft/ConnectionOverlay"; import { AuthRecoveryOverlay } from "~/components/draft/AuthRecoveryOverlay"; import { ParticipantSelectionDialog } from "~/components/draft/ParticipantSelectionDialog"; import { calculateDraftEligibility } from "~/lib/draft-eligibility"; -import { getTeamForPick } from "~/lib/draft-order"; +import { getTeamForPick, getProjectedPicks } from "~/lib/draft-order"; import { useDraftNotifications } from "~/hooks/useDraftNotifications"; import { NotificationSettings } from "~/components/NotificationSettings"; import { AutodraftBadgeWithPopover } from "~/components/AutodraftSettings"; @@ -854,6 +854,20 @@ export default function DraftRoom() { return grid; }, [picks, draftSlots, season.draftRounds]); + const projectedPicks = useMemo(() => { + if (!userTeam || season.status !== "draft" || isDraftComplete) return []; + return getProjectedPicks({ + draftSlots, + userTeamId: userTeam.id, + currentPick, + totalRounds: season.draftRounds, + existingPicks: picks.map((p) => ({ + pickNumber: p.pickNumber, + teamId: p.team.id, + })), + }); + }, [userTeam, season.status, isDraftComplete, draftSlots, currentPick, season.draftRounds, picks]); + // Get drafted participant IDs for filtering const draftedParticipantIds = useMemo( () => new Set(picks.map((p) => p.participant.id)), @@ -971,7 +985,8 @@ export default function DraftRoom() { onMakePick: handleMakePick, onAddToQueue: handleAddToQueue, onRemoveFromQueue: handleRemoveFromQueue, - }), [filteredParticipants, participantRanks, miniDraftGrid, searchQuery, sportFilters, hideDrafted, hideIneligible, uniqueSports, draftedParticipantIds, queue, eligibility, canPick, userTeam, hideCompletedSports, userDraftedSportNames, handleHideCompletedSportsChange, handleMakePick, handleAddToQueue, handleRemoveFromQueue, setSearchQuery, setSportFilters, setHideDrafted, setHideIneligible]); + projectedPicks, + }), [filteredParticipants, participantRanks, miniDraftGrid, searchQuery, sportFilters, hideDrafted, hideIneligible, uniqueSports, draftedParticipantIds, queue, eligibility, canPick, userTeam, hideCompletedSports, userDraftedSportNames, handleHideCompletedSportsChange, handleMakePick, handleAddToQueue, handleRemoveFromQueue, setSearchQuery, setSportFilters, setHideDrafted, setHideIneligible, projectedPicks]); const draftGridSectionProps = { draftSlots,