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 <noreply@anthropic.com>
This commit is contained in:
Chris Parsons 2026-02-20 21:10:26 -08:00 committed by GitHub
parent 9ae00a8385
commit edc4eb75e8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 76 additions and 118 deletions

View file

@ -24,7 +24,6 @@ interface TeamsDraftedGridProps {
sport: { sport: {
id: string; id: string;
name: string; name: string;
type: string;
}; };
}>; }>;
availableParticipants: Array<{ availableParticipants: Array<{
@ -36,14 +35,7 @@ interface TeamsDraftedGridProps {
}; };
}>; }>;
season: { season: {
id: string;
name: string;
year: number;
numFlexPicks: number; numFlexPicks: number;
sports?: Array<{
id: string;
name: string;
}>;
}; };
} }
@ -91,22 +83,9 @@ export function TeamsDraftedGrid({
const flexPicksByTeam = useMemo(() => { const flexPicksByTeam = useMemo(() => {
const map = new Map<string, number>(); const map = new Map<string, number>();
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) => { draftSlots.forEach((slot) => {
const teamPicks = picks.filter((p) => p.team.id === slot.team.id); const teamPicks = picks.filter((p) => p.team.id === slot.team.id);
const totalPicks = teamPicks.length; const flexUsed = Math.max(0, teamPicks.length - sports.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);
map.set(slot.team.id, flexUsed); map.set(slot.team.id, flexUsed);
}); });

View file

@ -162,14 +162,9 @@ export async function loader(args: any) {
where: eq(schema.autodraftSettings.seasonId, seasonId), 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 const userAutodraftSettings = userTeam
? await db.query.autodraftSettings.findFirst({ ? (autodraftSettings.find((s) => s.teamId === userTeam.id) ?? null)
where: and(
eq(schema.autodraftSettings.seasonId, seasonId),
eq(schema.autodraftSettings.teamId, userTeam.id)
),
})
: null; : null;
return { return {
@ -184,6 +179,7 @@ export async function loader(args: any) {
userAutodraftSettings, userAutodraftSettings,
isCommissioner: !!isCommissioner, isCommissioner: !!isCommissioner,
currentUserId: userId, currentUserId: userId,
numFlexPicks: Math.max(0, season.draftRounds - seasonSports.length),
}; };
} }
@ -200,6 +196,7 @@ export default function DraftRoom() {
userAutodraftSettings, userAutodraftSettings,
isCommissioner, isCommissioner,
currentUserId, currentUserId,
numFlexPicks,
} = useLoaderData<typeof loader>(); } = useLoaderData<typeof loader>();
const { isConnected, connectionError, isReconnecting, on, off } = useDraftSocket(season.id, userTeam?.id); const { isConnected, connectionError, isReconnecting, on, off } = useDraftSocket(season.id, userTeam?.id);
const { const {
@ -281,45 +278,47 @@ export default function DraftRoom() {
pickNumber: number; pickNumber: number;
teamId: string; teamId: string;
} | null>(null); } | null>(null);
const [dialogSearchQuery, setDialogSearchQuery] = useState("");
const [dialogSportFilter, setDialogSportFilter] = useState<string>("all");
// Calculate draft eligibility for current user's team // Shared transforms for eligibility calculations
const eligibility = useMemo(() => { const transformedPicks = useMemo(
if (!userTeam) return null; () =>
picks.map((p: any) => ({
// Transform picks data to match eligibility function signature teamId: p.team.id,
const transformedPicks = picks.map((p: any) => ({ participant: {
teamId: p.team.id, id: p.participant.id,
participant: { sport: { id: p.sport.id, name: p.sport.name },
id: p.participant.id,
sport: {
id: p.sport.id,
name: p.sport.name,
}, },
}, })),
})); [picks]
);
const userTeamPicks = transformedPicks.filter( const transformedParticipants = useMemo(
(p: any) => p.teamId === userTeam.id () =>
); availableParticipants.map((p: any) => ({
id: p.id,
sport: { id: p.sport.id, name: p.sport.name },
})),
[availableParticipants]
);
// Transform participants to match signature const seasonSportsData = useMemo(() => {
const transformedParticipants = availableParticipants.map((p: any) => ({ const sportsMap = new Map<string, { id: string; name: string }>();
id: p.id,
sport: {
id: p.sport.id,
name: p.sport.name,
},
}));
// Get unique sports from availableParticipants
const sportsMap = new Map();
availableParticipants.forEach((p: any) => { availableParticipants.forEach((p: any) => {
if (!sportsMap.has(p.sport.id)) { if (!sportsMap.has(p.sport.id)) {
sportsMap.set(p.sport.id, { id: p.sport.id, name: p.sport.name }); 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( return calculateDraftEligibility(
userTeam.id, userTeam.id,
userTeamPicks, userTeamPicks,
@ -329,43 +328,14 @@ export default function DraftRoom() {
season.draftRounds, season.draftRounds,
season.teams season.teams
); );
}, [userTeam, picks, availableParticipants, season]); }, [userTeam, transformedPicks, transformedParticipants, seasonSportsData, season]);
// Calculate eligibility for force manual pick dialog (selected team) // Calculate eligibility for force manual pick dialog (selected team)
const forcePickEligibility = useMemo(() => { const forcePickEligibility = useMemo(() => {
if (!selectedPickSlot) return null; 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( const selectedTeamPicks = transformedPicks.filter(
(p: any) => p.teamId === selectedPickSlot.teamId (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( return calculateDraftEligibility(
selectedPickSlot.teamId, selectedPickSlot.teamId,
selectedTeamPicks, selectedTeamPicks,
@ -375,7 +345,7 @@ export default function DraftRoom() {
season.draftRounds, season.draftRounds,
season.teams season.teams
); );
}, [selectedPickSlot, picks, availableParticipants, season]); }, [selectedPickSlot, transformedPicks, transformedParticipants, seasonSportsData, season]);
// Listen for new picks from other users // Listen for new picks from other users
useEffect(() => { useEffect(() => {
@ -711,7 +681,7 @@ export default function DraftRoom() {
const canPick = isMyTurn && season.status === "draft"; // Only team owner on their turn const canPick = isMyTurn && season.status === "draft"; // Only team owner on their turn
// Generate snake draft grid structure // Generate snake draft grid structure
const generateDraftGrid = () => { const draftGrid = useMemo(() => {
const teamCount = draftSlots.length; const teamCount = draftSlots.length;
const rounds = season.draftRounds; const rounds = season.draftRounds;
const grid: Array< const grid: Array<
@ -734,34 +704,47 @@ export default function DraftRoom() {
const pickNumber = (round - 1) * teamCount + pickInRound; const pickNumber = (round - 1) * teamCount + pickInRound;
const teamId = draftSlots[teamIndex]?.team.id; const teamId = draftSlots[teamIndex]?.team.id;
// Find if this pick has been made
const pick = picks.find((p: any) => p.pickNumber === pickNumber); const pick = picks.find((p: any) => p.pickNumber === pickNumber);
roundPicks.push({ roundPicks.push({ pickNumber, round, pickInRound, teamId, pick });
pickNumber,
round,
pickInRound,
teamId,
pick,
});
} }
grid.push(roundPicks); grid.push(roundPicks);
} }
return grid; return grid;
}; }, [picks, draftSlots, season.draftRounds]);
const draftGrid = generateDraftGrid();
// Get drafted participant IDs for filtering // Get drafted participant IDs for filtering
const draftedParticipantIds = new Set( const draftedParticipantIds = useMemo(
picks.map((p: any) => p.participant.id) () => new Set(picks.map((p: any) => p.participant.id)),
[picks]
); );
// Get unique sports for filter dropdown // Get unique sports for filter dropdown
const uniqueSports = Array.from( const uniqueSports = useMemo(
new Set(availableParticipants.map((p: any) => p.sport.name)) () =>
).sort(); 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 // Filter participants based on search, sport, drafted status, and eligibility
const filteredParticipants = availableParticipants.filter( const filteredParticipants = availableParticipants.filter(
@ -984,6 +967,8 @@ export default function DraftRoom() {
onForceAutopick={handleForceAutopick} onForceAutopick={handleForceAutopick}
onForceManualPickOpen={(pickNumber, teamId) => { onForceManualPickOpen={(pickNumber, teamId) => {
setSelectedPickSlot({ pickNumber, teamId }); setSelectedPickSlot({ pickNumber, teamId });
setDialogSearchQuery("");
setDialogSportFilter("all");
setForcePickDialogOpen(true); setForcePickDialogOpen(true);
}} }}
/> />
@ -995,12 +980,7 @@ export default function DraftRoom() {
draftSlots={draftSlots} draftSlots={draftSlots}
picks={picks} picks={picks}
availableParticipants={availableParticipants} availableParticipants={availableParticipants}
season={{ season={{ numFlexPicks }}
id: season.id,
name: `${season.year} Season`,
year: season.year,
numFlexPicks: season.flexSpots,
}}
/> />
</div> </div>
</TabsContent> </TabsContent>
@ -1031,13 +1011,13 @@ export default function DraftRoom() {
type="text" type="text"
placeholder="Search participants..." placeholder="Search participants..."
className="flex-1 px-3 py-2 border rounded-md" className="flex-1 px-3 py-2 border rounded-md"
value={searchQuery} value={dialogSearchQuery}
onChange={(e) => setSearchQuery(e.target.value)} onChange={(e) => setDialogSearchQuery(e.target.value)}
/> />
<select <select
className="px-3 py-2 border rounded-md" className="px-3 py-2 border rounded-md"
value={sportFilter} value={dialogSportFilter}
onChange={(e) => setSportFilter(e.target.value)} onChange={(e) => setDialogSportFilter(e.target.value)}
> >
<option value="all">All Sports</option> <option value="all">All Sports</option>
{uniqueSports.map((sport) => ( {uniqueSports.map((sport) => (
@ -1061,8 +1041,7 @@ export default function DraftRoom() {
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{filteredParticipants {dialogFilteredParticipants
.filter((p: any) => !draftedParticipantIds.has(p.id))
.map((participant: any) => { .map((participant: any) => {
// Check eligibility for force pick team // Check eligibility for force pick team
const isEligible = forcePickEligibility const isEligible = forcePickEligibility