More draft room fixes.
This commit is contained in:
parent
44ad954dfe
commit
a4eaec865b
4 changed files with 346 additions and 294 deletions
|
|
@ -1,12 +1,7 @@
|
|||
import { useState } from "react";
|
||||
import type { ReactNode } from "react";
|
||||
import { ChevronLeft, ChevronRight } from "lucide-react";
|
||||
import { ChevronDown, ChevronLeft, ChevronRight } from "lucide-react";
|
||||
import { Button } from "~/components/ui/button";
|
||||
import {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
} from "~/components/ui/accordion";
|
||||
import { cn } from "~/lib/utils";
|
||||
|
||||
interface DraftSidebarProps {
|
||||
|
|
@ -26,17 +21,19 @@ export function DraftSidebar({
|
|||
settingsSection,
|
||||
className,
|
||||
}: DraftSidebarProps) {
|
||||
const [queueOpen, setQueueOpen] = useState(true);
|
||||
const [picksOpen, setPicksOpen] = useState(true);
|
||||
|
||||
if (collapsed) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"relative flex-shrink-0 bg-card border-r border-border transition-all duration-300 flex flex-col",
|
||||
"w-12 hidden lg:block", // Hide completely on mobile when collapsed
|
||||
"relative flex-shrink-0 bg-card border-r border-border transition-all duration-300 flex flex-col h-full",
|
||||
"w-12 hidden lg:flex",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<div className="flex-1" />
|
||||
{/* Expand button at bottom to match hide button position */}
|
||||
<div className="border-t border-border p-2 flex-shrink-0">
|
||||
<Button
|
||||
variant="ghost"
|
||||
|
|
@ -65,45 +62,50 @@ export function DraftSidebar({
|
|||
className={cn(
|
||||
"relative flex-shrink-0 bg-card border-r border-border transition-all duration-300 flex flex-col",
|
||||
"w-[300px]",
|
||||
// Mobile: fixed overlay, Desktop: normal sidebar
|
||||
"fixed inset-y-0 left-0 z-50 lg:relative lg:z-auto",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<Accordion
|
||||
type="multiple"
|
||||
defaultValue={["queue", "recent-picks"]}
|
||||
className="flex-1 overflow-hidden flex flex-col"
|
||||
>
|
||||
<div className="flex-1 flex flex-col min-h-0 overflow-hidden">
|
||||
{/* Queue Section */}
|
||||
<AccordionItem value="queue" className="border-b flex-shrink-0">
|
||||
<AccordionTrigger className="px-4 py-3 hover:no-underline bg-muted/50 hover:bg-muted">
|
||||
<div className={cn("flex flex-col border-b", queueOpen ? "flex-1 min-h-0" : "flex-shrink-0")}>
|
||||
<button
|
||||
onClick={() => setQueueOpen((o) => !o)}
|
||||
className="px-4 py-3 bg-muted/50 hover:bg-muted flex items-center justify-between w-full flex-shrink-0"
|
||||
>
|
||||
<h2 className="font-semibold text-sm">My Queue</h2>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="max-h-[35vh] overflow-y-auto pb-0">
|
||||
{queueSection}
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
<ChevronDown className={cn("h-4 w-4 transition-transform", queueOpen && "rotate-180")} />
|
||||
</button>
|
||||
{queueOpen && (
|
||||
<div className="flex-1 overflow-y-auto min-h-0">
|
||||
{queueSection}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Recent Picks Section */}
|
||||
<AccordionItem value="recent-picks" className="border-0">
|
||||
<AccordionTrigger className="px-4 py-3 hover:no-underline bg-muted/50 hover:bg-muted">
|
||||
{/* Picks Section */}
|
||||
<div className={cn("flex flex-col", picksOpen ? "flex-1 min-h-0" : "flex-shrink-0")}>
|
||||
<button
|
||||
onClick={() => setPicksOpen((o) => !o)}
|
||||
className="px-4 py-3 bg-muted/50 hover:bg-muted flex items-center justify-between w-full flex-shrink-0"
|
||||
>
|
||||
<h2 className="font-semibold text-sm">Picks</h2>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="pb-0 overflow-y-auto max-h-[45vh]">
|
||||
{recentPicksSection}
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
<ChevronDown className={cn("h-4 w-4 transition-transform", picksOpen && "rotate-180")} />
|
||||
</button>
|
||||
{picksOpen && (
|
||||
<div className="flex-1 overflow-y-auto min-h-0">
|
||||
{recentPicksSection}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Settings section (e.g. notification toggle) */}
|
||||
{settingsSection && (
|
||||
<div className="border-t border-border px-4 py-3 flex-shrink-0">
|
||||
{settingsSection}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Collapse button at bottom */}
|
||||
<div className="border-t border-border p-2 flex-shrink-0">
|
||||
<Button
|
||||
variant="ghost"
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ 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 } from "~/components/draft/MiniDraftGrid";
|
||||
import { MiniDraftGrid, type MiniDraftGridProps } from "~/components/draft/MiniDraftGrid";
|
||||
import {
|
||||
Popover,
|
||||
PopoverTrigger,
|
||||
|
|
@ -133,32 +133,7 @@ interface AvailableParticipantsSectionProps {
|
|||
};
|
||||
}>;
|
||||
participantRanks: Map<string, { overallRank: number; sportRank: number }>;
|
||||
miniDraftGrid?: {
|
||||
draftSlots: Array<{
|
||||
id: string;
|
||||
draftOrder: number;
|
||||
team: { id: string; name: string; logoUrl?: string | null };
|
||||
}>;
|
||||
draftGrid: Array<
|
||||
Array<{
|
||||
pickNumber: number;
|
||||
round: number;
|
||||
pickInRound: number;
|
||||
teamId: string;
|
||||
pick?: {
|
||||
participant: { name: string };
|
||||
sport: { name: string };
|
||||
};
|
||||
}>
|
||||
>;
|
||||
currentPick: number;
|
||||
currentRound: number;
|
||||
ownerMap?: Record<string, string>;
|
||||
onForceAutopick?: (pickNumber: number, teamId: string) => void;
|
||||
onForceManualPickOpen?: (pickNumber: number, teamId: string) => void;
|
||||
onReplacePick?: (pickNumber: number, teamId: string) => void;
|
||||
onRollbackToPick?: (pickNumber: number) => void;
|
||||
};
|
||||
miniDraftGrid?: MiniDraftGridProps;
|
||||
searchQuery: string;
|
||||
sportFilters: string[];
|
||||
hideDrafted: boolean;
|
||||
|
|
@ -293,116 +268,111 @@ export const AvailableParticipantsSection = memo(function AvailableParticipantsS
|
|||
</div>
|
||||
)}
|
||||
<div className="px-4 pt-4 pb-2 flex-shrink-0">
|
||||
<div className="space-y-2">
|
||||
<div className="flex flex-wrap gap-2 items-center">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search participants..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => onSearchChange(e.target.value)}
|
||||
className="w-full px-3 py-2 border rounded-md text-base md:text-sm bg-background text-foreground"
|
||||
className="flex-1 min-w-0 px-3 py-2 border rounded-md text-base md:text-sm bg-background text-foreground"
|
||||
/>
|
||||
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<div className="flex-1 min-w-[120px]">
|
||||
<div className="md:hidden">
|
||||
<Sheet>
|
||||
<SheetTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
aria-label={triggerAriaLabel}
|
||||
className="w-full 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">
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
aria-label={triggerAriaLabel}
|
||||
className="w-full 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>
|
||||
|
||||
<label className="flex items-center gap-2 text-sm cursor-pointer whitespace-nowrap px-3 py-2 border rounded-md">
|
||||
<Checkbox
|
||||
checked={!hideDrafted}
|
||||
onCheckedChange={(checked) => onHideDraftedChange(checked === false)}
|
||||
/>
|
||||
<span>Show Drafted</span>
|
||||
</label>
|
||||
|
||||
{hasTeam && eligibility && (
|
||||
<label className="flex items-center gap-2 text-sm cursor-pointer whitespace-nowrap px-3 py-2 border rounded-md">
|
||||
<Checkbox
|
||||
checked={!hideIneligible}
|
||||
onCheckedChange={(checked) => onHideIneligibleChange(checked === false)}
|
||||
<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}
|
||||
/>
|
||||
<span>Show Ineligible</span>
|
||||
</label>
|
||||
)}
|
||||
<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>
|
||||
|
||||
<label className="flex items-center gap-2 text-sm cursor-pointer whitespace-nowrap px-3 py-2 border rounded-md shrink-0">
|
||||
<Checkbox
|
||||
checked={!hideDrafted}
|
||||
onCheckedChange={(checked) => onHideDraftedChange(checked === false)}
|
||||
/>
|
||||
<span>Show Drafted</span>
|
||||
</label>
|
||||
|
||||
{hasTeam && eligibility && (
|
||||
<label className="flex items-center gap-2 text-sm cursor-pointer whitespace-nowrap px-3 py-2 border rounded-md shrink-0">
|
||||
<Checkbox
|
||||
checked={!hideIneligible}
|
||||
onCheckedChange={(checked) => onHideIneligibleChange(checked === false)}
|
||||
/>
|
||||
<span>Show Ineligible</span>
|
||||
</label>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { memo, useMemo } from "react";
|
||||
import { memo, useState, useEffect, useRef } from "react";
|
||||
import { DraftPickCell } from "~/components/draft/DraftPickCell";
|
||||
import type { SeasonStatus } from "~/models/season";
|
||||
import {
|
||||
|
|
@ -7,8 +7,17 @@ import {
|
|||
ContextMenuItem,
|
||||
ContextMenuTrigger,
|
||||
} from "~/components/ui/context-menu";
|
||||
import { formatClockTime, getTimerColorClass } from "~/lib/draft-timer";
|
||||
|
||||
interface MiniDraftGridProps {
|
||||
const ROW_GAP = 6; // gap-1.5
|
||||
const ANIMATION_MS = 300;
|
||||
|
||||
function getRoundIndices(round: number): [number, number] {
|
||||
if (round <= 2) return [0, 1];
|
||||
return [round - 2, round - 1];
|
||||
}
|
||||
|
||||
export interface MiniDraftGridProps {
|
||||
draftSlots: Array<{
|
||||
id: string;
|
||||
draftOrder: number;
|
||||
|
|
@ -37,6 +46,8 @@ interface MiniDraftGridProps {
|
|||
currentPick: number;
|
||||
currentRound: number;
|
||||
ownerMap?: Record<string, string>;
|
||||
teamTimers?: Record<string, number | undefined>;
|
||||
autodraftStatus?: Record<string, { isEnabled: boolean }>;
|
||||
seasonStatus?: SeasonStatus;
|
||||
draftPaused?: boolean;
|
||||
onForceAutopick?: (pickNumber: number, teamId: string) => void;
|
||||
|
|
@ -51,6 +62,8 @@ export const MiniDraftGrid = memo(function MiniDraftGrid({
|
|||
currentPick,
|
||||
currentRound,
|
||||
ownerMap = {},
|
||||
teamTimers = {},
|
||||
autodraftStatus = {},
|
||||
seasonStatus,
|
||||
draftPaused,
|
||||
onForceAutopick,
|
||||
|
|
@ -58,139 +71,204 @@ export const MiniDraftGrid = memo(function MiniDraftGrid({
|
|||
onReplacePick,
|
||||
onRollbackToPick,
|
||||
}: MiniDraftGridProps) {
|
||||
const roundsToShow = useMemo(() => {
|
||||
if (currentRound <= 1) return [0, 1];
|
||||
return [currentRound - 2, currentRound - 1];
|
||||
const [displayedIndices, setDisplayedIndices] = useState<[number, number]>(() => getRoundIndices(currentRound));
|
||||
const [extraIndex, setExtraIndex] = useState<number | null>(null);
|
||||
const [sliding, setSliding] = useState(false);
|
||||
const animGenRef = useRef(0);
|
||||
const prevRoundRef = useRef(currentRound);
|
||||
const firstRowRef = useRef<HTMLDivElement>(null);
|
||||
const [rowHeight, setRowHeight] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
if (firstRowRef.current) {
|
||||
setRowHeight(firstRowRef.current.getBoundingClientRect().height);
|
||||
}
|
||||
}, [draftGrid]);
|
||||
|
||||
useEffect(() => {
|
||||
const prev = prevRoundRef.current;
|
||||
prevRoundRef.current = currentRound;
|
||||
|
||||
const newIndices = getRoundIndices(currentRound);
|
||||
const prevIndices = getRoundIndices(prev);
|
||||
|
||||
if (newIndices[0] === prevIndices[0]) return;
|
||||
|
||||
// Non-sequential jump or animation already running: snap and invalidate any in-flight animation
|
||||
if (newIndices[0] !== prevIndices[0] + 1 || animGenRef.current > 0) {
|
||||
animGenRef.current++; // invalidate any in-flight animation closure
|
||||
setDisplayedIndices(newIndices);
|
||||
setExtraIndex(null);
|
||||
setSliding(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const gen = ++animGenRef.current;
|
||||
setExtraIndex(newIndices[1]);
|
||||
|
||||
// Two rAFs: first lets React flush the extra row to DOM, second starts the CSS transition
|
||||
requestAnimationFrame(() => {
|
||||
requestAnimationFrame(() => {
|
||||
setSliding(true);
|
||||
|
||||
setTimeout(() => {
|
||||
if (animGenRef.current !== gen) return; // interrupted by snap
|
||||
setDisplayedIndices(newIndices);
|
||||
setExtraIndex(null);
|
||||
setSliding(false);
|
||||
animGenRef.current = 0;
|
||||
}, ANIMATION_MS + 20);
|
||||
});
|
||||
});
|
||||
}, [currentRound]);
|
||||
|
||||
if (draftGrid.length === 0) return null;
|
||||
|
||||
// h-14 (56px) + gap-1.5 (6px) fallback until first measurement
|
||||
const effectiveRowHeight = rowHeight > 0 ? rowHeight : 56;
|
||||
|
||||
const roundIndicesToRender: number[] = extraIndex !== null
|
||||
? [displayedIndices[0], displayedIndices[1], extraIndex]
|
||||
: [displayedIndices[0], displayedIndices[1]];
|
||||
|
||||
return (
|
||||
<div className="overflow-x-auto">
|
||||
<div className="inline-block min-w-full">
|
||||
{/* Team header */}
|
||||
<div className="flex gap-1.5 mb-1.5">
|
||||
<div className="w-7 flex-shrink-0" />
|
||||
{draftSlots.map((slot) => (
|
||||
<div key={slot.id} className="flex-1 min-w-20 text-center">
|
||||
<div className="text-xs font-medium truncate px-1 text-muted-foreground">
|
||||
{ownerMap[slot.team.id] || slot.team.name}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
{roundsToShow.map((roundIndex) => {
|
||||
if (roundIndex >= draftGrid.length) return null;
|
||||
const roundPicks = draftGrid[roundIndex];
|
||||
const round = roundIndex + 1;
|
||||
const isEvenRound = round % 2 === 0;
|
||||
const displayPicks = isEvenRound
|
||||
? [...roundPicks].toReversed()
|
||||
: roundPicks;
|
||||
|
||||
{draftSlots.map((slot) => {
|
||||
const teamTime = teamTimers[slot.team.id];
|
||||
const isAutodraft = autodraftStatus[slot.team.id]?.isEnabled ?? false;
|
||||
return (
|
||||
<div key={round} className="flex gap-1.5 items-stretch">
|
||||
<div className="w-7 flex-shrink-0 flex items-center justify-center">
|
||||
<span className="text-xs font-mono text-muted-foreground">R{round}</span>
|
||||
<div key={slot.id} className="flex-1 min-w-20 text-center">
|
||||
<div className="text-xs font-medium truncate px-1 flex items-center justify-center gap-0.5">
|
||||
{isAutodraft && (
|
||||
<span className="inline-flex items-center justify-center h-3.5 w-3.5 text-[9px] font-bold text-white bg-black shrink-0">A</span>
|
||||
)}
|
||||
<span className="truncate">{ownerMap[slot.team.id] || slot.team.name}</span>
|
||||
</div>
|
||||
<div className={`text-xs font-mono px-1 ${getTimerColorClass(teamTime)}`}>
|
||||
{formatClockTime(teamTime)}
|
||||
</div>
|
||||
{displayPicks.map((cell) => {
|
||||
const isCurrent = cell.pickNumber === currentPick;
|
||||
const isPicked = !!cell.pick;
|
||||
const cellState = isPicked
|
||||
? "picked"
|
||||
: isCurrent
|
||||
? "current"
|
||||
: "upcoming";
|
||||
const pickData = cell.pick
|
||||
? {
|
||||
participant: { name: cell.pick.participant.name },
|
||||
sport: { name: cell.pick.sport.name },
|
||||
}
|
||||
: undefined;
|
||||
|
||||
if (!isPicked && isCurrent && (onForceAutopick || onForceManualPickOpen)) {
|
||||
return (
|
||||
<ContextMenu key={cell.pickNumber}>
|
||||
<ContextMenuTrigger asChild>
|
||||
<DraftPickCell
|
||||
pickNumber={cell.pickNumber}
|
||||
round={cell.round}
|
||||
pickInRound={cell.pickInRound}
|
||||
state={cellState}
|
||||
pick={pickData}
|
||||
seasonStatus={seasonStatus}
|
||||
draftPaused={draftPaused}
|
||||
className="[&>div]:h-10 cursor-context-menu"
|
||||
/>
|
||||
</ContextMenuTrigger>
|
||||
<ContextMenuContent>
|
||||
{onForceAutopick && (
|
||||
<ContextMenuItem onClick={() => onForceAutopick(cell.pickNumber, cell.teamId)}>
|
||||
Force Auto Pick
|
||||
</ContextMenuItem>
|
||||
)}
|
||||
{onForceManualPickOpen && (
|
||||
<ContextMenuItem onClick={() => onForceManualPickOpen(cell.pickNumber, cell.teamId)}>
|
||||
Force Manual Pick
|
||||
</ContextMenuItem>
|
||||
)}
|
||||
</ContextMenuContent>
|
||||
</ContextMenu>
|
||||
);
|
||||
}
|
||||
|
||||
if (isPicked && (onReplacePick || onRollbackToPick)) {
|
||||
return (
|
||||
<ContextMenu key={cell.pickNumber}>
|
||||
<ContextMenuTrigger asChild>
|
||||
<DraftPickCell
|
||||
pickNumber={cell.pickNumber}
|
||||
round={cell.round}
|
||||
pickInRound={cell.pickInRound}
|
||||
state={cellState}
|
||||
pick={pickData}
|
||||
seasonStatus={seasonStatus}
|
||||
draftPaused={draftPaused}
|
||||
className="[&>div]:h-10 cursor-context-menu"
|
||||
/>
|
||||
</ContextMenuTrigger>
|
||||
<ContextMenuContent>
|
||||
{onReplacePick && (
|
||||
<ContextMenuItem onClick={() => onReplacePick(cell.pickNumber, cell.teamId)}>
|
||||
Replace Pick
|
||||
</ContextMenuItem>
|
||||
)}
|
||||
{onRollbackToPick && (
|
||||
<ContextMenuItem
|
||||
onClick={() => onRollbackToPick(cell.pickNumber)}
|
||||
className="text-destructive focus:text-destructive"
|
||||
>
|
||||
Roll Back to This Pick
|
||||
</ContextMenuItem>
|
||||
)}
|
||||
</ContextMenuContent>
|
||||
</ContextMenu>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<DraftPickCell
|
||||
key={cell.pickNumber}
|
||||
pickNumber={cell.pickNumber}
|
||||
round={cell.round}
|
||||
pickInRound={cell.pickInRound}
|
||||
state={cellState}
|
||||
pick={pickData}
|
||||
seasonStatus={seasonStatus}
|
||||
draftPaused={draftPaused}
|
||||
className="[&>div]:h-10"
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Rows — clipped to 2-row height, slides to reveal new round */}
|
||||
<div style={{ height: `${effectiveRowHeight * 2 + ROW_GAP}px`, overflow: "hidden" }}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: `${ROW_GAP}px`,
|
||||
transform: sliding ? `translateY(-${effectiveRowHeight + ROW_GAP}px)` : "translateY(0)",
|
||||
transition: sliding ? `transform ${ANIMATION_MS}ms ease-in-out` : "none",
|
||||
}}
|
||||
>
|
||||
{roundIndicesToRender.map((roundIndex) => {
|
||||
if (roundIndex >= draftGrid.length) return null;
|
||||
const roundPicks = draftGrid[roundIndex];
|
||||
const round = roundIndex + 1;
|
||||
const isEvenRound = round % 2 === 0;
|
||||
const displayPicks = isEvenRound ? [...roundPicks].toReversed() : roundPicks;
|
||||
|
||||
return (
|
||||
<div key={round} ref={roundIndex === displayedIndices[0] ? firstRowRef : undefined} className="flex gap-1.5 items-stretch flex-shrink-0">
|
||||
{displayPicks.map((cell) => {
|
||||
const isCurrent = cell.pickNumber === currentPick;
|
||||
const isPicked = !!cell.pick;
|
||||
const cellState = isPicked ? "picked" : isCurrent ? "current" : "upcoming";
|
||||
const pickData = cell.pick
|
||||
? {
|
||||
participant: { name: cell.pick.participant.name },
|
||||
sport: { name: cell.pick.sport.name },
|
||||
}
|
||||
: undefined;
|
||||
|
||||
if (!isPicked && isCurrent && (onForceAutopick || onForceManualPickOpen)) {
|
||||
return (
|
||||
<ContextMenu key={cell.pickNumber}>
|
||||
<ContextMenuTrigger asChild>
|
||||
<DraftPickCell
|
||||
pickNumber={cell.pickNumber}
|
||||
round={cell.round}
|
||||
pickInRound={cell.pickInRound}
|
||||
state={cellState}
|
||||
pick={pickData}
|
||||
seasonStatus={seasonStatus}
|
||||
draftPaused={draftPaused}
|
||||
className="cursor-context-menu"
|
||||
/>
|
||||
</ContextMenuTrigger>
|
||||
<ContextMenuContent>
|
||||
{onForceAutopick && (
|
||||
<ContextMenuItem onClick={() => onForceAutopick(cell.pickNumber, cell.teamId)}>
|
||||
Force Auto Pick
|
||||
</ContextMenuItem>
|
||||
)}
|
||||
{onForceManualPickOpen && (
|
||||
<ContextMenuItem onClick={() => onForceManualPickOpen(cell.pickNumber, cell.teamId)}>
|
||||
Force Manual Pick
|
||||
</ContextMenuItem>
|
||||
)}
|
||||
</ContextMenuContent>
|
||||
</ContextMenu>
|
||||
);
|
||||
}
|
||||
|
||||
if (isPicked && (onReplacePick || onRollbackToPick)) {
|
||||
return (
|
||||
<ContextMenu key={cell.pickNumber}>
|
||||
<ContextMenuTrigger asChild>
|
||||
<DraftPickCell
|
||||
pickNumber={cell.pickNumber}
|
||||
round={cell.round}
|
||||
pickInRound={cell.pickInRound}
|
||||
state={cellState}
|
||||
pick={pickData}
|
||||
seasonStatus={seasonStatus}
|
||||
draftPaused={draftPaused}
|
||||
className="cursor-context-menu"
|
||||
/>
|
||||
</ContextMenuTrigger>
|
||||
<ContextMenuContent>
|
||||
{onReplacePick && (
|
||||
<ContextMenuItem onClick={() => onReplacePick(cell.pickNumber, cell.teamId)}>
|
||||
Replace Pick
|
||||
</ContextMenuItem>
|
||||
)}
|
||||
{onRollbackToPick && (
|
||||
<ContextMenuItem
|
||||
onClick={() => onRollbackToPick(cell.pickNumber)}
|
||||
className="text-destructive focus:text-destructive"
|
||||
>
|
||||
Roll Back to This Pick
|
||||
</ContextMenuItem>
|
||||
)}
|
||||
</ContextMenuContent>
|
||||
</ContextMenu>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<DraftPickCell
|
||||
key={cell.pickNumber}
|
||||
pickNumber={cell.pickNumber}
|
||||
round={cell.round}
|
||||
pickInRound={cell.pickInRound}
|
||||
state={cellState}
|
||||
pick={pickData}
|
||||
seasonStatus={seasonStatus}
|
||||
draftPaused={draftPaused}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -539,7 +539,7 @@ export default function DraftRoom() {
|
|||
const stored = localStorage.getItem("draftSidebarCollapsed");
|
||||
return stored ? JSON.parse(stored) : false;
|
||||
});
|
||||
const [activeTab, setActiveTab] = useState<"participants" | "board" | "rosters" | "summary">("board");
|
||||
const [activeTab, setActiveTab] = useState<"participants" | "board" | "rosters" | "summary">("participants");
|
||||
const [mobileTab, setMobileTab] = useState<"available" | "queue" | "board" | "teams" | "controls">(
|
||||
!userTeam && isCommissioner ? "board" : "available"
|
||||
);
|
||||
|
|
@ -1509,13 +1509,15 @@ export default function DraftRoom() {
|
|||
currentPick,
|
||||
currentRound,
|
||||
ownerMap,
|
||||
teamTimers,
|
||||
autodraftStatus,
|
||||
seasonStatus: season.status,
|
||||
draftPaused: isPaused,
|
||||
onForceAutopick: isCommissioner ? handleForceAutopick : undefined,
|
||||
onForceManualPickOpen: isCommissioner ? handleForceManualPickOpen : undefined,
|
||||
onReplacePick: isCommissioner ? handleReplacePickOpen : undefined,
|
||||
onRollbackToPick: isCommissioner && !isDraftComplete ? handleRollbackToPick : undefined,
|
||||
}), [draftSlots, draftGrid, currentPick, currentRound, ownerMap, season.status, isPaused, isCommissioner, handleForceAutopick, handleForceManualPickOpen, handleReplacePickOpen, handleRollbackToPick, isDraftComplete]);
|
||||
}), [draftSlots, draftGrid, currentPick, currentRound, ownerMap, teamTimers, autodraftStatus, season.status, isPaused, isCommissioner, handleForceAutopick, handleForceManualPickOpen, handleReplacePickOpen, handleRollbackToPick, isDraftComplete]);
|
||||
|
||||
const availableParticipantsSectionProps = useMemo(() => ({
|
||||
participants: filteredParticipants,
|
||||
|
|
@ -1738,7 +1740,7 @@ export default function DraftRoom() {
|
|||
}
|
||||
>
|
||||
<TabsList>
|
||||
<TabsTrigger value="participants">Available Participants</TabsTrigger>
|
||||
<TabsTrigger value="participants">Participants</TabsTrigger>
|
||||
<TabsTrigger value="board">Draft Board</TabsTrigger>
|
||||
<TabsTrigger value="rosters">Rosters</TabsTrigger>
|
||||
<TabsTrigger value="summary">Summary</TabsTrigger>
|
||||
|
|
@ -1852,19 +1854,19 @@ export default function DraftRoom() {
|
|||
/>
|
||||
)}
|
||||
|
||||
<div className="flex-1 overflow-hidden">
|
||||
{activeTab === "participants" && (
|
||||
<div className="flex-1 overflow-hidden relative">
|
||||
<div className={`absolute inset-0 ${activeTab === "participants" ? "" : "hidden"}`}>
|
||||
<AvailableParticipantsSection {...availableParticipantsSectionProps} />
|
||||
)}
|
||||
{activeTab === "board" && (
|
||||
</div>
|
||||
<div className={`absolute inset-0 ${activeTab === "board" ? "" : "hidden"}`}>
|
||||
<DraftGridSection {...draftGridSectionProps} />
|
||||
)}
|
||||
{activeTab === "rosters" && (
|
||||
</div>
|
||||
<div className={`absolute inset-0 ${activeTab === "rosters" ? "" : "hidden"}`}>
|
||||
<TeamRosterView {...rosterViewProps} />
|
||||
)}
|
||||
{activeTab === "summary" && (
|
||||
</div>
|
||||
<div className={`absolute inset-0 ${activeTab === "summary" ? "" : "hidden"}`}>
|
||||
<DraftSummaryView {...summaryViewProps} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue