import { memo, useCallback, useMemo, useRef, useState } from "react"; import { useVirtualizer } from "@tanstack/react-virtual"; import { Button } from "~/components/ui/button"; import { Badge } from "~/components/ui/badge"; import { Checkbox } from "~/components/ui/checkbox"; import { MiniDraftGrid, type MiniDraftGridProps } from "~/components/draft/MiniDraftGrid"; import { Popover, PopoverTrigger, PopoverContent, } from "~/components/ui/popover"; import { Sheet, SheetTrigger, SheetContent, SheetTitle, SheetFooter, SheetClose, } from "~/components/ui/sheet"; import { ListPlus, ListX, ChevronDown, Eye, EyeOff, SlidersHorizontal } from "lucide-react"; type ListItem = | { type: "participant"; index: number; key: string } | { type: "divider"; round: number; key: string }; function getParticipantState( participant: { id: string; sport: { id: string } }, draftedParticipantIds: Set, queueMap: Map, eligibility: { eligibleSportIds: Set; ineligibleReasons: Record; } | null, watchedParticipantIds: Set ) { const isDrafted = draftedParticipantIds.has(participant.id); const isInQueue = queueMap.has(participant.id); const isEligible = eligibility ? eligibility.eligibleSportIds.has(participant.sport.id) : true; const ineligibleReason = eligibility?.ineligibleReasons[participant.sport.id]; const isWatched = watchedParticipantIds.has(participant.id); return { isDrafted, isInQueue, isEligible, ineligibleReason, isWatched }; } interface SportFilterContentProps { sportsForDropdown: Array<{ name: string; isDrafted: boolean }>; sportFilterSet: Set; hideCompletedSports: boolean; hasTeam: boolean; variant: "sheet" | "popover"; onToggleSport: (sport: string, checked: boolean | "indeterminate") => void; onHideCompletedSportsChange: (hide: boolean) => void; } function SportFilterContent({ sportsForDropdown, sportFilterSet, hideCompletedSports, hasTeam, variant, onToggleSport, onHideCompletedSportsChange, }: SportFilterContentProps) { const isSheet = variant === "sheet"; const idPrefix = isSheet ? "sheet-sport-filter-" : "popover-sport-filter-"; const listClass = isSheet ? "overflow-y-auto flex-1 px-4" : "max-h-64 overflow-y-auto p-2 space-y-0.5"; const itemBaseClass = isSheet ? "flex items-center gap-4 py-4 border-b last:border-b-0 cursor-pointer text-base" : "flex items-center gap-2 px-2 py-2 rounded-sm hover:bg-accent cursor-pointer text-sm"; const toggleLabelClass = isSheet ? "flex items-center gap-4 py-2 cursor-pointer text-base" : "flex items-center gap-2 px-4 py-3 cursor-pointer text-sm"; return ( <>
{sportsForDropdown .filter(({ isDrafted }) => !isDrafted || !hideCompletedSports) .map(({ name, isDrafted }) => { const checkboxId = `${idPrefix}${name.replace(/\s+/g, "-").toLowerCase()}`; return ( ); })}
{hasTeam && ( isSheet ? (
) : ( <>
) )} ); } interface AvailableParticipantsSectionProps { participants: Array<{ id: string; name: string; sport: { id: string; name: string; }; }>; participantRanks: Map; miniDraftGrid?: MiniDraftGridProps; searchQuery: string; sportFilters: string[]; hideDrafted: boolean; hideIneligible: boolean; hideCompletedSports: boolean; userDraftedSportNames: Set; uniqueSports: string[]; draftedParticipantIds: Set; queue: Array<{ id: string; participantId: string }>; eligibility: { eligibleSportIds: Set; ineligibleReasons: Record; } | null; canPick: boolean; hasTeam: boolean; onSearchChange: (query: string) => void; onSportFiltersChange: (sports: string[]) => void; onHideDraftedChange: (hide: boolean) => void; onHideIneligibleChange: (hide: boolean) => void; onHideCompletedSportsChange: (hide: boolean) => void; onMakePick: (participantId: string) => void; onAddToQueue: (participantId: string) => void; onRemoveFromQueue: (queueId: string) => void; projectedPicks?: Array<{ round: number; picksFromNow: number }>; watchedParticipantIds: Set; onToggleWatchlist: (participantId: string) => void; showOnlyWatched: boolean; onShowOnlyWatchedChange: (show: boolean) => void; } export const AvailableParticipantsSection = memo(function AvailableParticipantsSection({ participants, participantRanks, miniDraftGrid, searchQuery, sportFilters, hideDrafted, hideIneligible, hideCompletedSports, userDraftedSportNames, uniqueSports, draftedParticipantIds, queue, eligibility, canPick, hasTeam, onSearchChange, onSportFiltersChange, onHideDraftedChange, onHideIneligibleChange, onHideCompletedSportsChange, onMakePick, onAddToQueue, onRemoveFromQueue, projectedPicks, watchedParticipantIds, onToggleWatchlist, showOnlyWatched, onShowOnlyWatchedChange, }: AvailableParticipantsSectionProps) { const queueMap = useMemo( () => new Map(queue.map((item) => [item.participantId, item.id])), [queue] ); const sportFilterSet = useMemo(() => new Set(sportFilters), [sportFilters]); const sportsForDropdown = useMemo(() => { return uniqueSports.map((s) => ({ name: s, isDrafted: hasTeam && userDraftedSportNames.has(s), })); }, [uniqueSports, userDraftedSportNames, hasTeam]); const triggerText = useMemo( () => sportFilters.length === 0 ? "All Sports" : sportFilters.length === 1 ? sportFilters[0] : `${sportFilters.length} Sports`, [sportFilters] ); const triggerAriaLabel = useMemo( () => sportFilters.length === 0 ? "Filter by sport: All Sports" : sportFilters.length === 1 ? `Filter by sport: ${sportFilters[0]}` : `Filter by sport: ${sportFilters.length} sports selected`, [sportFilters] ); const handleToggleSport = useCallback( (sport: string, checked: boolean | "indeterminate") => { if (checked === true) { onSportFiltersChange([...sportFilters, sport]); } else { onSportFiltersChange(sportFilters.filter((s) => s !== sport)); } }, [sportFilters, onSportFiltersChange] ); const handleReset = useCallback(() => { onSportFiltersChange([]); onHideCompletedSportsChange(false); }, [onSportFiltersChange, onHideCompletedSportsChange]); const emptyMessage = useMemo(() => { if (participants.length > 0) return null; const active: string[] = []; if (hideDrafted) active.push("drafted players"); if (hideIneligible && eligibility) active.push("ineligible players"); if (hideCompletedSports) active.push("drafted sports"); if (sportFilters.length > 0) active.push("other sports"); if (active.length === 0) return "No participants found."; return `No participants found. Try showing ${active.join(", ")}.`; }, [participants.length, hideDrafted, hideIneligible, hideCompletedSports, eligibility, sportFilters]); const hasActiveFilters = sportFilters.length > 0 || hideCompletedSports; const anyFilterActive = searchQuery !== "" || sportFilters.length > 0 || hideDrafted || hideIneligible || hideCompletedSports; const listItems = useMemo(() => { const items: ListItem[] = []; if (!projectedPicks || projectedPicks.length === 0 || anyFilterActive) { for (let i = 0; i < participants.length; i++) { items.push({ type: "participant", index: i, key: participants[i].id }); } return items; } const dividersByAfterIndex = new Map(); for (const pp of projectedPicks) { if (pp.picksFromNow <= 0) continue; const afterIndex = pp.picksFromNow - 1; if (afterIndex >= 0 && afterIndex < participants.length - 1) { dividersByAfterIndex.set(afterIndex, pp.round); } } for (let i = 0; i < participants.length; i++) { items.push({ type: "participant", index: i, key: participants[i].id }); const dividerRound = dividersByAfterIndex.get(i); if (dividerRound !== undefined) { items.push({ type: "divider", round: dividerRound, key: `divider-rd${dividerRound}` }); } } return items; }, [participants, projectedPicks, anyFilterActive]); const parentRef = useRef(null); const virtualizer = useVirtualizer({ count: listItems.length, getScrollElement: () => parentRef.current, estimateSize: (index) => listItems[index]?.type === "divider" ? 36 : 72, overscan: 5, getItemKey: (index) => listItems[index].key, }); const desktopGridClass = hasTeam ? "grid-cols-[60px_60px_1fr_140px]" : "grid-cols-[60px_60px_1fr]"; const [mobileFiltersOpen, setMobileFiltersOpen] = useState(false); return (
{miniDraftGrid && (
)}
onSearchChange(e.target.value)} className="flex-1 min-w-0 px-3 py-2 h-9 border rounded-md text-base md:text-sm bg-background text-foreground" />
{hasActiveFilters && (
)}
Filter by sport {hasActiveFilters && ( )}
{hasTeam && eligibility && ( )} {hasTeam && ( )}
OVR SPR Participant
{participants.length === 0 ? (
{emptyMessage}
) : (
{virtualizer.getVirtualItems().map((virtualItem) => { const listItem = listItems[virtualItem.index]; if (listItem.type === "divider") { return (
Projected Round {listItem.round} Pick
); } const participant = participants[listItem.index]; const { isDrafted, isInQueue, isEligible, ineligibleReason, isWatched } = getParticipantState(participant, draftedParticipantIds, queueMap, eligibility, watchedParticipantIds); const rank = participantRanks.get(participant.id); const followsDivider = listItems[virtualItem.index - 1]?.type === "divider"; return (
{participant.name}
{participant.sport.name} OVR {rank?.overallRank} ยท SPR {rank?.sportRank} {isDrafted && ( Drafted )} {!isDrafted && !isEligible && ( Ineligible )} {isInQueue && !isDrafted && isEligible && ( Queued )}
{hasTeam && !isDrafted && (
{!isInQueue ? ( ) : ( )}
)}
{rank?.overallRank} {rank?.sportRank}
{participant.name} {participant.sport.name} {isDrafted && ( Drafted )} {!isDrafted && !isEligible && ( Ineligible )} {isInQueue && !isDrafted && isEligible && ( Queued )}
{hasTeam && (
{!isDrafted && ( <> {!isInQueue ? ( ) : ( )} )}
)}
); })}
)}
); });