From edc4eb75e88f11337ae080b399b9c99124fc9cb6 Mon Sep 17 00:00:00 2001 From: Chris Parsons <438676+chrisparsons83@users.noreply.github.com> Date: Fri, 20 Feb 2026 21:10:26 -0800 Subject: [PATCH] Optimize draft room performance with memoization and refactoring (#15) * Fix flex spot count in draft room Teams Drafted view The flexSpots column in the database defaults to 0 and was never populated, causing the Teams Drafted grid to always show "0 of 0 flex". Calculate numFlexPicks dynamically in the loader as max(0, draftRounds - seasonSports.length) so teams see the correct flex count (e.g. 7 rounds - 5 sports = 2 flex). https://claude.ai/code/session_01CSzF4aWuyppGBDqM4MJmhc * Clean up draft room flex calculation area - Fix bug: force pick dialog now uses its own isolated search/sport filter state so it no longer mutates the main participants tab filters - TeamsDraftedGrid: remove dead first pass in flexPicksByTeam memo, trim season prop to numFlexPicks only, drop unused type field from picks sport shape - Loader: derive userAutodraftSettings from already-loaded autodraftSettings list, eliminating a redundant DB query - Extract shared transformedPicks, transformedParticipants, and seasonSportsData into their own useMemos so both eligibility memos reuse them instead of duplicating the work - Memoize draftGrid, draftedParticipantIds, and uniqueSports which were being recomputed on every render https://claude.ai/code/session_01CSzF4aWuyppGBDqM4MJmhc --------- Co-authored-by: Claude --- app/components/draft/TeamsDraftedGrid.tsx | 23 +-- .../leagues/$leagueId.draft.$seasonId.tsx | 171 ++++++++---------- 2 files changed, 76 insertions(+), 118 deletions(-) diff --git a/app/components/draft/TeamsDraftedGrid.tsx b/app/components/draft/TeamsDraftedGrid.tsx index 7bfb4a5..3781403 100644 --- a/app/components/draft/TeamsDraftedGrid.tsx +++ b/app/components/draft/TeamsDraftedGrid.tsx @@ -24,7 +24,6 @@ interface TeamsDraftedGridProps { sport: { id: string; name: string; - type: string; }; }>; availableParticipants: Array<{ @@ -36,14 +35,7 @@ interface TeamsDraftedGridProps { }; }>; season: { - id: string; - name: string; - year: number; numFlexPicks: number; - sports?: Array<{ - id: string; - name: string; - }>; }; } @@ -91,22 +83,9 @@ export function TeamsDraftedGrid({ const flexPicksByTeam = useMemo(() => { const map = new Map(); - picks.forEach((pick) => { - // Flex picks are counted based on sport type or a different logic - // For now, we'll just count total picks and subtract sport-specific limits - // This is a simplified version - adjust based on your actual flex logic - if (!map.has(pick.team.id)) { - map.set(pick.team.id, 0); - } - }); - - // Calculate flex usage based on season settings draftSlots.forEach((slot) => { const teamPicks = picks.filter((p) => p.team.id === slot.team.id); - const totalPicks = teamPicks.length; - // This is simplified - you may need to implement actual flex calculation - // based on sport-specific limits in your season settings - const flexUsed = Math.max(0, totalPicks - sports.length); + const flexUsed = Math.max(0, teamPicks.length - sports.length); map.set(slot.team.id, flexUsed); }); diff --git a/app/routes/leagues/$leagueId.draft.$seasonId.tsx b/app/routes/leagues/$leagueId.draft.$seasonId.tsx index a144379..480c5b5 100644 --- a/app/routes/leagues/$leagueId.draft.$seasonId.tsx +++ b/app/routes/leagues/$leagueId.draft.$seasonId.tsx @@ -162,14 +162,9 @@ export async function loader(args: any) { where: eq(schema.autodraftSettings.seasonId, seasonId), }); - // Load user's autodraft settings if they have a team + // Derive user's autodraft settings from the already-loaded list const userAutodraftSettings = userTeam - ? await db.query.autodraftSettings.findFirst({ - where: and( - eq(schema.autodraftSettings.seasonId, seasonId), - eq(schema.autodraftSettings.teamId, userTeam.id) - ), - }) + ? (autodraftSettings.find((s) => s.teamId === userTeam.id) ?? null) : null; return { @@ -184,6 +179,7 @@ export async function loader(args: any) { userAutodraftSettings, isCommissioner: !!isCommissioner, currentUserId: userId, + numFlexPicks: Math.max(0, season.draftRounds - seasonSports.length), }; } @@ -200,6 +196,7 @@ export default function DraftRoom() { userAutodraftSettings, isCommissioner, currentUserId, + numFlexPicks, } = useLoaderData(); const { isConnected, connectionError, isReconnecting, on, off } = useDraftSocket(season.id, userTeam?.id); const { @@ -281,45 +278,47 @@ export default function DraftRoom() { pickNumber: number; teamId: string; } | null>(null); + const [dialogSearchQuery, setDialogSearchQuery] = useState(""); + const [dialogSportFilter, setDialogSportFilter] = useState("all"); - // Calculate draft eligibility for current user's team - const eligibility = useMemo(() => { - if (!userTeam) return null; - - // Transform picks data to match eligibility function signature - const transformedPicks = picks.map((p: any) => ({ - teamId: p.team.id, - participant: { - id: p.participant.id, - sport: { - id: p.sport.id, - name: p.sport.name, + // Shared transforms for eligibility calculations + const transformedPicks = useMemo( + () => + picks.map((p: any) => ({ + teamId: p.team.id, + participant: { + id: p.participant.id, + sport: { id: p.sport.id, name: p.sport.name }, }, - }, - })); + })), + [picks] + ); - const userTeamPicks = transformedPicks.filter( - (p: any) => p.teamId === userTeam.id - ); + const transformedParticipants = useMemo( + () => + availableParticipants.map((p: any) => ({ + id: p.id, + sport: { id: p.sport.id, name: p.sport.name }, + })), + [availableParticipants] + ); - // Transform participants to match signature - const transformedParticipants = availableParticipants.map((p: any) => ({ - id: p.id, - sport: { - id: p.sport.id, - name: p.sport.name, - }, - })); - - // Get unique sports from availableParticipants - const sportsMap = new Map(); + const seasonSportsData = useMemo(() => { + const sportsMap = new Map(); availableParticipants.forEach((p: any) => { if (!sportsMap.has(p.sport.id)) { sportsMap.set(p.sport.id, { id: p.sport.id, name: p.sport.name }); } }); - const seasonSportsData = Array.from(sportsMap.values()); + return Array.from(sportsMap.values()); + }, [availableParticipants]); + // Calculate draft eligibility for current user's team + const eligibility = useMemo(() => { + if (!userTeam) return null; + const userTeamPicks = transformedPicks.filter( + (p: any) => p.teamId === userTeam.id + ); return calculateDraftEligibility( userTeam.id, userTeamPicks, @@ -329,43 +328,14 @@ export default function DraftRoom() { season.draftRounds, season.teams ); - }, [userTeam, picks, availableParticipants, season]); + }, [userTeam, transformedPicks, transformedParticipants, seasonSportsData, season]); // Calculate eligibility for force manual pick dialog (selected team) const forcePickEligibility = useMemo(() => { if (!selectedPickSlot) return null; - - const transformedPicks = picks.map((p: any) => ({ - teamId: p.team.id, - participant: { - id: p.participant.id, - sport: { - id: p.sport.id, - name: p.sport.name, - }, - }, - })); - const selectedTeamPicks = transformedPicks.filter( (p: any) => p.teamId === selectedPickSlot.teamId ); - - const transformedParticipants = availableParticipants.map((p: any) => ({ - id: p.id, - sport: { - id: p.sport.id, - name: p.sport.name, - }, - })); - - const sportsMap = new Map(); - availableParticipants.forEach((p: any) => { - if (!sportsMap.has(p.sport.id)) { - sportsMap.set(p.sport.id, { id: p.sport.id, name: p.sport.name }); - } - }); - const seasonSportsData = Array.from(sportsMap.values()); - return calculateDraftEligibility( selectedPickSlot.teamId, selectedTeamPicks, @@ -375,7 +345,7 @@ export default function DraftRoom() { season.draftRounds, season.teams ); - }, [selectedPickSlot, picks, availableParticipants, season]); + }, [selectedPickSlot, transformedPicks, transformedParticipants, seasonSportsData, season]); // Listen for new picks from other users useEffect(() => { @@ -711,7 +681,7 @@ export default function DraftRoom() { const canPick = isMyTurn && season.status === "draft"; // Only team owner on their turn // Generate snake draft grid structure - const generateDraftGrid = () => { + const draftGrid = useMemo(() => { const teamCount = draftSlots.length; const rounds = season.draftRounds; const grid: Array< @@ -734,34 +704,47 @@ export default function DraftRoom() { const pickNumber = (round - 1) * teamCount + pickInRound; const teamId = draftSlots[teamIndex]?.team.id; - // Find if this pick has been made const pick = picks.find((p: any) => p.pickNumber === pickNumber); - roundPicks.push({ - pickNumber, - round, - pickInRound, - teamId, - pick, - }); + roundPicks.push({ pickNumber, round, pickInRound, teamId, pick }); } grid.push(roundPicks); } return grid; - }; - - const draftGrid = generateDraftGrid(); + }, [picks, draftSlots, season.draftRounds]); // Get drafted participant IDs for filtering - const draftedParticipantIds = new Set( - picks.map((p: any) => p.participant.id) + const draftedParticipantIds = useMemo( + () => new Set(picks.map((p: any) => p.participant.id)), + [picks] ); // Get unique sports for filter dropdown - const uniqueSports = Array.from( - new Set(availableParticipants.map((p: any) => p.sport.name)) - ).sort(); + const uniqueSports = useMemo( + () => + Array.from( + new Set(availableParticipants.map((p: any) => p.sport.name)) + ).sort(), + [availableParticipants] + ); + + // Participants filtered for the force pick dialog (always excludes drafted, uses dialog-local filters) + const dialogFilteredParticipants = useMemo( + () => + availableParticipants.filter((p: any) => { + if (draftedParticipantIds.has(p.id)) return false; + if ( + dialogSearchQuery && + !p.name.toLowerCase().includes(dialogSearchQuery.toLowerCase()) + ) + return false; + if (dialogSportFilter !== "all" && p.sport.name !== dialogSportFilter) + return false; + return true; + }), + [availableParticipants, draftedParticipantIds, dialogSearchQuery, dialogSportFilter] + ); // Filter participants based on search, sport, drafted status, and eligibility const filteredParticipants = availableParticipants.filter( @@ -984,6 +967,8 @@ export default function DraftRoom() { onForceAutopick={handleForceAutopick} onForceManualPickOpen={(pickNumber, teamId) => { setSelectedPickSlot({ pickNumber, teamId }); + setDialogSearchQuery(""); + setDialogSportFilter("all"); setForcePickDialogOpen(true); }} /> @@ -995,12 +980,7 @@ export default function DraftRoom() { draftSlots={draftSlots} picks={picks} availableParticipants={availableParticipants} - season={{ - id: season.id, - name: `${season.year} Season`, - year: season.year, - numFlexPicks: season.flexSpots, - }} + season={{ numFlexPicks }} /> @@ -1031,13 +1011,13 @@ export default function DraftRoom() { type="text" placeholder="Search participants..." className="flex-1 px-3 py-2 border rounded-md" - value={searchQuery} - onChange={(e) => setSearchQuery(e.target.value)} + value={dialogSearchQuery} + onChange={(e) => setDialogSearchQuery(e.target.value)} />