fix: prevent iOS Safari viewport zoom on draft room form inputs (#45)
Add text-base md:text-sm to all native <input> and <select> elements in the draft room (including the ShadCN SelectTrigger) so iOS Safari doesn't auto-zoom when focused. Also extract the two near-duplicate participant selection dialogs into a shared ParticipantSelectionDialog component (-236 lines), and replace O(n) queue.find() scans with a memoized Map in AvailableParticipantsSection. Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
4e447c30ab
commit
2df47658fd
4 changed files with 228 additions and 276 deletions
|
|
@ -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<string>,
|
||||
queue: Array<{ participantId: string }>,
|
||||
queueMap: Map<string, string>,
|
||||
eligibility: {
|
||||
eligibleSportIds: Set<string>;
|
||||
ineligibleReasons: Record<string, string>;
|
||||
} | 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 (
|
||||
<div className="flex flex-col h-full">
|
||||
<div className="px-4 pt-4 pb-2 flex-shrink-0">
|
||||
|
|
@ -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"
|
||||
/>
|
||||
|
||||
<div className="flex flex-wrap gap-2">
|
||||
|
|
@ -89,7 +95,7 @@ export function AvailableParticipantsSection({
|
|||
<select
|
||||
value={sportFilter}
|
||||
onChange={(e) => onSportFilterChange(e.target.value)}
|
||||
className="flex-1 min-w-[120px] px-3 py-2 border rounded-md text-sm bg-background text-foreground"
|
||||
className="flex-1 min-w-[120px] px-3 py-2 border rounded-md text-base md:text-sm bg-background text-foreground"
|
||||
>
|
||||
<option value="all">All Sports</option>
|
||||
{uniqueSports.map((sport) => (
|
||||
|
|
@ -135,7 +141,7 @@ export function AvailableParticipantsSection({
|
|||
) : (
|
||||
participants.map((participant) => {
|
||||
const { isDrafted, isInQueue, isEligible, ineligibleReason } =
|
||||
getParticipantState(participant, draftedParticipantIds, queue, eligibility);
|
||||
getParticipantState(participant, draftedParticipantIds, queueMap, eligibility);
|
||||
|
||||
return (
|
||||
<div
|
||||
|
|
@ -198,12 +204,8 @@ export function AvailableParticipantsSection({
|
|||
size="sm"
|
||||
className="min-h-[44px] min-w-[44px]"
|
||||
onClick={() => {
|
||||
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 (
|
||||
<tr
|
||||
|
|
@ -332,13 +334,8 @@ export function AvailableParticipantsSection({
|
|||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
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"
|
||||
>
|
||||
|
|
|
|||
188
app/components/draft/ParticipantSelectionDialog.tsx
Normal file
188
app/components/draft/ParticipantSelectionDialog.tsx
Normal file
|
|
@ -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<string>;
|
||||
/** Pass the old participant's ID to keep them in the list (replace-pick dialog) */
|
||||
allowParticipantId?: string;
|
||||
eligibility: {
|
||||
eligibleSportIds: Set<string>;
|
||||
ineligibleReasons: Record<string, string>;
|
||||
} | 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 (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-2xl max-h-[80vh] flex flex-col gap-0 p-0">
|
||||
<DialogHeader className="p-6 pb-4">
|
||||
<DialogTitle>{title}</DialogTitle>
|
||||
<DialogDescription>{description}</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="px-6 pb-4">
|
||||
<div className="flex gap-2 mb-4">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search participants..."
|
||||
className="flex-1 px-3 py-2 border rounded-md text-base md:text-sm bg-background text-foreground"
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
/>
|
||||
<select
|
||||
className="px-3 py-2 border rounded-md text-base md:text-sm bg-background text-foreground"
|
||||
value={sportFilter}
|
||||
onChange={(e) => setSportFilter(e.target.value)}
|
||||
>
|
||||
<option value="all">All Sports</option>
|
||||
{uniqueSports.map((sport) => (
|
||||
<option key={sport} value={sport}>
|
||||
{sport}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="max-h-96 overflow-y-auto border rounded-lg">
|
||||
<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">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{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 (
|
||||
<tr
|
||||
key={participant.id}
|
||||
className={`border-t ${
|
||||
isEligible
|
||||
? "hover:bg-muted/50"
|
||||
: "bg-destructive/10 opacity-60"
|
||||
}`}
|
||||
title={!isEligible ? ineligibleReason : undefined}
|
||||
>
|
||||
<td className="p-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<span
|
||||
className={`font-medium ${!isEligible ? "text-muted-foreground" : ""}`}
|
||||
>
|
||||
{participant.name}
|
||||
</span>
|
||||
{isCurrentPick && (
|
||||
<Badge variant="outline" className="text-xs">
|
||||
Current
|
||||
</Badge>
|
||||
)}
|
||||
{!isEligible && (
|
||||
<Badge
|
||||
variant="destructive"
|
||||
className="text-xs"
|
||||
title={ineligibleReason}
|
||||
>
|
||||
Ineligible
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
<td className="p-3 text-muted-foreground">
|
||||
{participant.sport.name}
|
||||
</td>
|
||||
<td className="p-3 text-right">
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => onSelect(participant.id)}
|
||||
disabled={!isEligible}
|
||||
title={!isEligible ? ineligibleReason : undefined}
|
||||
>
|
||||
{isCurrentPick ? "Keep" : "Draft"}
|
||||
</Button>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter className="px-6 pb-6">
|
||||
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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<string>("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() {
|
|||
})}
|
||||
</nav>
|
||||
|
||||
{/* Force Manual Pick Dialog */}
|
||||
<Dialog
|
||||
<ParticipantSelectionDialog
|
||||
open={forcePickDialogOpen && !!selectedPickSlot}
|
||||
onOpenChange={(open) => {
|
||||
setForcePickDialogOpen(open);
|
||||
if (!open) setSelectedPickSlot(null);
|
||||
}}
|
||||
>
|
||||
<DialogContent className="max-w-2xl max-h-[80vh] flex flex-col gap-0 p-0">
|
||||
<DialogHeader className="p-6 pb-4">
|
||||
<DialogTitle>Force Manual Pick</DialogTitle>
|
||||
<DialogDescription>
|
||||
Select a participant to draft for Pick #{selectedPickSlot?.pickNumber}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
title="Force Manual Pick"
|
||||
description={`Select a participant to draft for Pick #${selectedPickSlot?.pickNumber}`}
|
||||
participants={availableParticipants}
|
||||
draftedParticipantIds={draftedParticipantIds}
|
||||
eligibility={forcePickEligibility}
|
||||
uniqueSports={uniqueSports}
|
||||
onSelect={handleForceManualPick}
|
||||
/>
|
||||
|
||||
<div className="px-6 pb-4">
|
||||
{/* Search and Filter */}
|
||||
<div className="flex gap-2 mb-4">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search participants..."
|
||||
className="flex-1 px-3 py-2 border rounded-md bg-background text-foreground"
|
||||
value={dialogSearchQuery}
|
||||
onChange={(e) => setDialogSearchQuery(e.target.value)}
|
||||
/>
|
||||
<select
|
||||
className="px-3 py-2 border rounded-md bg-background text-foreground"
|
||||
value={dialogSportFilter}
|
||||
onChange={(e) => setDialogSportFilter(e.target.value)}
|
||||
>
|
||||
<option value="all">All Sports</option>
|
||||
{uniqueSports.map((sport) => (
|
||||
<option key={sport} value={sport}>
|
||||
{sport}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Participant List */}
|
||||
<div className="max-h-96 overflow-y-auto border rounded-lg">
|
||||
<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">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{dialogFilteredParticipants.map((participant: any) => {
|
||||
const isEligible = forcePickEligibility
|
||||
? forcePickEligibility.eligibleSportIds.has(participant.sport.id)
|
||||
: true;
|
||||
const ineligibleReason = forcePickEligibility?.ineligibleReasons[participant.sport.id];
|
||||
|
||||
return (
|
||||
<tr
|
||||
key={participant.id}
|
||||
className={`border-t ${
|
||||
isEligible ? "hover:bg-muted/50" : "bg-destructive/10 opacity-60"
|
||||
}`}
|
||||
title={!isEligible ? ineligibleReason : undefined}
|
||||
>
|
||||
<td className="p-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className={`font-medium ${!isEligible ? "text-muted-foreground" : ""}`}>
|
||||
{participant.name}
|
||||
</span>
|
||||
{!isEligible && (
|
||||
<Badge
|
||||
variant="destructive"
|
||||
className="text-xs"
|
||||
title={ineligibleReason}
|
||||
>
|
||||
Ineligible
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
<td className="p-3 text-muted-foreground">
|
||||
{participant.sport.name}
|
||||
</td>
|
||||
<td className="p-3 text-right">
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => handleForceManualPick(participant.id)}
|
||||
disabled={!isEligible}
|
||||
title={!isEligible ? ineligibleReason : undefined}
|
||||
>
|
||||
Draft
|
||||
</Button>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter className="px-6 pb-6">
|
||||
<Button variant="outline" onClick={() => setForcePickDialogOpen(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* Replace Pick Dialog */}
|
||||
<Dialog
|
||||
<ParticipantSelectionDialog
|
||||
open={replacePickDialogOpen && !!replacePickSlot}
|
||||
onOpenChange={(open) => {
|
||||
setReplacePickDialogOpen(open);
|
||||
if (!open) setReplacePickSlot(null);
|
||||
}}
|
||||
>
|
||||
<DialogContent className="max-w-2xl max-h-[80vh] flex flex-col gap-0 p-0">
|
||||
<DialogHeader className="p-6 pb-4">
|
||||
<DialogTitle>Replace Pick</DialogTitle>
|
||||
<DialogDescription>
|
||||
Select a participant to replace the current pick at slot #{replacePickSlot?.pickNumber}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="px-6 pb-4">
|
||||
<div className="flex gap-2 mb-4">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search participants..."
|
||||
className="flex-1 px-3 py-2 border rounded-md bg-background text-foreground"
|
||||
value={dialogSearchQuery}
|
||||
onChange={(e) => setDialogSearchQuery(e.target.value)}
|
||||
/>
|
||||
<select
|
||||
className="px-3 py-2 border rounded-md bg-background text-foreground"
|
||||
value={dialogSportFilter}
|
||||
onChange={(e) => setDialogSportFilter(e.target.value)}
|
||||
>
|
||||
<option value="all">All Sports</option>
|
||||
{uniqueSports.map((sport) => (
|
||||
<option key={sport} value={sport}>
|
||||
{sport}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="max-h-96 overflow-y-auto border rounded-lg">
|
||||
<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">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{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 (
|
||||
<tr
|
||||
key={participant.id}
|
||||
className={`border-t ${
|
||||
isEligible ? "hover:bg-muted/50" : "bg-destructive/10 opacity-60"
|
||||
}`}
|
||||
title={!isEligible ? ineligibleReason : undefined}
|
||||
>
|
||||
<td className="p-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className={`font-medium ${!isEligible ? "text-muted-foreground" : ""}`}>
|
||||
{participant.name}
|
||||
</span>
|
||||
{isCurrentPick && (
|
||||
<Badge variant="outline" className="text-xs">
|
||||
Current
|
||||
</Badge>
|
||||
)}
|
||||
{!isEligible && (
|
||||
<Badge
|
||||
variant="destructive"
|
||||
className="text-xs"
|
||||
title={ineligibleReason}
|
||||
>
|
||||
Ineligible
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
<td className="p-3 text-muted-foreground">
|
||||
{participant.sport.name}
|
||||
</td>
|
||||
<td className="p-3 text-right">
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => handleReplacePick(participant.id)}
|
||||
disabled={!isEligible}
|
||||
title={!isEligible ? ineligibleReason : undefined}
|
||||
>
|
||||
{isCurrentPick ? "Keep" : "Draft"}
|
||||
</Button>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter className="px-6 pb-6">
|
||||
<Button variant="outline" onClick={() => setReplacePickDialogOpen(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
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 */}
|
||||
<Dialog
|
||||
|
|
@ -1828,7 +1595,7 @@ export default function DraftRoom() {
|
|||
step="any"
|
||||
value={timeBankAmount}
|
||||
onChange={(e) => 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"
|
||||
/>
|
||||
<select
|
||||
|
|
@ -1836,7 +1603,7 @@ export default function DraftRoom() {
|
|||
onChange={(e) =>
|
||||
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"
|
||||
>
|
||||
<option value="seconds">Seconds</option>
|
||||
<option value="minutes">Minutes</option>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue