Show visual dividers in the available participants list indicating where your future draft picks fall relative to the current pick position. Dividers are suppressed when any filter is active (search, sport filter, hide drafted, etc.) since positional references are meaningless in a filtered view. - Add getProjectedPicks() to draft-order.ts for computing future pick positions in a snake draft - Interleave divider items into the virtualized list in AvailableParticipantsSection - Extract ListItem type to module scope - Add tests for getProjectedPicks, divider rendering, and filter suppression
685 lines
26 KiB
TypeScript
685 lines
26 KiB
TypeScript
import { memo, useCallback, useMemo, useRef } 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 } 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<string>,
|
|
queueMap: Map<string, string>,
|
|
eligibility: {
|
|
eligibleSportIds: Set<string>;
|
|
ineligibleReasons: Record<string, string>;
|
|
} | null
|
|
) {
|
|
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];
|
|
return { isDrafted, isInQueue, isEligible, ineligibleReason };
|
|
}
|
|
|
|
interface SportFilterContentProps {
|
|
sportsForDropdown: Array<{ name: string; isDrafted: boolean }>;
|
|
sportFilterSet: Set<string>;
|
|
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 (
|
|
<>
|
|
<div className={listClass}>
|
|
{sportsForDropdown
|
|
.filter(({ isDrafted }) => !isDrafted || !hideCompletedSports)
|
|
.map(({ name, isDrafted }) => {
|
|
const checkboxId = `${idPrefix}${name.replace(/\s+/g, "-").toLowerCase()}`;
|
|
return (
|
|
<label
|
|
key={name}
|
|
htmlFor={checkboxId}
|
|
className={`${itemBaseClass}${isDrafted ? " text-muted-foreground" : ""}`}
|
|
>
|
|
<Checkbox
|
|
id={checkboxId}
|
|
checked={sportFilterSet.has(name)}
|
|
onCheckedChange={(checked) => onToggleSport(name, checked)}
|
|
/>
|
|
{name}
|
|
</label>
|
|
);
|
|
})}
|
|
</div>
|
|
{hasTeam && (
|
|
isSheet ? (
|
|
<div className="px-4 border-t pt-3">
|
|
<label className={toggleLabelClass}>
|
|
<Checkbox
|
|
checked={!hideCompletedSports}
|
|
onCheckedChange={(checked) =>
|
|
onHideCompletedSportsChange(checked === false)
|
|
}
|
|
/>
|
|
<span>Show drafted sports</span>
|
|
</label>
|
|
</div>
|
|
) : (
|
|
<>
|
|
<hr className="mx-2 border-border" />
|
|
<label className={toggleLabelClass}>
|
|
<Checkbox
|
|
checked={!hideCompletedSports}
|
|
onCheckedChange={(checked) =>
|
|
onHideCompletedSportsChange(checked === false)
|
|
}
|
|
/>
|
|
<span>Show drafted sports</span>
|
|
</label>
|
|
</>
|
|
)
|
|
)}
|
|
</>
|
|
);
|
|
}
|
|
|
|
interface AvailableParticipantsSectionProps {
|
|
participants: Array<{
|
|
id: string;
|
|
name: string;
|
|
sport: {
|
|
id: string;
|
|
name: string;
|
|
};
|
|
}>;
|
|
participantRanks: Map<string, { overallRank: number; sportRank: number }>;
|
|
miniDraftGrid?: MiniDraftGridProps;
|
|
searchQuery: string;
|
|
sportFilters: string[];
|
|
hideDrafted: boolean;
|
|
hideIneligible: boolean;
|
|
hideCompletedSports: boolean;
|
|
userDraftedSportNames: Set<string>;
|
|
uniqueSports: string[];
|
|
draftedParticipantIds: Set<string>;
|
|
queue: Array<{ id: string; participantId: string }>;
|
|
eligibility: {
|
|
eligibleSportIds: Set<string>;
|
|
ineligibleReasons: Record<string, string>;
|
|
} | 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 }>;
|
|
}
|
|
|
|
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,
|
|
}: 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<number, number>();
|
|
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<HTMLDivElement>(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]";
|
|
|
|
return (
|
|
<div className="flex flex-col h-full">
|
|
{miniDraftGrid && (
|
|
<div className="px-4 pt-4 pb-2 flex-shrink-0 border-b">
|
|
<MiniDraftGrid {...miniDraftGrid} />
|
|
</div>
|
|
)}
|
|
<div className="px-4 pt-4 pb-2 flex-shrink-0">
|
|
<div className="flex flex-col gap-2 md:flex-row md:flex-wrap md:items-center">
|
|
<div className="flex gap-2 md:contents">
|
|
<input
|
|
type="text"
|
|
placeholder="Search participants..."
|
|
value={searchQuery}
|
|
onChange={(e) => 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"
|
|
/>
|
|
|
|
<div className="md:hidden shrink-0">
|
|
<Sheet>
|
|
<SheetTrigger asChild>
|
|
<Button
|
|
variant="outline"
|
|
aria-label={triggerAriaLabel}
|
|
className="justify-between text-base font-normal"
|
|
>
|
|
<span className="truncate">{triggerText}</span>
|
|
<ChevronDown className="h-4 w-4 shrink-0 opacity-50" />
|
|
</Button>
|
|
</SheetTrigger>
|
|
<SheetContent side="bottom" className="max-h-[70vh] pt-12" aria-describedby={undefined}>
|
|
<SheetTitle className="sr-only">Filter by sport</SheetTitle>
|
|
<SportFilterContent
|
|
sportsForDropdown={sportsForDropdown}
|
|
sportFilterSet={sportFilterSet}
|
|
hideCompletedSports={hideCompletedSports}
|
|
hasTeam={hasTeam}
|
|
variant="sheet"
|
|
onToggleSport={handleToggleSport}
|
|
onHideCompletedSportsChange={onHideCompletedSportsChange}
|
|
/>
|
|
<SheetFooter className="px-4 pb-8 flex-row gap-2">
|
|
{hasActiveFilters && (
|
|
<Button
|
|
variant="outline"
|
|
className="flex-1"
|
|
onClick={handleReset}
|
|
>
|
|
Reset
|
|
</Button>
|
|
)}
|
|
<SheetClose asChild>
|
|
<Button className="flex-1">Done</Button>
|
|
</SheetClose>
|
|
</SheetFooter>
|
|
</SheetContent>
|
|
</Sheet>
|
|
</div>
|
|
|
|
<div className="hidden md:block shrink-0">
|
|
<Popover>
|
|
<PopoverTrigger asChild>
|
|
<Button
|
|
variant="outline"
|
|
aria-label={triggerAriaLabel}
|
|
className="w-[160px] justify-between text-sm font-normal"
|
|
>
|
|
<span className="truncate">{triggerText}</span>
|
|
<ChevronDown className="h-4 w-4 shrink-0 opacity-50" />
|
|
</Button>
|
|
</PopoverTrigger>
|
|
<PopoverContent className="w-72 p-0" align="start">
|
|
{hasActiveFilters && (
|
|
<div className="flex justify-end px-2 pt-2">
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
className="h-auto px-2 py-1 text-xs"
|
|
onClick={handleReset}
|
|
>
|
|
Reset
|
|
</Button>
|
|
</div>
|
|
)}
|
|
<SportFilterContent
|
|
sportsForDropdown={sportsForDropdown}
|
|
sportFilterSet={sportFilterSet}
|
|
hideCompletedSports={hideCompletedSports}
|
|
hasTeam={hasTeam}
|
|
variant="popover"
|
|
onToggleSport={handleToggleSport}
|
|
onHideCompletedSportsChange={onHideCompletedSportsChange}
|
|
/>
|
|
</PopoverContent>
|
|
</Popover>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex gap-2 flex-wrap md:contents">
|
|
<label className="flex flex-1 items-center gap-2 text-sm cursor-pointer px-3 py-2 border rounded-md md:shrink-0 md:flex-none">
|
|
<Checkbox
|
|
checked={!hideDrafted}
|
|
onCheckedChange={(checked) => onHideDraftedChange(checked === false)}
|
|
/>
|
|
<span>Show Drafted</span>
|
|
</label>
|
|
|
|
{hasTeam && eligibility && (
|
|
<label className="flex flex-1 items-center gap-2 text-sm cursor-pointer px-3 py-2 border rounded-md md:shrink-0 md:flex-none">
|
|
<Checkbox
|
|
checked={!hideIneligible}
|
|
onCheckedChange={(checked) => onHideIneligibleChange(checked === false)}
|
|
/>
|
|
<span>Show Ineligible</span>
|
|
</label>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className={`hidden md:grid ${desktopGridClass} bg-muted border-b text-sm font-semibold flex-shrink-0 px-4`}>
|
|
<span className="text-center p-3 px-0">OVR</span>
|
|
<span className="text-center p-3 px-0">SPR</span>
|
|
<span className="text-left p-3">Participant</span>
|
|
</div>
|
|
|
|
<div ref={parentRef} className="flex-1 overflow-y-auto">
|
|
{participants.length === 0 ? (
|
|
<div className="text-center py-8 text-muted-foreground text-sm">
|
|
{emptyMessage}
|
|
</div>
|
|
) : (
|
|
<div
|
|
style={{
|
|
height: virtualizer.getTotalSize(),
|
|
position: "relative",
|
|
width: "100%",
|
|
}}
|
|
>
|
|
{virtualizer.getVirtualItems().map((virtualItem) => {
|
|
const listItem = listItems[virtualItem.index];
|
|
|
|
if (listItem.type === "divider") {
|
|
return (
|
|
<div
|
|
key={virtualItem.key}
|
|
data-index={virtualItem.index}
|
|
ref={virtualizer.measureElement}
|
|
style={{
|
|
position: "absolute",
|
|
top: virtualItem.start,
|
|
left: 0,
|
|
right: 0,
|
|
}}
|
|
>
|
|
<div className="flex items-center gap-2 px-4 py-2">
|
|
<div className="flex-1 border-t-2 border-dashed border-electric/40" />
|
|
<span className="text-sm font-semibold text-electric whitespace-nowrap">
|
|
Projected Round {listItem.round} Pick
|
|
</span>
|
|
<div className="flex-1 border-t-2 border-dashed border-electric/40" />
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const participant = participants[listItem.index];
|
|
const { isDrafted, isInQueue, isEligible, ineligibleReason } =
|
|
getParticipantState(participant, draftedParticipantIds, queueMap, eligibility);
|
|
const rank = participantRanks.get(participant.id);
|
|
const followsDivider = listItems[virtualItem.index - 1]?.type === "divider";
|
|
|
|
return (
|
|
<div
|
|
key={virtualItem.key}
|
|
data-index={virtualItem.index}
|
|
ref={virtualizer.measureElement}
|
|
style={{
|
|
position: "absolute",
|
|
top: virtualItem.start,
|
|
left: 0,
|
|
right: 0,
|
|
}}
|
|
>
|
|
<div className="md:hidden px-4 py-1">
|
|
<div
|
|
className={`bg-card border rounded-lg p-3 flex items-start justify-between gap-3 transition-colors ${
|
|
isDrafted
|
|
? "bg-muted/50 opacity-60"
|
|
: !isEligible
|
|
? "bg-destructive/10 opacity-75"
|
|
: ""
|
|
}`}
|
|
>
|
|
<div className="flex flex-col gap-1 min-w-0">
|
|
<span
|
|
className={`font-semibold text-sm truncate ${!isEligible && !isDrafted ? "text-muted-foreground" : ""}`}
|
|
>
|
|
{participant.name}
|
|
</span>
|
|
<div className="flex flex-wrap gap-1 items-center">
|
|
<Badge variant="outline" className="text-xs">
|
|
{participant.sport.name}
|
|
</Badge>
|
|
<span className="text-xs text-muted-foreground tabular-nums">
|
|
OVR {rank?.overallRank} · SPR {rank?.sportRank}
|
|
</span>
|
|
{isDrafted && (
|
|
<Badge variant="secondary" className="text-xs">
|
|
Drafted
|
|
</Badge>
|
|
)}
|
|
{!isDrafted && !isEligible && (
|
|
<Badge
|
|
variant="destructive"
|
|
className="text-xs"
|
|
title={ineligibleReason}
|
|
>
|
|
Ineligible
|
|
</Badge>
|
|
)}
|
|
{isInQueue && !isDrafted && isEligible && (
|
|
<Badge variant="default" className="text-xs">
|
|
Queued
|
|
</Badge>
|
|
)}
|
|
</div>
|
|
</div>
|
|
{hasTeam && !isDrafted && (
|
|
<div className="flex gap-2 flex-shrink-0">
|
|
{!isInQueue ? (
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
className="min-h-[44px] min-w-[44px]"
|
|
onClick={() => onAddToQueue(participant.id)}
|
|
title={!isEligible ? ineligibleReason : "Add to queue"}
|
|
disabled={!isEligible}
|
|
>
|
|
<ListPlus className="h-4 w-4" />
|
|
</Button>
|
|
) : (
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
className="min-h-[44px] min-w-[44px]"
|
|
onClick={() => {
|
|
const queueId = queueMap.get(participant.id);
|
|
if (queueId) onRemoveFromQueue(queueId);
|
|
}}
|
|
title="Remove from queue"
|
|
>
|
|
<ListX className="h-4 w-4" />
|
|
</Button>
|
|
)}
|
|
<Button
|
|
variant="default"
|
|
size="sm"
|
|
className="min-h-[44px]"
|
|
onClick={() => onMakePick(participant.id)}
|
|
disabled={!canPick || !isEligible}
|
|
title={
|
|
!canPick
|
|
? "Not your turn"
|
|
: !isEligible
|
|
? ineligibleReason
|
|
: undefined
|
|
}
|
|
>
|
|
Draft
|
|
</Button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<div
|
|
className={`hidden md:grid ${desktopGridClass} items-center px-4 transition-colors ${
|
|
followsDivider ? "" : "border-t"
|
|
} ${
|
|
isDrafted
|
|
? "bg-muted/50 opacity-60"
|
|
: !isEligible
|
|
? "bg-destructive/10 opacity-75"
|
|
: "hover:bg-muted/50"
|
|
}`}
|
|
title={
|
|
!isEligible && !isDrafted ? ineligibleReason : undefined
|
|
}
|
|
>
|
|
<span className="text-center text-muted-foreground tabular-nums p-3 px-0">
|
|
{rank?.overallRank}
|
|
</span>
|
|
<span className="text-center text-muted-foreground tabular-nums p-3 px-0">
|
|
{rank?.sportRank}
|
|
</span>
|
|
<div className="p-3">
|
|
<div className="flex items-center gap-2">
|
|
<span
|
|
className={`font-medium ${!isEligible && !isDrafted ? "text-muted-foreground" : ""}`}
|
|
>
|
|
{participant.name}
|
|
</span>
|
|
<Badge variant="outline" className="text-xs">
|
|
{participant.sport.name}
|
|
</Badge>
|
|
{isDrafted && (
|
|
<Badge variant="secondary" className="text-xs">
|
|
Drafted
|
|
</Badge>
|
|
)}
|
|
{!isDrafted && !isEligible && (
|
|
<Badge
|
|
variant="destructive"
|
|
className="text-xs"
|
|
title={ineligibleReason}
|
|
>
|
|
Ineligible
|
|
</Badge>
|
|
)}
|
|
{isInQueue && !isDrafted && isEligible && (
|
|
<Badge variant="default" className="text-xs">
|
|
Queued
|
|
</Badge>
|
|
)}
|
|
</div>
|
|
</div>
|
|
{hasTeam && (
|
|
<div className="p-3">
|
|
<div className="flex gap-2 justify-end items-center">
|
|
{!isDrafted && (
|
|
<>
|
|
{!isInQueue ? (
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => onAddToQueue(participant.id)}
|
|
title={
|
|
!isEligible
|
|
? ineligibleReason
|
|
: "Add to queue"
|
|
}
|
|
disabled={!isEligible}
|
|
>
|
|
<ListPlus className="h-4 w-4" />
|
|
</Button>
|
|
) : (
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => {
|
|
const queueId = queueMap.get(participant.id);
|
|
if (queueId) onRemoveFromQueue(queueId);
|
|
}}
|
|
title="Remove from queue"
|
|
>
|
|
<ListX className="h-4 w-4" />
|
|
</Button>
|
|
)}
|
|
<Button
|
|
variant="default"
|
|
size="sm"
|
|
onClick={() => onMakePick(participant.id)}
|
|
disabled={!canPick || !isEligible}
|
|
title={
|
|
!canPick
|
|
? "Not your turn"
|
|
: !isEligible
|
|
? ineligibleReason
|
|
: undefined
|
|
}
|
|
>
|
|
Draft
|
|
</Button>
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
});
|