{
- const queueItem = queue.find(
- (item) => item.participantId === participant.id
- );
- if (queueItem) {
- onRemoveFromQueue(queueItem.id);
- }
+ const queueId = queueMap.get(participant.id);
+ if (queueId) onRemoveFromQueue(queueId);
}}
title="Remove from queue"
>
@@ -260,7 +262,7 @@ export function AvailableParticipantsSection({
) : (
participants.map((participant) => {
const { isDrafted, isInQueue, isEligible, ineligibleReason } =
- getParticipantState(participant, draftedParticipantIds, queue, eligibility);
+ getParticipantState(participant, draftedParticipantIds, queueMap, eligibility);
return (
{
- const queueItem = queue.find(
- (item) =>
- item.participantId === participant.id
- );
- if (queueItem) {
- onRemoveFromQueue(queueItem.id);
- }
+ const queueId = queueMap.get(participant.id);
+ if (queueId) onRemoveFromQueue(queueId);
}}
title="Remove from queue"
>
diff --git a/app/components/draft/ParticipantSelectionDialog.tsx b/app/components/draft/ParticipantSelectionDialog.tsx
new file mode 100644
index 0000000..b7cfe7b
--- /dev/null
+++ b/app/components/draft/ParticipantSelectionDialog.tsx
@@ -0,0 +1,188 @@
+import { useState, useEffect, useMemo } from "react";
+import { Button } from "~/components/ui/button";
+import { Badge } from "~/components/ui/badge";
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+} from "~/components/ui/dialog";
+
+interface ParticipantSelectionDialogProps {
+ open: boolean;
+ onOpenChange: (open: boolean) => void;
+ title: string;
+ description: string;
+ participants: Array<{
+ id: string;
+ name: string;
+ sport: { id: string; name: string };
+ }>;
+ draftedParticipantIds: Set;
+ /** Pass the old participant's ID to keep them in the list (replace-pick dialog) */
+ allowParticipantId?: string;
+ eligibility: {
+ eligibleSportIds: Set;
+ ineligibleReasons: Record;
+ } | null;
+ /** Marks a row with "Current" badge and "Keep" button label */
+ currentParticipantId?: string;
+ uniqueSports: string[];
+ onSelect: (participantId: string) => void;
+}
+
+export function ParticipantSelectionDialog({
+ open,
+ onOpenChange,
+ title,
+ description,
+ participants,
+ draftedParticipantIds,
+ allowParticipantId,
+ eligibility,
+ currentParticipantId,
+ uniqueSports,
+ onSelect,
+}: ParticipantSelectionDialogProps) {
+ const [searchQuery, setSearchQuery] = useState("");
+ const [sportFilter, setSportFilter] = useState("all");
+
+ // Reset filters each time the dialog opens
+ useEffect(() => {
+ if (open) {
+ setSearchQuery("");
+ setSportFilter("all");
+ }
+ }, [open]);
+
+ const filtered = useMemo(
+ () =>
+ participants.filter((p) => {
+ if (p.id !== allowParticipantId && draftedParticipantIds.has(p.id))
+ return false;
+ if (
+ searchQuery &&
+ !p.name.toLowerCase().includes(searchQuery.toLowerCase())
+ )
+ return false;
+ if (sportFilter !== "all" && p.sport.name !== sportFilter) return false;
+ return true;
+ }),
+ [participants, draftedParticipantIds, allowParticipantId, searchQuery, sportFilter]
+ );
+
+ return (
+
+
+
+ {title}
+ {description}
+
+
+
+
+ setSearchQuery(e.target.value)}
+ />
+ setSportFilter(e.target.value)}
+ >
+ All Sports
+ {uniqueSports.map((sport) => (
+
+ {sport}
+
+ ))}
+
+
+
+
+
+
+
+ Participant
+ Sport
+ Action
+
+
+
+ {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 (
+
+
+
+
+ {participant.name}
+
+ {isCurrentPick && (
+
+ Current
+
+ )}
+ {!isEligible && (
+
+ Ineligible
+
+ )}
+
+
+
+ {participant.sport.name}
+
+
+ onSelect(participant.id)}
+ disabled={!isEligible}
+ title={!isEligible ? ineligibleReason : undefined}
+ >
+ {isCurrentPick ? "Keep" : "Draft"}
+
+
+
+ );
+ })}
+
+
+
+
+
+
+ onOpenChange(false)}>
+ Cancel
+
+
+
+
+ );
+}
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)}
- />
- setDialogSportFilter(e.target.value)}
- >
- All Sports
- {uniqueSports.map((sport) => (
-
- {sport}
-
- ))}
-
-
-
- {/* Participant List */}
-
-
-
-
- Participant
- Sport
- Action
-
-
-
- {dialogFilteredParticipants.map((participant: any) => {
- const isEligible = forcePickEligibility
- ? forcePickEligibility.eligibleSportIds.has(participant.sport.id)
- : true;
- const ineligibleReason = forcePickEligibility?.ineligibleReasons[participant.sport.id];
-
- return (
-
-
-
-
- {participant.name}
-
- {!isEligible && (
-
- Ineligible
-
- )}
-
-
-
- {participant.sport.name}
-
-
- handleForceManualPick(participant.id)}
- disabled={!isEligible}
- title={!isEligible ? ineligibleReason : undefined}
- >
- Draft
-
-
-
- );
- })}
-
-
-
-
-
-
- setForcePickDialogOpen(false)}>
- Cancel
-
-
-
-
-
- {/* 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)}
- />
- setDialogSportFilter(e.target.value)}
- >
- All Sports
- {uniqueSports.map((sport) => (
-
- {sport}
-
- ))}
-
-
-
-
-
-
-
- Participant
- Sport
- Action
-
-
-
- {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 (
-
-
-
-
- {participant.name}
-
- {isCurrentPick && (
-
- Current
-
- )}
- {!isEligible && (
-
- Ineligible
-
- )}
-
-
-
- {participant.sport.name}
-
-
- handleReplacePick(participant.id)}
- disabled={!isEligible}
- title={!isEligible ? ineligibleReason : undefined}
- >
- {isCurrentPick ? "Keep" : "Draft"}
-
-
-
- );
- })}
-
-
-
-
-
-
- setReplacePickDialogOpen(false)}>
- Cancel
-
-
-
-
+ 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"
/>
setTimeBankUnit(e.target.value as "seconds" | "minutes" | "hours")
}
- className="px-3 py-2 border rounded-md bg-background text-foreground text-sm"
+ className="px-3 py-2 border rounded-md bg-background text-foreground text-base md:text-sm"
>
Seconds
Minutes