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:
parent
9ae00a8385
commit
edc4eb75e8
2 changed files with 76 additions and 118 deletions
|
|
@ -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);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue