brackt/app/components/draft/ParticipantSelectionDialog.tsx
Chris Parsons 2df47658fd
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>
2026-02-27 23:46:09 -08:00

188 lines
6.4 KiB
TypeScript

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>
);
}