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: {
|
||||
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);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue