brackt/app/components/draft/AvailableParticipantsSection.tsx
Chris Parsons 3210ab265f
Change participant expectedValue from integer to decimal (#1)
* feat: sync odds-based EV to participants table for draft room ranking

When admin enters futures odds or manual probabilities, the calculated EV
is now synced from participantExpectedValues to participants.expectedValue.
This closes the gap where EVs were calculated but never reflected in the
draft room ranking. Also changes expectedValue from integer to decimal(10,2)
for better precision, and displays "—" for participants with no odds data.

https://claude.ai/code/session_01JWG2zg2EMzCn6RgEufPC1T

* fix: correct expectedValue type to string in participants route

Missed instance of `expectedValue: 0` (number) that failed typecheck
after schema change from integer to decimal.

https://claude.ai/code/session_01JWG2zg2EMzCn6RgEufPC1T

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-02-19 11:26:40 -08:00

267 lines
10 KiB
TypeScript

import { Button } from "~/components/ui/button";
import { Badge } from "~/components/ui/badge";
import { ListPlus, ListX } from "lucide-react";
interface AvailableParticipantsSectionProps {
participants: Array<{
id: string;
name: string;
expectedValue: number;
sport: {
id: string;
name: string;
};
}>;
searchQuery: string;
sportFilter: string;
hideDrafted: boolean;
hideIneligible: boolean;
uniqueSports: string[];
draftedParticipantIds: Set<string>;
queue: Array<{ id: string; participantId: string }>;
eligibility: {
eligibleSportIds: Set<string>;
ineligibleReasons: Record<string, string>;
} | null;
canPick: boolean;
hasTeam: boolean;
onSearchChange: (query: string) => void;
onSportFilterChange: (sport: string) => void;
onHideDraftedChange: (hide: boolean) => void;
onHideIneligibleChange: (hide: boolean) => void;
onMakePick: (participantId: string) => void;
onAddToQueue: (participantId: string) => void;
onRemoveFromQueue: (queueId: string) => void;
}
export function AvailableParticipantsSection({
participants,
searchQuery,
sportFilter,
hideDrafted,
hideIneligible,
uniqueSports,
draftedParticipantIds,
queue,
eligibility,
canPick,
hasTeam,
onSearchChange,
onSportFilterChange,
onHideDraftedChange,
onHideIneligibleChange,
onMakePick,
onAddToQueue,
onRemoveFromQueue,
}: AvailableParticipantsSectionProps) {
return (
<div className="flex flex-col h-full">
<div className="px-4 pt-4 pb-2 flex-shrink-0">
<div className="space-y-2">
{/* Search Input */}
<input
type="text"
placeholder="Search participants..."
value={searchQuery}
onChange={(e) => onSearchChange(e.target.value)}
className="w-full px-3 py-2 border rounded-md text-sm"
/>
<div className="flex gap-3">
{/* Sport Filter Dropdown */}
<select
value={sportFilter}
onChange={(e) => onSportFilterChange(e.target.value)}
className="flex-1 px-3 py-2 border rounded-md text-sm"
>
<option value="all">All Sports</option>
{uniqueSports.map((sport) => (
<option key={sport} value={sport}>
{sport}
</option>
))}
</select>
{/* Show/Hide Drafted Toggle */}
<label className="flex items-center gap-2 text-sm cursor-pointer whitespace-nowrap px-3 py-2 border rounded-md">
<input
type="checkbox"
checked={!hideDrafted}
onChange={(e) => onHideDraftedChange(!e.target.checked)}
className="rounded"
/>
<span>Show Drafted</span>
</label>
{/* Show/Hide Ineligible Toggle - only show if user has a team */}
{hasTeam && eligibility && (
<label className="flex items-center gap-2 text-sm cursor-pointer whitespace-nowrap px-3 py-2 border rounded-md">
<input
type="checkbox"
checked={!hideIneligible}
onChange={(e) => onHideIneligibleChange(!e.target.checked)}
className="rounded"
/>
<span>Show Ineligible</span>
</label>
)}
</div>
</div>
</div>
{/* Table - This will scroll independently */}
<div className="flex-1 overflow-hidden">
<div className="h-full overflow-y-auto">
<table className="w-full text-sm">
<thead className="bg-muted sticky top-0">
<tr>
<th className="text-left p-3 font-semibold">Participant</th>
<th className="text-left p-3 font-semibold">Sport</th>
<th className="text-right p-3 font-semibold">EV</th>
{hasTeam && (
<th className="text-right p-3 font-semibold">Queue</th>
)}
</tr>
</thead>
<tbody>
{participants.length === 0 ? (
<tr>
<td
colSpan={hasTeam ? 4 : 3}
className="text-center py-8 text-muted-foreground"
>
No participants found
</td>
</tr>
) : (
participants.map((participant) => {
const isDrafted = draftedParticipantIds.has(participant.id);
const isInQueue = queue.some(
(item) => item.participantId === participant.id
);
// Check eligibility
const isEligible = eligibility
? eligibility.eligibleSportIds.has(participant.sport.id)
: true;
const ineligibleReason =
eligibility?.ineligibleReasons[participant.sport.id];
return (
<tr
key={participant.id}
className={`border-t transition-colors ${
isDrafted
? "bg-muted/50 opacity-60"
: !isEligible
? "bg-red-50/30 dark:bg-red-950/20 opacity-75"
: "hover:bg-muted/50"
}`}
title={
!isEligible && !isDrafted ? ineligibleReason : undefined
}
>
<td className="p-3">
<div className="flex items-center gap-2">
<span
className={`font-medium ${!isEligible && !isDrafted ? "text-muted-foreground" : ""}`}
>
{participant.name}
</span>
{isDrafted && (
<Badge variant="secondary" className="text-xs">
Drafted
</Badge>
)}
{!isDrafted && !isEligible && (
<Badge
variant="destructive"
className="text-xs"
title={ineligibleReason}
>
Ineligible
</Badge>
)}
{isInQueue && !isDrafted && isEligible && (
<Badge variant="default" className="text-xs">
Queued
</Badge>
)}
</div>
</td>
<td className="p-3 text-muted-foreground">
{participant.sport.name}
</td>
<td className="p-3 text-right font-mono font-semibold">
{participant.expectedValue ? participant.expectedValue.toFixed(1) : "—"}
</td>
{hasTeam && (
<td className="p-3 text-right">
<div className="flex gap-2 justify-end items-center">
{!isDrafted && (
<>
{/* Queue icon button */}
{!isInQueue ? (
<Button
variant="ghost"
size="sm"
onClick={() => onAddToQueue(participant.id)}
title={
!isEligible
? ineligibleReason
: "Add to queue"
}
disabled={!isEligible}
>
<ListPlus className="h-4 w-4" />
</Button>
) : (
<Button
variant="ghost"
size="sm"
onClick={() => {
const queueItem = queue.find(
(item) =>
item.participantId === participant.id
);
if (queueItem) {
onRemoveFromQueue(queueItem.id);
}
}}
title="Remove from queue"
>
<ListX className="h-4 w-4" />
</Button>
)}
{/* Draft button - always visible, disabled when not your turn */}
<Button
variant="default"
size="sm"
onClick={() => onMakePick(participant.id)}
disabled={!canPick || !isEligible}
title={
!canPick
? "Not your turn"
: !isEligible
? ineligibleReason
: undefined
}
>
Draft
</Button>
</>
)}
</div>
</td>
)}
</tr>
);
})
)}
</tbody>
</table>
</div>
</div>
</div>
);
}