Show why a participant is ineligible to draft (#378)

Closes #72

Upgrades ineligibleReasons from Record<string, string> to a structured
{code, message} type so the UI can render the human-readable reason
inline below the Ineligible badge in both AvailableParticipantsSection
and ParticipantSelectionDialog. API routes surface the message in 400
responses unchanged.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Chris Parsons 2026-05-04 21:54:14 -07:00 committed by GitHub
parent 5b03b22267
commit 049ec8a596
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 214 additions and 40 deletions

View file

@ -2,6 +2,7 @@ import { describe, it, expect, vi, beforeEach } from "vitest";
import { render, screen, within } from "@testing-library/react"; import { render, screen, within } from "@testing-library/react";
import userEvent from "@testing-library/user-event"; import userEvent from "@testing-library/user-event";
import { AvailableParticipantsSection } from "~/components/draft/AvailableParticipantsSection"; import { AvailableParticipantsSection } from "~/components/draft/AvailableParticipantsSection";
import type { DraftIneligibilityReason } from "~/lib/draft-eligibility";
vi.mock("@tanstack/react-virtual", () => ({ vi.mock("@tanstack/react-virtual", () => ({
useVirtualizer: ({ useVirtualizer: ({
@ -67,6 +68,13 @@ const defaultProps = {
// (CSS breakpoints are not applied). Click the first one to open the Sheet. // (CSS breakpoints are not applied). Click the first one to open the Sheet.
let user: ReturnType<typeof userEvent.setup>; let user: ReturnType<typeof userEvent.setup>;
function createReason(
code: DraftIneligibilityReason["code"],
message: string
): DraftIneligibilityReason {
return { code, message };
}
function clickFirstSportFilterTrigger(name: string | RegExp) { function clickFirstSportFilterTrigger(name: string | RegExp) {
return user.click(screen.getAllByRole("button", { name })[0]); return user.click(screen.getAllByRole("button", { name })[0]);
} }
@ -506,4 +514,43 @@ describe("AvailableParticipantsSection", () => {
expect(onShowOnlyWatchedChange).toHaveBeenCalledWith(true); expect(onShowOnlyWatchedChange).toHaveBeenCalledWith(true);
}); });
}); });
describe("Ineligible reasons", () => {
it("renders inline out-of-flex-spots copy for ineligible participants", () => {
render(
<AvailableParticipantsSection
{...defaultProps}
hasTeam={true}
canPick={true}
eligibility={{
eligibleSportIds: new Set(["s2"]),
ineligibleReasons: {
s1: createReason("no_flex_spots", "Out of flex spots (2/2)"),
},
}}
/>
);
expect(screen.getAllByText("Out of flex spots (2/2)").length).toBeGreaterThan(0);
expect(screen.getAllByTitle("Out of flex spots (2/2)").length).toBeGreaterThan(0);
});
it("renders inline no-extras copy for ineligible participants", () => {
render(
<AvailableParticipantsSection
{...defaultProps}
hasTeam={true}
canPick={true}
eligibility={{
eligibleSportIds: new Set(["s2"]),
ineligibleReasons: {
s1: createReason("no_extra_participants", "Not enough NFL participants for remaining teams"),
},
}}
/>
);
expect(screen.getAllByText("Not enough NFL participants for remaining teams").length).toBeGreaterThan(0);
});
});
}); });

View file

@ -0,0 +1,59 @@
import { describe, it, expect, vi } from "vitest";
import { render, screen } from "@testing-library/react";
import { ParticipantSelectionDialog } from "~/components/draft/ParticipantSelectionDialog";
const defaultProps = {
open: true,
onOpenChange: vi.fn(),
title: "Force pick",
description: "Choose a participant",
participants: [
{ id: "1", name: "Player A", sport: { id: "s1", name: "NFL" } },
{ id: "2", name: "Player B", sport: { id: "s2", name: "NBA" } },
],
draftedParticipantIds: new Set<string>(),
eligibility: null,
uniqueSports: ["NFL", "NBA"],
onSelect: vi.fn(),
};
describe("ParticipantSelectionDialog", () => {
it("renders inline out-of-flex-spots reason and disables drafting", () => {
render(
<ParticipantSelectionDialog
{...defaultProps}
eligibility={{
eligibleSportIds: new Set(["s2"]),
ineligibleReasons: {
s1: {
code: "no_flex_spots",
message: "Out of flex spots (2/2)",
},
},
}}
/>
);
expect(screen.getByText("Out of flex spots (2/2)")).toBeInTheDocument();
expect(screen.getAllByRole("button", { name: "Draft" })[0]).toBeDisabled();
});
it("renders inline no-extras reason for blocked flex picks", () => {
render(
<ParticipantSelectionDialog
{...defaultProps}
eligibility={{
eligibleSportIds: new Set(["s2"]),
ineligibleReasons: {
s1: {
code: "no_extra_participants",
message: "Not enough NFL participants for remaining teams",
},
},
}}
/>
);
expect(screen.getByText("Not enough NFL participants for remaining teams")).toBeInTheDocument();
});
});

View file

@ -4,6 +4,7 @@ import { Button } from "~/components/ui/button";
import { Badge } from "~/components/ui/badge"; import { Badge } from "~/components/ui/badge";
import { Checkbox } from "~/components/ui/checkbox"; import { Checkbox } from "~/components/ui/checkbox";
import { MiniDraftGrid, type MiniDraftGridProps } from "~/components/draft/MiniDraftGrid"; import { MiniDraftGrid, type MiniDraftGridProps } from "~/components/draft/MiniDraftGrid";
import type { DraftIneligibilityReason } from "~/lib/draft-eligibility";
import { import {
Popover, Popover,
PopoverTrigger, PopoverTrigger,
@ -29,7 +30,7 @@ function getParticipantState(
queueMap: Map<string, string>, queueMap: Map<string, string>,
eligibility: { eligibility: {
eligibleSportIds: Set<string>; eligibleSportIds: Set<string>;
ineligibleReasons: Record<string, string>; ineligibleReasons: Record<string, DraftIneligibilityReason>;
} | null, } | null,
watchedParticipantIds: Set<string> watchedParticipantIds: Set<string>
) { ) {
@ -38,7 +39,7 @@ function getParticipantState(
const isEligible = eligibility const isEligible = eligibility
? eligibility.eligibleSportIds.has(participant.sport.id) ? eligibility.eligibleSportIds.has(participant.sport.id)
: true; : true;
const ineligibleReason = eligibility?.ineligibleReasons[participant.sport.id]; const ineligibleReason = eligibility?.ineligibleReasons[participant.sport.id] ?? null;
const isWatched = watchedParticipantIds.has(participant.id); const isWatched = watchedParticipantIds.has(participant.id);
return { isDrafted, isInQueue, isEligible, ineligibleReason, isWatched }; return { isDrafted, isInQueue, isEligible, ineligibleReason, isWatched };
} }
@ -151,7 +152,7 @@ interface AvailableParticipantsSectionProps {
queue: Array<{ id: string; participantId: string }>; queue: Array<{ id: string; participantId: string }>;
eligibility: { eligibility: {
eligibleSportIds: Set<string>; eligibleSportIds: Set<string>;
ineligibleReasons: Record<string, string>; ineligibleReasons: Record<string, DraftIneligibilityReason>;
} | null; } | null;
canPick: boolean; canPick: boolean;
hasTeam: boolean; hasTeam: boolean;
@ -547,11 +548,16 @@ export const AvailableParticipantsSection = memo(function AvailableParticipantsS
<Badge <Badge
variant="destructive" variant="destructive"
className="text-xs" className="text-xs"
title={ineligibleReason} title={ineligibleReason?.message}
> >
Ineligible Ineligible
</Badge> </Badge>
)} )}
{!isDrafted && !isEligible && ineligibleReason && (
<p className="text-xs text-destructive/90">
{ineligibleReason.message}
</p>
)}
{isInQueue && !isDrafted && isEligible && ( {isInQueue && !isDrafted && isEligible && (
<Badge variant="default" className="text-xs"> <Badge variant="default" className="text-xs">
Queued Queued
@ -578,7 +584,7 @@ export const AvailableParticipantsSection = memo(function AvailableParticipantsS
size="sm" size="sm"
className="min-h-[44px] min-w-[44px]" className="min-h-[44px] min-w-[44px]"
onClick={() => onAddToQueue(participant.id)} onClick={() => onAddToQueue(participant.id)}
title={!isEligible ? ineligibleReason : "Add to queue"} title={!isEligible ? ineligibleReason?.message : "Add to queue"}
disabled={!isEligible} disabled={!isEligible}
> >
<ListPlus className="h-4 w-4" /> <ListPlus className="h-4 w-4" />
@ -607,7 +613,7 @@ export const AvailableParticipantsSection = memo(function AvailableParticipantsS
!canPick !canPick
? "Not your turn" ? "Not your turn"
: !isEligible : !isEligible
? ineligibleReason ? ineligibleReason?.message
: undefined : undefined
} }
> >
@ -631,7 +637,7 @@ export const AvailableParticipantsSection = memo(function AvailableParticipantsS
: "hover:bg-muted/50" : "hover:bg-muted/50"
}`} }`}
title={ title={
!isEligible && !isDrafted ? ineligibleReason : undefined !isEligible && !isDrafted ? ineligibleReason?.message : undefined
} }
> >
<span className="text-center text-muted-foreground tabular-nums p-3 px-0"> <span className="text-center text-muted-foreground tabular-nums p-3 px-0">
@ -659,7 +665,7 @@ export const AvailableParticipantsSection = memo(function AvailableParticipantsS
<Badge <Badge
variant="destructive" variant="destructive"
className="text-xs" className="text-xs"
title={ineligibleReason} title={ineligibleReason?.message}
> >
Ineligible Ineligible
</Badge> </Badge>
@ -669,6 +675,11 @@ export const AvailableParticipantsSection = memo(function AvailableParticipantsS
Queued Queued
</Badge> </Badge>
)} )}
{!isDrafted && !isEligible && ineligibleReason && (
<p className="mt-1 text-xs text-destructive/90">
{ineligibleReason.message}
</p>
)}
</div> </div>
</div> </div>
{hasTeam && ( {hasTeam && (
@ -693,7 +704,7 @@ export const AvailableParticipantsSection = memo(function AvailableParticipantsS
onClick={() => onAddToQueue(participant.id)} onClick={() => onAddToQueue(participant.id)}
title={ title={
!isEligible !isEligible
? ineligibleReason ? ineligibleReason?.message
: "Add to queue" : "Add to queue"
} }
disabled={!isEligible} disabled={!isEligible}
@ -722,7 +733,7 @@ export const AvailableParticipantsSection = memo(function AvailableParticipantsS
!canPick !canPick
? "Not your turn" ? "Not your turn"
: !isEligible : !isEligible
? ineligibleReason ? ineligibleReason?.message
: undefined : undefined
} }
> >

View file

@ -1,6 +1,7 @@
import { useState, useEffect, useMemo } from "react"; import { useState, useEffect, useMemo } from "react";
import { Button } from "~/components/ui/button"; import { Button } from "~/components/ui/button";
import { Badge } from "~/components/ui/badge"; import { Badge } from "~/components/ui/badge";
import type { DraftIneligibilityReason } from "~/lib/draft-eligibility";
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
@ -25,7 +26,7 @@ interface ParticipantSelectionDialogProps {
allowParticipantId?: string; allowParticipantId?: string;
eligibility: { eligibility: {
eligibleSportIds: Set<string>; eligibleSportIds: Set<string>;
ineligibleReasons: Record<string, string>; ineligibleReasons: Record<string, DraftIneligibilityReason>;
} | null; } | null;
/** Marks a row with "Current" badge and "Keep" button label */ /** Marks a row with "Current" badge and "Keep" button label */
currentParticipantId?: string; currentParticipantId?: string;
@ -121,7 +122,7 @@ export function ParticipantSelectionDialog({
? eligibility.eligibleSportIds.has(participant.sport.id) ? eligibility.eligibleSportIds.has(participant.sport.id)
: true; : true;
const ineligibleReason = const ineligibleReason =
eligibility?.ineligibleReasons[participant.sport.id]; eligibility?.ineligibleReasons[participant.sport.id] ?? null;
return ( return (
<tr <tr
@ -131,7 +132,7 @@ export function ParticipantSelectionDialog({
? "hover:bg-muted/50" ? "hover:bg-muted/50"
: "bg-destructive/10 opacity-60" : "bg-destructive/10 opacity-60"
}`} }`}
title={!isEligible ? ineligibleReason : undefined} title={!isEligible ? ineligibleReason?.message : undefined}
> >
<td className="p-3"> <td className="p-3">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
@ -149,12 +150,17 @@ export function ParticipantSelectionDialog({
<Badge <Badge
variant="destructive" variant="destructive"
className="text-xs" className="text-xs"
title={ineligibleReason} title={ineligibleReason?.message}
> >
Ineligible Ineligible
</Badge> </Badge>
)} )}
</div> </div>
{!isEligible && ineligibleReason && (
<p className="mt-1 text-xs text-destructive/90">
{ineligibleReason.message}
</p>
)}
</td> </td>
<td className="p-3 text-muted-foreground"> <td className="p-3 text-muted-foreground">
{participant.sport.name} {participant.sport.name}
@ -164,7 +170,7 @@ export function ParticipantSelectionDialog({
size="sm" size="sm"
onClick={() => onSelect(participant.id)} onClick={() => onSelect(participant.id)}
disabled={!isEligible} disabled={!isEligible}
title={!isEligible ? ineligibleReason : undefined} title={!isEligible ? ineligibleReason?.message : undefined}
> >
{isCurrentPick ? "Keep" : "Draft"} {isCurrentPick ? "Keep" : "Draft"}
</Button> </Button>

View file

@ -117,8 +117,14 @@ describe("calculateDraftEligibility", () => {
expect(eligibility.flexPicksAvailable).toBe(2); expect(eligibility.flexPicksAvailable).toBe(2);
// Can only draft from NHL (not yet drafted) // Can only draft from NHL (not yet drafted)
expect(eligibility.eligibleSportIds).toEqual(new Set(["nhl"])); expect(eligibility.eligibleSportIds).toEqual(new Set(["nhl"]));
expect(eligibility.ineligibleReasons["nfl"]).toContain("All flex picks used"); expect(eligibility.ineligibleReasons["nfl"]).toMatchObject({
expect(eligibility.ineligibleReasons["nba"]).toContain("All flex picks used"); code: "no_flex_spots",
message: "Out of flex spots (2/2)",
});
expect(eligibility.ineligibleReasons["nba"]).toMatchObject({
code: "no_flex_spots",
message: "Out of flex spots (2/2)",
});
}); });
}); });
@ -181,9 +187,10 @@ describe("calculateDraftEligibility", () => {
// 2 NFL participants left, 2 teams still need NFL // 2 NFL participants left, 2 teams still need NFL
// For flex pick: 2 > 2 is FALSE, so blocked // For flex pick: 2 > 2 is FALSE, so blocked
expect(eligibility.eligibleSportIds).not.toContain("nfl"); expect(eligibility.eligibleSportIds).not.toContain("nfl");
expect(eligibility.ineligibleReasons["nfl"]).toContain( expect(eligibility.ineligibleReasons["nfl"]).toMatchObject({
"Would prevent other teams from getting NFL" code: "no_extra_participants",
); message: "Not enough NFL participants for remaining teams",
});
expect(eligibility.sportAvailability["nfl"].canDraftAsFlex).toBe(false); expect(eligibility.sportAvailability["nfl"].canDraftAsFlex).toBe(false);
}); });
@ -286,9 +293,9 @@ describe("calculateDraftEligibility", () => {
// Even though NFL has 3 > 4 (global constraint would block flex), // Even though NFL has 3 > 4 (global constraint would block flex),
// team can't draft NFL because flex picks are exhausted // team can't draft NFL because flex picks are exhausted
expect(eligibility.eligibleSportIds).toEqual(new Set([])); expect(eligibility.eligibleSportIds).toEqual(new Set([]));
expect(eligibility.ineligibleReasons["nfl"]).toContain("All flex picks used"); expect(eligibility.ineligibleReasons["nfl"].code).toBe("no_flex_spots");
expect(eligibility.ineligibleReasons["nba"]).toContain("All flex picks used"); expect(eligibility.ineligibleReasons["nba"].code).toBe("no_flex_spots");
expect(eligibility.ineligibleReasons["nhl"]).toContain("All flex picks used"); expect(eligibility.ineligibleReasons["nhl"].code).toBe("no_flex_spots");
}); });
it("allows pick when both constraints satisfied", () => { it("allows pick when both constraints satisfied", () => {
@ -401,9 +408,10 @@ describe("calculateDraftEligibility", () => {
// 3 participants, 5 teams need it, 3 < 5 // 3 participants, 5 teams need it, 3 < 5
expect(eligibility.sportAvailability["nfl"].canDraftAsFirst).toBe(false); expect(eligibility.sportAvailability["nfl"].canDraftAsFirst).toBe(false);
expect(eligibility.eligibleSportIds).not.toContain("nfl"); expect(eligibility.eligibleSportIds).not.toContain("nfl");
expect(eligibility.ineligibleReasons["nfl"]).toContain( expect(eligibility.ineligibleReasons["nfl"]).toMatchObject({
"Only 3 participants left for 5 teams" code: "unavailable",
); message: "Only 3 NFL participants left for 5 teams",
});
}); });
}); });
@ -580,8 +588,8 @@ describe("calculateDraftEligibility", () => {
// Can only draft NHL (not yet picked) // Can only draft NHL (not yet picked)
expect(eligibility.eligibleSportIds).toEqual(new Set(["nhl"])); expect(eligibility.eligibleSportIds).toEqual(new Set(["nhl"]));
expect(eligibility.ineligibleReasons["nfl"]).toContain("All flex picks used"); expect(eligibility.ineligibleReasons["nfl"].code).toBe("no_flex_spots");
expect(eligibility.ineligibleReasons["nba"]).toContain("All flex picks used"); expect(eligibility.ineligibleReasons["nba"].code).toBe("no_flex_spots");
}); });
}); });

View file

@ -18,10 +18,20 @@ export interface SportAvailability {
canDraftAsFlex: boolean; // undraftedCount > teamsStillNeeding canDraftAsFlex: boolean; // undraftedCount > teamsStillNeeding
} }
export type DraftIneligibilityCode =
| "no_flex_spots"
| "no_extra_participants"
| "unavailable";
export interface DraftIneligibilityReason {
code: DraftIneligibilityCode;
message: string;
}
export interface DraftEligibility { export interface DraftEligibility {
teamId: string; teamId: string;
eligibleSportIds: Set<string>; eligibleSportIds: Set<string>;
ineligibleReasons: Record<string, string>; // sportId -> reason ineligibleReasons: Record<string, DraftIneligibilityReason>; // sportId -> reason
flexPicksUsed: number; flexPicksUsed: number;
flexPicksAvailable: number; flexPicksAvailable: number;
picksBySport: Record<string, number>; picksBySport: Record<string, number>;
@ -56,6 +66,34 @@ interface TeamData {
id: string; id: string;
} }
function getFlexSpotsMessage(
flexPicksUsed: number,
flexPicksAvailable: number
): DraftIneligibilityReason {
return {
code: "no_flex_spots",
message: `Out of flex spots (${flexPicksUsed}/${flexPicksAvailable})`,
};
}
function getNoExtrasMessage(sportName: string): DraftIneligibilityReason {
return {
code: "no_extra_participants",
message: `Not enough ${sportName} participants for remaining teams`,
};
}
function getUnavailableMessage(
sportName: string,
undraftedCount: number,
teamsStillNeeding: number
): DraftIneligibilityReason {
return {
code: "unavailable",
message: `Only ${undraftedCount} ${sportName} participant${undraftedCount === 1 ? "" : "s"} left for ${teamsStillNeeding} team${teamsStillNeeding === 1 ? "" : "s"}`,
};
}
/** /**
* Calculate which sports a team is eligible to draft from based on: * Calculate which sports a team is eligible to draft from based on:
* - Team's flex pick usage * - Team's flex pick usage
@ -132,7 +170,7 @@ export function calculateDraftEligibility(
// Step 3: Combine constraints to determine eligibility // Step 3: Combine constraints to determine eligibility
const eligibleSportIds = new Set<string>(); const eligibleSportIds = new Set<string>();
const ineligibleReasons: Record<string, string> = {}; const ineligibleReasons: Record<string, DraftIneligibilityReason> = {};
for (const sport of seasonSports) { for (const sport of seasonSports) {
const sportId = sport.id; const sportId = sport.id;
@ -140,21 +178,21 @@ export function calculateDraftEligibility(
const currentTeamHasSport = (picksBySport[sportId] || 0) > 0; const currentTeamHasSport = (picksBySport[sportId] || 0) > 0;
let isEligible = false; let isEligible = false;
let reason = ""; let reason: DraftIneligibilityReason | null = null;
if (!currentTeamHasSport) { if (!currentTeamHasSport) {
// Team hasn't drafted from this sport yet (would be first pick) // Team hasn't drafted from this sport yet (would be first pick)
if (availability.canDraftAsFirst) { if (availability.canDraftAsFirst) {
isEligible = true; isEligible = true;
} else { } else {
reason = `Only ${availability.undraftedCount} participants left for ${availability.teamsStillNeeding} teams needing ${sport.name}`; reason = getUnavailableMessage(sport.name, availability.undraftedCount, availability.teamsStillNeeding);
} }
} else { } else {
// Team has drafted from this sport (would be flex pick) // Team has drafted from this sport (would be flex pick)
if (!hasFlexesRemaining) { if (!hasFlexesRemaining) {
reason = `All flex picks used (${flexPicksUsed}/${flexPicksAvailable})`; reason = getFlexSpotsMessage(flexPicksUsed, flexPicksAvailable);
} else if (!availability.canDraftAsFlex) { } else if (!availability.canDraftAsFlex) {
reason = `Would prevent other teams from getting ${sport.name} (${availability.undraftedCount} left for ${availability.teamsStillNeeding} teams)`; reason = getNoExtrasMessage(sport.name);
} else { } else {
isEligible = true; isEligible = true;
} }
@ -162,7 +200,7 @@ export function calculateDraftEligibility(
if (isEligible) { if (isEligible) {
eligibleSportIds.add(sportId); eligibleSportIds.add(sportId);
} else { } else if (reason) {
ineligibleReasons[sportId] = reason; ineligibleReasons[sportId] = reason;
} }
} }

View file

@ -288,14 +288,19 @@ describe("draft.force-manual-pick action", () => {
const { calculateDraftEligibility } = await import("~/lib/draft-eligibility"); const { calculateDraftEligibility } = await import("~/lib/draft-eligibility");
vi.mocked(calculateDraftEligibility).mockReturnValue({ vi.mocked(calculateDraftEligibility).mockReturnValue({
eligibleSportIds: new Set(), eligibleSportIds: new Set(),
ineligibleReasons: { [SPORT_ID]: "Sport limit reached" }, ineligibleReasons: {
[SPORT_ID]: {
code: "no_extra_participants",
message: "Not enough NFL participants for remaining teams",
},
},
} as any); } as any);
const response = await action({ request: defaultPickRequest(), params: {}, context: ctx }); const response = await action({ request: defaultPickRequest(), params: {}, context: ctx });
expect(response.status).toBe(400); expect(response.status).toBe(400);
const data = await response.json(); const data = await response.json();
expect(data.error).toMatch(/sport limit reached/i); expect(data.error).toMatch(/not enough nfl participants/i);
}); });
}); });

View file

@ -145,7 +145,7 @@ export async function action(args: ActionFunctionArgs) {
const sportId = participant.sportsSeason.sport.id; const sportId = participant.sportsSeason.sport.id;
if (!eligibility.eligibleSportIds.has(sportId)) { if (!eligibility.eligibleSportIds.has(sportId)) {
const reason = eligibility.ineligibleReasons[sportId] || "Cannot draft from this sport"; const reason = eligibility.ineligibleReasons[sportId]?.message || "Cannot draft from this sport";
return Response.json({ error: reason }, { status: 400 }); return Response.json({ error: reason }, { status: 400 });
} }

View file

@ -133,7 +133,7 @@ export async function action(args: ActionFunctionArgs) {
const sportId = participant.sportsSeason.sport.id; const sportId = participant.sportsSeason.sport.id;
if (!eligibility.eligibleSportIds.has(sportId)) { if (!eligibility.eligibleSportIds.has(sportId)) {
const reason = eligibility.ineligibleReasons[sportId] || "Cannot draft from this sport"; const reason = eligibility.ineligibleReasons[sportId]?.message || "Cannot draft from this sport";
return Response.json({ error: reason }, { status: 400 }); return Response.json({ error: reason }, { status: 400 });
} }

View file

@ -123,7 +123,7 @@ export async function action(args: ActionFunctionArgs) {
const sportId = participant.sportsSeason.sport.id; const sportId = participant.sportsSeason.sport.id;
if (!eligibility.eligibleSportIds.has(sportId)) { if (!eligibility.eligibleSportIds.has(sportId)) {
const reason = eligibility.ineligibleReasons[sportId] || "Cannot draft from this sport"; const reason = eligibility.ineligibleReasons[sportId]?.message || "Cannot draft from this sport";
return Response.json({ error: reason }, { status: 400 }); return Response.json({ error: reason }, { status: 400 });
} }