Virtualize available participants list, memoize draft room props
Adds @tanstack/react-virtual to replace separate mobile/desktop lists with a single unified virtual scroll loop. Also memoizes miniDraftGrid and availableParticipantsSectionProps, and switches pick lookup from Array.find to a Map for O(1) access. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
27ee876fd1
commit
e3bc3c1ce2
5 changed files with 286 additions and 236 deletions
|
|
@ -3,6 +3,29 @@ import { render, screen, within } from "@testing-library/react";
|
||||||
import userEvent from "@testing-library/user-event";
|
import userEvent from "@testing-library/user-event";
|
||||||
import { AvailableParticipantsSection } from "~/components/draft/AvailableParticipantsSection";
|
import { AvailableParticipantsSection } from "~/components/draft/AvailableParticipantsSection";
|
||||||
|
|
||||||
|
vi.mock("@tanstack/react-virtual", () => ({
|
||||||
|
useVirtualizer: ({
|
||||||
|
count,
|
||||||
|
estimateSize,
|
||||||
|
}: {
|
||||||
|
count: number;
|
||||||
|
estimateSize: () => number;
|
||||||
|
}) => {
|
||||||
|
const size = estimateSize();
|
||||||
|
const items = Array.from({ length: count }, (_, i) => ({
|
||||||
|
index: i,
|
||||||
|
start: i * size,
|
||||||
|
size,
|
||||||
|
key: i,
|
||||||
|
}));
|
||||||
|
return {
|
||||||
|
getTotalSize: () => count * size,
|
||||||
|
getVirtualItems: () => items,
|
||||||
|
measureElement: vi.fn(),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
const defaultProps = {
|
const defaultProps = {
|
||||||
participants: [
|
participants: [
|
||||||
{ id: "1", name: "Player A", sport: { id: "s1", name: "NFL" } },
|
{ id: "1", name: "Player A", sport: { id: "s1", name: "NFL" } },
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { memo, useCallback, useMemo } from "react";
|
import { memo, useCallback, useMemo, useRef } from "react";
|
||||||
|
import { useVirtualizer } from "@tanstack/react-virtual";
|
||||||
import { Button } from "~/components/ui/button";
|
import { Button } from "~/components/ui/button";
|
||||||
import { Badge } from "~/components/ui/badge";
|
import { Badge } from "~/components/ui/badge";
|
||||||
import { Checkbox } from "~/components/ui/checkbox";
|
import { Checkbox } from "~/components/ui/checkbox";
|
||||||
|
|
@ -271,6 +272,19 @@ export const AvailableParticipantsSection = memo(function AvailableParticipantsS
|
||||||
|
|
||||||
const hasActiveFilters = sportFilters.length > 0 || hideCompletedSports;
|
const hasActiveFilters = sportFilters.length > 0 || hideCompletedSports;
|
||||||
|
|
||||||
|
const parentRef = useRef<HTMLDivElement>(null);
|
||||||
|
const virtualizer = useVirtualizer({
|
||||||
|
count: participants.length,
|
||||||
|
getScrollElement: () => parentRef.current,
|
||||||
|
estimateSize: () => 72,
|
||||||
|
overscan: 5,
|
||||||
|
getItemKey: (index) => participants[index]?.id ?? index,
|
||||||
|
});
|
||||||
|
|
||||||
|
const desktopGridClass = hasTeam
|
||||||
|
? "grid-cols-[60px_60px_1fr_140px]"
|
||||||
|
: "grid-cols-[60px_60px_1fr]";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-full">
|
<div className="flex flex-col h-full">
|
||||||
{miniDraftGrid && (
|
{miniDraftGrid && (
|
||||||
|
|
@ -280,7 +294,6 @@ export const AvailableParticipantsSection = memo(function AvailableParticipantsS
|
||||||
)}
|
)}
|
||||||
<div className="px-4 pt-4 pb-2 flex-shrink-0">
|
<div className="px-4 pt-4 pb-2 flex-shrink-0">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{/* Search Input */}
|
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Search participants..."
|
placeholder="Search participants..."
|
||||||
|
|
@ -290,9 +303,7 @@ export const AvailableParticipantsSection = memo(function AvailableParticipantsS
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
{/* Sport Filter Multi-Select */}
|
|
||||||
<div className="flex-1 min-w-[120px]">
|
<div className="flex-1 min-w-[120px]">
|
||||||
{/* Mobile: bottom sheet */}
|
|
||||||
<div className="md:hidden">
|
<div className="md:hidden">
|
||||||
<Sheet>
|
<Sheet>
|
||||||
<SheetTrigger asChild>
|
<SheetTrigger asChild>
|
||||||
|
|
@ -334,7 +345,6 @@ export const AvailableParticipantsSection = memo(function AvailableParticipantsS
|
||||||
</Sheet>
|
</Sheet>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Desktop: popover */}
|
|
||||||
<div className="hidden md:block">
|
<div className="hidden md:block">
|
||||||
<Popover>
|
<Popover>
|
||||||
<PopoverTrigger asChild>
|
<PopoverTrigger asChild>
|
||||||
|
|
@ -374,7 +384,6 @@ export const AvailableParticipantsSection = memo(function AvailableParticipantsS
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Show/Hide Drafted Toggle */}
|
|
||||||
<label className="flex items-center gap-2 text-sm cursor-pointer whitespace-nowrap px-3 py-2 border rounded-md">
|
<label className="flex items-center gap-2 text-sm cursor-pointer whitespace-nowrap px-3 py-2 border rounded-md">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={!hideDrafted}
|
checked={!hideDrafted}
|
||||||
|
|
@ -383,7 +392,6 @@ export const AvailableParticipantsSection = memo(function AvailableParticipantsS
|
||||||
<span>Show Drafted</span>
|
<span>Show Drafted</span>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
{/* Show/Hide Ineligible Toggle - only show if user has a team */}
|
|
||||||
{hasTeam && eligibility && (
|
{hasTeam && eligibility && (
|
||||||
<label className="flex items-center gap-2 text-sm cursor-pointer whitespace-nowrap px-3 py-2 border rounded-md">
|
<label className="flex items-center gap-2 text-sm cursor-pointer whitespace-nowrap px-3 py-2 border rounded-md">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
|
|
@ -398,169 +406,65 @@ export const AvailableParticipantsSection = memo(function AvailableParticipantsS
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Mobile Card List - visible on mobile only */}
|
<div className={`hidden md:grid ${desktopGridClass} bg-muted border-b text-sm font-semibold flex-shrink-0 px-4`}>
|
||||||
<div className="flex md:hidden flex-1 overflow-y-auto flex-col gap-2 px-4 py-2">
|
<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 ? (
|
{participants.length === 0 ? (
|
||||||
<div className="text-center py-8 text-muted-foreground text-sm">
|
<div className="text-center py-8 text-muted-foreground text-sm">
|
||||||
{emptyMessage}
|
{emptyMessage}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
participants.map((participant) => {
|
<div
|
||||||
const { isDrafted, isInQueue, isEligible, ineligibleReason } =
|
style={{
|
||||||
getParticipantState(participant, draftedParticipantIds, queueMap, eligibility);
|
height: virtualizer.getTotalSize(),
|
||||||
|
position: "relative",
|
||||||
|
width: "100%",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{virtualizer.getVirtualItems().map((virtualItem) => {
|
||||||
|
const participant = participants[virtualItem.index];
|
||||||
|
const { isDrafted, isInQueue, isEligible, ineligibleReason } =
|
||||||
|
getParticipantState(participant, draftedParticipantIds, queueMap, eligibility);
|
||||||
|
const rank = participantRanks.get(participant.id);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={participant.id}
|
key={virtualItem.key}
|
||||||
className={`bg-card border rounded-lg p-3 flex items-start justify-between gap-3 transition-colors ${
|
data-index={virtualItem.index}
|
||||||
isDrafted
|
ref={virtualizer.measureElement}
|
||||||
? "bg-muted/50 opacity-60"
|
style={{
|
||||||
: !isEligible
|
position: "absolute",
|
||||||
? "bg-destructive/10 opacity-75"
|
top: virtualItem.start,
|
||||||
: ""
|
left: 0,
|
||||||
}`}
|
right: 0,
|
||||||
>
|
}}
|
||||||
<div className="flex flex-col gap-1 min-w-0">
|
>
|
||||||
<span
|
<div className="md:hidden px-4 py-1">
|
||||||
className={`font-semibold text-sm truncate ${!isEligible && !isDrafted ? "text-muted-foreground" : ""}`}
|
<div
|
||||||
>
|
className={`bg-card border rounded-lg p-3 flex items-start justify-between gap-3 transition-colors ${
|
||||||
{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 {participantRanks.get(participant.id)?.overallRank} · SPR {participantRanks.get(participant.id)?.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>
|
|
||||||
|
|
||||||
{/* Desktop Table - hidden on mobile */}
|
|
||||||
<div className="hidden md:flex flex-1 overflow-hidden">
|
|
||||||
<div className="h-full w-full overflow-y-auto">
|
|
||||||
<table className="w-full text-sm">
|
|
||||||
<thead className="bg-muted sticky top-0">
|
|
||||||
<tr>
|
|
||||||
<th className="text-center p-3 px-0 font-semibold">OVR</th>
|
|
||||||
<th className="text-center p-3 px-0 font-semibold">SPR</th>
|
|
||||||
<th className="text-left p-3 font-semibold">Participant</th>
|
|
||||||
<th className="text-left p-3 font-semibold">Sport</th>
|
|
||||||
{hasTeam && (
|
|
||||||
<th className="text-right p-3 font-semibold">Queue</th>
|
|
||||||
)}
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{participants.length === 0 ? (
|
|
||||||
<tr>
|
|
||||||
<td
|
|
||||||
colSpan={hasTeam ? 5 : 4}
|
|
||||||
className="text-center py-8 text-muted-foreground"
|
|
||||||
>
|
|
||||||
{emptyMessage}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
) : (
|
|
||||||
participants.map((participant) => {
|
|
||||||
const { isDrafted, isInQueue, isEligible, ineligibleReason } =
|
|
||||||
getParticipantState(participant, draftedParticipantIds, queueMap, eligibility);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<tr
|
|
||||||
key={participant.id}
|
|
||||||
className={`border-t transition-colors ${
|
|
||||||
isDrafted
|
isDrafted
|
||||||
? "bg-muted/50 opacity-60"
|
? "bg-muted/50 opacity-60"
|
||||||
: !isEligible
|
: !isEligible
|
||||||
? "bg-destructive/10 opacity-75"
|
? "bg-destructive/10 opacity-75"
|
||||||
: "hover:bg-muted/50"
|
: ""
|
||||||
}`}
|
}`}
|
||||||
title={
|
|
||||||
!isEligible && !isDrafted ? ineligibleReason : undefined
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<td className="p-3 px-0 text-center text-muted-foreground tabular-nums">
|
<div className="flex flex-col gap-1 min-w-0">
|
||||||
{participantRanks.get(participant.id)?.overallRank}
|
<span
|
||||||
</td>
|
className={`font-semibold text-sm truncate ${!isEligible && !isDrafted ? "text-muted-foreground" : ""}`}
|
||||||
<td className="p-3 px-0 text-center text-muted-foreground tabular-nums">
|
>
|
||||||
{participantRanks.get(participant.id)?.sportRank}
|
{participant.name}
|
||||||
</td>
|
</span>
|
||||||
<td className="p-3">
|
<div className="flex flex-wrap gap-1 items-center">
|
||||||
<div className="flex items-center gap-2">
|
<Badge variant="outline" className="text-xs">
|
||||||
<span
|
{participant.sport.name}
|
||||||
className={`font-medium ${!isEligible && !isDrafted ? "text-muted-foreground" : ""}`}
|
</Badge>
|
||||||
>
|
<span className="text-xs text-muted-foreground tabular-nums">
|
||||||
{participant.name}
|
OVR {rank?.overallRank} · SPR {rank?.sportRank}
|
||||||
</span>
|
</span>
|
||||||
{isDrafted && (
|
{isDrafted && (
|
||||||
<Badge variant="secondary" className="text-xs">
|
<Badge variant="secondary" className="text-xs">
|
||||||
|
|
@ -582,71 +486,162 @@ export const AvailableParticipantsSection = memo(function AvailableParticipantsS
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</div>
|
||||||
<td className="p-3 text-muted-foreground">
|
{hasTeam && !isDrafted && (
|
||||||
{participant.sport.name}
|
<div className="flex gap-2 flex-shrink-0">
|
||||||
</td>
|
{!isInQueue ? (
|
||||||
{hasTeam && (
|
<Button
|
||||||
<td className="p-3 text-right">
|
variant="ghost"
|
||||||
<div className="flex gap-2 justify-end items-center">
|
size="sm"
|
||||||
{!isDrafted && (
|
className="min-h-[44px] min-w-[44px]"
|
||||||
<>
|
onClick={() => onAddToQueue(participant.id)}
|
||||||
{/* Queue icon button */}
|
title={!isEligible ? ineligibleReason : "Add to queue"}
|
||||||
{!isInQueue ? (
|
disabled={!isEligible}
|
||||||
<Button
|
>
|
||||||
variant="ghost"
|
<ListPlus className="h-4 w-4" />
|
||||||
size="sm"
|
</Button>
|
||||||
onClick={() => onAddToQueue(participant.id)}
|
) : (
|
||||||
title={
|
<Button
|
||||||
!isEligible
|
variant="ghost"
|
||||||
? ineligibleReason
|
size="sm"
|
||||||
: "Add to queue"
|
className="min-h-[44px] min-w-[44px]"
|
||||||
}
|
onClick={() => {
|
||||||
disabled={!isEligible}
|
const queueId = queueMap.get(participant.id);
|
||||||
>
|
if (queueId) onRemoveFromQueue(queueId);
|
||||||
<ListPlus className="h-4 w-4" />
|
}}
|
||||||
</Button>
|
title="Remove from queue"
|
||||||
) : (
|
>
|
||||||
<Button
|
<ListX className="h-4 w-4" />
|
||||||
variant="ghost"
|
</Button>
|
||||||
size="sm"
|
)}
|
||||||
onClick={() => {
|
<Button
|
||||||
const queueId = queueMap.get(participant.id);
|
variant="default"
|
||||||
if (queueId) onRemoveFromQueue(queueId);
|
size="sm"
|
||||||
}}
|
className="min-h-[44px]"
|
||||||
title="Remove from queue"
|
onClick={() => onMakePick(participant.id)}
|
||||||
>
|
disabled={!canPick || !isEligible}
|
||||||
<ListX className="h-4 w-4" />
|
title={
|
||||||
</Button>
|
!canPick
|
||||||
)}
|
? "Not your turn"
|
||||||
{/* Draft button - always visible, disabled when not your turn */}
|
: !isEligible
|
||||||
<Button
|
? ineligibleReason
|
||||||
variant="default"
|
: undefined
|
||||||
size="sm"
|
}
|
||||||
onClick={() => onMakePick(participant.id)}
|
>
|
||||||
disabled={!canPick || !isEligible}
|
Draft
|
||||||
title={
|
</Button>
|
||||||
!canPick
|
</div>
|
||||||
? "Not your turn"
|
|
||||||
: !isEligible
|
|
||||||
? ineligibleReason
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
>
|
|
||||||
Draft
|
|
||||||
</Button>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
)}
|
)}
|
||||||
</tr>
|
</div>
|
||||||
);
|
</div>
|
||||||
})
|
|
||||||
)}
|
<div
|
||||||
</tbody>
|
className={`hidden md:grid ${desktopGridClass} items-center px-4 transition-colors border-t ${
|
||||||
</table>
|
isDrafted
|
||||||
</div>
|
? "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>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1395,6 +1395,7 @@ export default function DraftRoom() {
|
||||||
const draftGrid = useMemo(() => {
|
const draftGrid = useMemo(() => {
|
||||||
const teamCount = draftSlots.length;
|
const teamCount = draftSlots.length;
|
||||||
const rounds = season.draftRounds;
|
const rounds = season.draftRounds;
|
||||||
|
const pickMap = new Map(picks.map((p) => [p.pickNumber, p]));
|
||||||
const grid: Array<
|
const grid: Array<
|
||||||
Array<{
|
Array<{
|
||||||
pickNumber: number;
|
pickNumber: number;
|
||||||
|
|
@ -1415,7 +1416,7 @@ export default function DraftRoom() {
|
||||||
const pickNumber = (round - 1) * teamCount + pickInRound;
|
const pickNumber = (round - 1) * teamCount + pickInRound;
|
||||||
const teamId = draftSlots[teamIndex]?.team.id;
|
const teamId = draftSlots[teamIndex]?.team.id;
|
||||||
|
|
||||||
const pick = picks.find((p) => p.pickNumber === pickNumber);
|
const pick = pickMap.get(pickNumber);
|
||||||
|
|
||||||
roundPicks.push({ pickNumber, round, pickInRound, teamId, pick });
|
roundPicks.push({ pickNumber, round, pickInRound, teamId, pick });
|
||||||
}
|
}
|
||||||
|
|
@ -1502,20 +1503,22 @@ export default function DraftRoom() {
|
||||||
}
|
}
|
||||||
}, [userDraftedSportNames]);
|
}, [userDraftedSportNames]);
|
||||||
|
|
||||||
const availableParticipantsSectionProps = {
|
const miniDraftGrid = useMemo(() => ({
|
||||||
|
draftSlots,
|
||||||
|
draftGrid,
|
||||||
|
currentPick,
|
||||||
|
currentRound,
|
||||||
|
ownerMap,
|
||||||
|
onForceAutopick: isCommissioner ? handleForceAutopick : undefined,
|
||||||
|
onForceManualPickOpen: isCommissioner ? handleForceManualPickOpen : undefined,
|
||||||
|
onReplacePick: isCommissioner ? handleReplacePickOpen : undefined,
|
||||||
|
onRollbackToPick: isCommissioner && !isDraftComplete ? handleRollbackToPick : undefined,
|
||||||
|
}), [draftSlots, draftGrid, currentPick, currentRound, ownerMap, isCommissioner, handleForceAutopick, handleForceManualPickOpen, handleReplacePickOpen, handleRollbackToPick, isDraftComplete]);
|
||||||
|
|
||||||
|
const availableParticipantsSectionProps = useMemo(() => ({
|
||||||
participants: filteredParticipants,
|
participants: filteredParticipants,
|
||||||
participantRanks,
|
participantRanks,
|
||||||
miniDraftGrid: {
|
miniDraftGrid,
|
||||||
draftSlots,
|
|
||||||
draftGrid,
|
|
||||||
currentPick,
|
|
||||||
currentRound,
|
|
||||||
ownerMap,
|
|
||||||
onForceAutopick: isCommissioner ? handleForceAutopick : undefined,
|
|
||||||
onForceManualPickOpen: isCommissioner ? handleForceManualPickOpen : undefined,
|
|
||||||
onReplacePick: isCommissioner ? handleReplacePickOpen : undefined,
|
|
||||||
onRollbackToPick: isCommissioner && !isDraftComplete ? handleRollbackToPick : undefined,
|
|
||||||
},
|
|
||||||
searchQuery,
|
searchQuery,
|
||||||
sportFilters,
|
sportFilters,
|
||||||
hideDrafted,
|
hideDrafted,
|
||||||
|
|
@ -1536,7 +1539,7 @@ export default function DraftRoom() {
|
||||||
onMakePick: handleMakePick,
|
onMakePick: handleMakePick,
|
||||||
onAddToQueue: handleAddToQueue,
|
onAddToQueue: handleAddToQueue,
|
||||||
onRemoveFromQueue: handleRemoveFromQueue,
|
onRemoveFromQueue: handleRemoveFromQueue,
|
||||||
};
|
}), [filteredParticipants, participantRanks, miniDraftGrid, searchQuery, sportFilters, hideDrafted, hideIneligible, uniqueSports, draftedParticipantIds, queue, eligibility, canPick, userTeam, hideCompletedSports, userDraftedSportNames, handleHideCompletedSportsChange, handleMakePick, handleAddToQueue, handleRemoveFromQueue]);
|
||||||
|
|
||||||
const draftGridSectionProps = {
|
const draftGridSectionProps = {
|
||||||
draftSlots,
|
draftSlots,
|
||||||
|
|
|
||||||
28
package-lock.json
generated
28
package-lock.json
generated
|
|
@ -29,6 +29,7 @@
|
||||||
"@react-router/express": "^7.7.1",
|
"@react-router/express": "^7.7.1",
|
||||||
"@react-router/node": "^7.7.1",
|
"@react-router/node": "^7.7.1",
|
||||||
"@sentry/react-router": "^10.43.0",
|
"@sentry/react-router": "^10.43.0",
|
||||||
|
"@tanstack/react-virtual": "^3.13.24",
|
||||||
"@types/nprogress": "^0.2.3",
|
"@types/nprogress": "^0.2.3",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
|
|
@ -6491,6 +6492,33 @@
|
||||||
"vite": "^5.2.0 || ^6 || ^7"
|
"vite": "^5.2.0 || ^6 || ^7"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@tanstack/react-virtual": {
|
||||||
|
"version": "3.13.24",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.13.24.tgz",
|
||||||
|
"integrity": "sha512-aIJvz5OSkhNIhZIpYivrxrPTKYsjW9Uzy+sP/mx0S3sev2HyvPb7xmjbYvokzEpfgYHy/HjzJ2zFAETuUfgCpg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@tanstack/virtual-core": "3.14.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/tannerlinsley"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||||
|
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tanstack/virtual-core": {
|
||||||
|
"version": "3.14.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.14.0.tgz",
|
||||||
|
"integrity": "sha512-JLANqGy/D6k4Ujmh8Tr25lGimuOXNiaVyXaCAZS0W+1390sADdGnyUdSWNIfd49gebtIxGMij4IktRVzrdr12Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/tannerlinsley"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@testing-library/cypress": {
|
"node_modules/@testing-library/cypress": {
|
||||||
"version": "10.1.0",
|
"version": "10.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@testing-library/cypress/-/cypress-10.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@testing-library/cypress/-/cypress-10.1.0.tgz",
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,7 @@
|
||||||
"@react-router/express": "^7.7.1",
|
"@react-router/express": "^7.7.1",
|
||||||
"@react-router/node": "^7.7.1",
|
"@react-router/node": "^7.7.1",
|
||||||
"@sentry/react-router": "^10.43.0",
|
"@sentry/react-router": "^10.43.0",
|
||||||
|
"@tanstack/react-virtual": "^3.13.24",
|
||||||
"@types/nprogress": "^0.2.3",
|
"@types/nprogress": "^0.2.3",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue