Refactor draft grid sports calculation and improve type safety (#16)

* Fix flex pick count using unique sports drafted, not total season sports

The flex count was calculated as `totalPicks - totalSportsInSeason`, which
always returned 0 when a team drafted multiple participants from the same
sport (e.g. 2 picks from UEFA Champions League with 2 sports in season =
2 - 2 = 0). Now uses the number of unique sports the team has actually
drafted from, so 2 picks in 1 sport correctly shows 1 flex used.

https://claude.ai/code/session_01DZ6jqgZTDEmJucanBtRCtM

* Code review fixes for TeamsDraftedGrid and draft route

- Fix misleading early-return message: "No picks have been made yet" only
  fires when there are no sports configured, so update to say "No sports
  have been configured for this season."
- Remove unused draftOrder and team.seasonId from TeamsDraftedGrid props
- Replace availableParticipants prop with sports prop to eliminate
  duplicate sports derivation; route already computes seasonSportsData
- Add alphabetical sort to seasonSportsData so ordering is consistent
- Fix availableParticipants: any[] in loader — restructure to ternary so
  TypeScript infers the Drizzle select type directly
- Remove any annotation from seasonSportsData forEach callback (now typed)
- Fix teamSportPicks.indexOf(pick) O(n) re-scan — use map callback index i

https://claude.ai/code/session_01DZ6jqgZTDEmJucanBtRCtM

---------

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Chris Parsons 2026-02-20 21:26:27 -08:00 committed by GitHub
parent edc4eb75e8
commit 7294084c13
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 39 additions and 61 deletions

View file

@ -4,11 +4,9 @@ import { useMemo } from "react";
interface TeamsDraftedGridProps { interface TeamsDraftedGridProps {
draftSlots: Array<{ draftSlots: Array<{
id: string; id: string;
draftOrder: number;
team: { team: {
id: string; id: string;
name: string; name: string;
seasonId: string;
}; };
}>; }>;
picks: Array<{ picks: Array<{
@ -26,13 +24,9 @@ interface TeamsDraftedGridProps {
name: string; name: string;
}; };
}>; }>;
availableParticipants: Array<{ sports: Array<{
id: string; id: string;
name: string; name: string;
sport: {
id: string;
name: string;
};
}>; }>;
season: { season: {
numFlexPicks: number; numFlexPicks: number;
@ -42,25 +36,9 @@ interface TeamsDraftedGridProps {
export function TeamsDraftedGrid({ export function TeamsDraftedGrid({
draftSlots, draftSlots,
picks, picks,
availableParticipants, sports,
season, season,
}: TeamsDraftedGridProps) { }: TeamsDraftedGridProps) {
// Get unique sports from all available participants (not just picked ones)
const sports = useMemo(() => {
const sportMap = new Map<string, { id: string; name: string }>();
availableParticipants.forEach((participant) => {
if (!sportMap.has(participant.sport.id)) {
sportMap.set(participant.sport.id, {
id: participant.sport.id,
name: participant.sport.name,
});
}
});
return Array.from(sportMap.values()).sort((a, b) =>
a.name.localeCompare(b.name)
);
}, [availableParticipants]);
// Calculate picks by team and sport // Calculate picks by team and sport
const picksByTeamAndSport = useMemo(() => { const picksByTeamAndSport = useMemo(() => {
const map = new Map<string, Map<string, typeof picks>>(); const map = new Map<string, Map<string, typeof picks>>();
@ -85,17 +63,18 @@ export function TeamsDraftedGrid({
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 flexUsed = Math.max(0, teamPicks.length - sports.length); const uniqueSportsPicked = new Set(teamPicks.map((p) => p.sport.id)).size;
const flexUsed = Math.max(0, teamPicks.length - uniqueSportsPicked);
map.set(slot.team.id, flexUsed); map.set(slot.team.id, flexUsed);
}); });
return map; return map;
}, [picks, draftSlots, sports]); }, [picks, draftSlots]);
if (sports.length === 0) { if (sports.length === 0) {
return ( return (
<div className="flex items-center justify-center h-full p-8 text-muted-foreground"> <div className="flex items-center justify-center h-full p-8 text-muted-foreground">
<p>No picks have been made yet</p> <p>No sports have been configured for this season.</p>
</div> </div>
); );
} }
@ -156,7 +135,7 @@ export function TeamsDraftedGrid({
> >
{teamSportPicks.length > 0 ? ( {teamSportPicks.length > 0 ? (
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
{teamSportPicks.map((pick) => ( {teamSportPicks.map((pick, i) => (
<div <div
key={pick.id} key={pick.id}
className="text-sm flex items-center gap-1" className="text-sm flex items-center gap-1"
@ -167,7 +146,7 @@ export function TeamsDraftedGrid({
variant="secondary" variant="secondary"
className="text-xs px-1 py-0" className="text-xs px-1 py-0"
> >
{teamSportPicks.indexOf(pick) + 1} {i + 1}
</Badge> </Badge>
)} )}
</div> </div>

View file

@ -119,35 +119,32 @@ export async function loader(args: any) {
// Get ALL participants (both drafted and undrafted) - sorted by EV desc, then name // Get ALL participants (both drafted and undrafted) - sorted by EV desc, then name
// Filtering will be done on the client side based on the "Show Drafted" toggle // Filtering will be done on the client side based on the "Show Drafted" toggle
let availableParticipants: any[] = []; const availableParticipants = sportsSeasonIds.length > 0
? await db
if (sportsSeasonIds.length > 0) { .select({
availableParticipants = await db id: schema.participants.id,
.select({ name: schema.participants.name,
id: schema.participants.id, sport: schema.sports,
name: schema.participants.name, })
sport: schema.sports, .from(schema.participants)
}) .innerJoin(
.from(schema.participants) schema.sportsSeasons,
.innerJoin( eq(schema.participants.sportsSeasonId, schema.sportsSeasons.id)
schema.sportsSeasons, )
eq(schema.participants.sportsSeasonId, schema.sportsSeasons.id) .innerJoin(
) schema.sports,
.innerJoin( eq(schema.sportsSeasons.sportId, schema.sports.id)
schema.sports, )
eq(schema.sportsSeasons.sportId, schema.sports.id) .where(
) sportsSeasonIds.length === 1
.where( ? eq(schema.participants.sportsSeasonId, sportsSeasonIds[0])
sportsSeasonIds.length === 1 : inArray(schema.participants.sportsSeasonId, sportsSeasonIds)
? eq(schema.participants.sportsSeasonId, sportsSeasonIds[0]) )
: inArray(schema.participants.sportsSeasonId, sportsSeasonIds) .orderBy(
) desc(schema.participants.expectedValue),
.orderBy( asc(schema.participants.name)
desc(schema.participants.expectedValue), )
asc(schema.participants.name) : [];
);
}
// Load user's team queue if they have a team // Load user's team queue if they have a team
const userQueue = userTeam ? await getTeamQueue(userTeam.id) : []; const userQueue = userTeam ? await getTeamQueue(userTeam.id) : [];
@ -305,12 +302,14 @@ export default function DraftRoom() {
const seasonSportsData = useMemo(() => { const seasonSportsData = useMemo(() => {
const sportsMap = new Map<string, { id: string; name: string }>(); const sportsMap = new Map<string, { id: string; name: string }>();
availableParticipants.forEach((p: any) => { availableParticipants.forEach((p) => {
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 });
} }
}); });
return Array.from(sportsMap.values()); return Array.from(sportsMap.values()).sort((a, b) =>
a.name.localeCompare(b.name)
);
}, [availableParticipants]); }, [availableParticipants]);
// Calculate draft eligibility for current user's team // Calculate draft eligibility for current user's team
@ -979,7 +978,7 @@ export default function DraftRoom() {
<TeamsDraftedGrid <TeamsDraftedGrid
draftSlots={draftSlots} draftSlots={draftSlots}
picks={picks} picks={picks}
availableParticipants={availableParticipants} sports={seasonSportsData}
season={{ numFlexPicks }} season={{ numFlexPicks }}
/> />
</div> </div>