diff --git a/app/components/draft/AvailableParticipantsSection.tsx b/app/components/draft/AvailableParticipantsSection.tsx index a3c3ce9..cd60608 100644 --- a/app/components/draft/AvailableParticipantsSection.tsx +++ b/app/components/draft/AvailableParticipantsSection.tsx @@ -1,3 +1,4 @@ +import { useMemo } from "react"; import { Button } from "~/components/ui/button"; import { Badge } from "~/components/ui/badge"; import { ListPlus, ListX } from "lucide-react"; @@ -5,14 +6,14 @@ import { ListPlus, ListX } from "lucide-react"; function getParticipantState( participant: { id: string; sport: { id: string } }, draftedParticipantIds: Set, - queue: Array<{ participantId: string }>, + queueMap: Map, eligibility: { eligibleSportIds: Set; ineligibleReasons: Record; } | null ) { const isDrafted = draftedParticipantIds.has(participant.id); - const isInQueue = queue.some((item) => item.participantId === participant.id); + const isInQueue = queueMap.has(participant.id); const isEligible = eligibility ? eligibility.eligibleSportIds.has(participant.sport.id) : true; @@ -71,6 +72,11 @@ export function AvailableParticipantsSection({ onAddToQueue, onRemoveFromQueue, }: AvailableParticipantsSectionProps) { + const queueMap = useMemo( + () => new Map(queue.map((item) => [item.participantId, item.id])), + [queue] + ); + return (
@@ -81,7 +87,7 @@ export function AvailableParticipantsSection({ placeholder="Search participants..." value={searchQuery} onChange={(e) => onSearchChange(e.target.value)} - className="w-full px-3 py-2 border rounded-md text-sm bg-background text-foreground" + className="w-full px-3 py-2 border rounded-md text-base md:text-sm bg-background text-foreground" />
@@ -89,7 +95,7 @@ export function AvailableParticipantsSection({ setSearchQuery(e.target.value)} + /> + +
+ +
+ + + + + + + + + + {filtered.map((participant) => { + const isCurrentPick = + participant.id === currentParticipantId; + const isEligible = eligibility + ? eligibility.eligibleSportIds.has(participant.sport.id) + : true; + const ineligibleReason = + eligibility?.ineligibleReasons[participant.sport.id]; + + return ( + + + + + + ); + })} + +
ParticipantSportAction
+
+ + {participant.name} + + {isCurrentPick && ( + + Current + + )} + {!isEligible && ( + + Ineligible + + )} +
+
+ {participant.sport.name} + + +
+
+
+ + + + + + + ); +} diff --git a/app/components/ui/select.tsx b/app/components/ui/select.tsx index 39bde28..034fc02 100644 --- a/app/components/ui/select.tsx +++ b/app/components/ui/select.tsx @@ -35,7 +35,7 @@ function SelectTrigger({ data-slot="select-trigger" data-size={size} className={cn( - "border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", + "border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-base md:text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", className )} {...props} diff --git a/app/routes/leagues/$leagueId.draft.$seasonId.tsx b/app/routes/leagues/$leagueId.draft.$seasonId.tsx index fa3767e..4b89b0f 100644 --- a/app/routes/leagues/$leagueId.draft.$seasonId.tsx +++ b/app/routes/leagues/$leagueId.draft.$seasonId.tsx @@ -19,6 +19,7 @@ import { AvailableParticipantsSection } from "~/components/draft/AvailablePartic import { SidebarRecentPicks } from "~/components/draft/SidebarRecentPicks"; import { DraftGridSection } from "~/components/draft/DraftGridSection"; import { ConnectionOverlay } from "~/components/draft/ConnectionOverlay"; +import { ParticipantSelectionDialog } from "~/components/draft/ParticipantSelectionDialog"; import { calculateDraftEligibility } from "~/lib/draft-eligibility"; import { getTeamForPick } from "~/lib/draft-order"; import { useDraftNotifications } from "~/hooks/useDraftNotifications"; @@ -374,9 +375,6 @@ export default function DraftRoom() { pickNumber: number; teamId: string; } | null>(null); - const [dialogSearchQuery, setDialogSearchQuery] = useState(""); - const [dialogSportFilter, setDialogSportFilter] = useState("all"); - // Replace pick dialog state const [replacePickDialogOpen, setReplacePickDialogOpen] = useState(false); const [replacePickSlot, setReplacePickSlot] = useState<{ @@ -897,8 +895,6 @@ export default function DraftRoom() { return; } setReplacePickSlot({ pickNumber, teamId, oldParticipantId: pick.participant.id }); - setDialogSearchQuery(""); - setDialogSportFilter("all"); setReplacePickDialogOpen(true); }; @@ -1012,8 +1008,6 @@ export default function DraftRoom() { const handleForceManualPickOpen = (pickNumber: number, teamId: string) => { setSelectedPickSlot({ pickNumber, teamId }); - setDialogSearchQuery(""); - setDialogSportFilter("all"); setForcePickDialogOpen(true); }; @@ -1083,41 +1077,6 @@ export default function DraftRoom() { [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] - ); - - // Participants for the replace pick dialog — excludes drafted except the old pick (which is freed) - const replaceDialogFilteredParticipants = useMemo(() => { - if (!replacePickSlot) return []; - return availableParticipants.filter((p: any) => { - // Exclude drafted players, but allow the old participant through since they'll be freed - if (p.id !== replacePickSlot.oldParticipantId && 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, replacePickSlot, draftedParticipantIds, dialogSearchQuery, dialogSportFilter]); - // Filter participants based on search, sport, drafted status, and eligibility const filteredParticipants = useMemo( () => @@ -1524,229 +1483,37 @@ export default function DraftRoom() { })} - {/* Force Manual Pick Dialog */} - { setForcePickDialogOpen(open); if (!open) setSelectedPickSlot(null); }} - > - - - Force Manual Pick - - Select a participant to draft for Pick #{selectedPickSlot?.pickNumber} - - + title="Force Manual Pick" + description={`Select a participant to draft for Pick #${selectedPickSlot?.pickNumber}`} + participants={availableParticipants} + draftedParticipantIds={draftedParticipantIds} + eligibility={forcePickEligibility} + uniqueSports={uniqueSports} + onSelect={handleForceManualPick} + /> -
- {/* Search and Filter */} -
- setDialogSearchQuery(e.target.value)} - /> - -
- - {/* Participant List */} -
- - - - - - - - - - {dialogFilteredParticipants.map((participant: any) => { - const isEligible = forcePickEligibility - ? forcePickEligibility.eligibleSportIds.has(participant.sport.id) - : true; - const ineligibleReason = forcePickEligibility?.ineligibleReasons[participant.sport.id]; - - return ( - - - - - - ); - })} - -
ParticipantSportAction
-
- - {participant.name} - - {!isEligible && ( - - Ineligible - - )} -
-
- {participant.sport.name} - - -
-
-
- - - - -
-
- - {/* Replace Pick Dialog */} - { setReplacePickDialogOpen(open); if (!open) setReplacePickSlot(null); }} - > - - - Replace Pick - - Select a participant to replace the current pick at slot #{replacePickSlot?.pickNumber} - - - -
-
- setDialogSearchQuery(e.target.value)} - /> - -
- -
- - - - - - - - - - {replaceDialogFilteredParticipants.map((participant: any) => { - const isCurrentPick = participant.id === replacePickSlot?.oldParticipantId; - const isEligible = replacePickEligibility - ? replacePickEligibility.eligibleSportIds.has(participant.sport.id) - : true; - const ineligibleReason = replacePickEligibility?.ineligibleReasons[participant.sport.id]; - - return ( - - - - - - ); - })} - -
ParticipantSportAction
-
- - {participant.name} - - {isCurrentPick && ( - - Current - - )} - {!isEligible && ( - - Ineligible - - )} -
-
- {participant.sport.name} - - -
-
-
- - - - -
-
+ title="Replace Pick" + description={`Select a participant to replace the current pick at slot #${replacePickSlot?.pickNumber}`} + participants={availableParticipants} + draftedParticipantIds={draftedParticipantIds} + allowParticipantId={replacePickSlot?.oldParticipantId} + currentParticipantId={replacePickSlot?.oldParticipantId} + eligibility={replacePickEligibility} + uniqueSports={uniqueSports} + onSelect={handleReplacePick} + /> {/* Rollback Confirmation Dialog */} setTimeBankAmount(e.target.value)} - className="flex-1 px-3 py-2 border rounded-md bg-background text-foreground text-sm" + className="flex-1 px-3 py-2 border rounded-md bg-background text-foreground text-base md:text-sm" placeholder="Amount" />