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: {
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<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) => {
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);
});

View file

@ -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<typeof loader>();
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<string>("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<string, { id: string; name: string }>();
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 }}
/>
</div>
</TabsContent>
@ -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)}
/>
<select
className="px-3 py-2 border rounded-md"
value={sportFilter}
onChange={(e) => setSportFilter(e.target.value)}
value={dialogSportFilter}
onChange={(e) => setDialogSportFilter(e.target.value)}
>
<option value="all">All Sports</option>
{uniqueSports.map((sport) => (
@ -1061,8 +1041,7 @@ export default function DraftRoom() {
</tr>
</thead>
<tbody>
{filteredParticipants
.filter((p: any) => !draftedParticipantIds.has(p.id))
{dialogFilteredParticipants
.map((participant: any) => {
// Check eligibility for force pick team
const isEligible = forcePickEligibility