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 {
draftSlots: Array<{
id: string;
draftOrder: number;
team: {
id: string;
name: string;
seasonId: string;
};
}>;
picks: Array<{
@ -26,13 +24,9 @@ interface TeamsDraftedGridProps {
name: string;
};
}>;
availableParticipants: Array<{
sports: Array<{
id: string;
name: string;
sport: {
id: string;
name: string;
};
}>;
season: {
numFlexPicks: number;
@ -42,25 +36,9 @@ interface TeamsDraftedGridProps {
export function TeamsDraftedGrid({
draftSlots,
picks,
availableParticipants,
sports,
season,
}: 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
const picksByTeamAndSport = useMemo(() => {
const map = new Map<string, Map<string, typeof picks>>();
@ -85,17 +63,18 @@ export function TeamsDraftedGrid({
draftSlots.forEach((slot) => {
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);
});
return map;
}, [picks, draftSlots, sports]);
}, [picks, draftSlots]);
if (sports.length === 0) {
return (
<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>
);
}
@ -156,7 +135,7 @@ export function TeamsDraftedGrid({
>
{teamSportPicks.length > 0 ? (
<div className="flex flex-col gap-1">
{teamSportPicks.map((pick) => (
{teamSportPicks.map((pick, i) => (
<div
key={pick.id}
className="text-sm flex items-center gap-1"
@ -167,7 +146,7 @@ export function TeamsDraftedGrid({
variant="secondary"
className="text-xs px-1 py-0"
>
{teamSportPicks.indexOf(pick) + 1}
{i + 1}
</Badge>
)}
</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
// Filtering will be done on the client side based on the "Show Drafted" toggle
let availableParticipants: any[] = [];
if (sportsSeasonIds.length > 0) {
availableParticipants = await db
.select({
id: schema.participants.id,
name: schema.participants.name,
sport: schema.sports,
})
.from(schema.participants)
.innerJoin(
schema.sportsSeasons,
eq(schema.participants.sportsSeasonId, schema.sportsSeasons.id)
)
.innerJoin(
schema.sports,
eq(schema.sportsSeasons.sportId, schema.sports.id)
)
.where(
sportsSeasonIds.length === 1
? eq(schema.participants.sportsSeasonId, sportsSeasonIds[0])
: inArray(schema.participants.sportsSeasonId, sportsSeasonIds)
)
.orderBy(
desc(schema.participants.expectedValue),
asc(schema.participants.name)
);
}
const availableParticipants = sportsSeasonIds.length > 0
? await db
.select({
id: schema.participants.id,
name: schema.participants.name,
sport: schema.sports,
})
.from(schema.participants)
.innerJoin(
schema.sportsSeasons,
eq(schema.participants.sportsSeasonId, schema.sportsSeasons.id)
)
.innerJoin(
schema.sports,
eq(schema.sportsSeasons.sportId, schema.sports.id)
)
.where(
sportsSeasonIds.length === 1
? eq(schema.participants.sportsSeasonId, sportsSeasonIds[0])
: inArray(schema.participants.sportsSeasonId, sportsSeasonIds)
)
.orderBy(
desc(schema.participants.expectedValue),
asc(schema.participants.name)
)
: [];
// Load user's team queue if they have a team
const userQueue = userTeam ? await getTeamQueue(userTeam.id) : [];
@ -305,12 +302,14 @@ export default function DraftRoom() {
const seasonSportsData = useMemo(() => {
const sportsMap = new Map<string, { id: string; name: string }>();
availableParticipants.forEach((p: any) => {
availableParticipants.forEach((p) => {
if (!sportsMap.has(p.sport.id)) {
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]);
// Calculate draft eligibility for current user's team
@ -979,7 +978,7 @@ export default function DraftRoom() {
<TeamsDraftedGrid
draftSlots={draftSlots}
picks={picks}
availableParticipants={availableParticipants}
sports={seasonSportsData}
season={{ numFlexPicks }}
/>
</div>