Move tab navigation and autodraft to header row, narrow sidebar

This commit is contained in:
Chris Parsons 2026-04-17 15:07:25 -07:00
parent 5f4b70d342
commit 27ee876fd1
4 changed files with 264 additions and 70 deletions

View file

@ -1,8 +1,9 @@
import { useState, useEffect, useRef } from "react"; import { useState, useEffect, useRef } from "react";
import { Check, Info } from "lucide-react"; import { Check, Info, ChevronDown } from "lucide-react";
import { toast } from "sonner"; import { toast } from "sonner";
import { Label } from "~/components/ui/label"; import { Label } from "~/components/ui/label";
import { Popover, PopoverContent, PopoverTrigger } from "~/components/ui/popover"; import { Popover, PopoverContent, PopoverTrigger } from "~/components/ui/popover";
import { Button } from "~/components/ui/button";
type AutodraftMode = "next_pick" | "while_on"; type AutodraftMode = "next_pick" | "while_on";
@ -90,6 +91,221 @@ export function getAutodraftLabel(
return OPTIONS[toAutodraftState(isEnabled, mode, queueOnly)].label; return OPTIONS[toAutodraftState(isEnabled, mode, queueOnly)].label;
} }
interface CompactAutodraftBadgeProps {
isEnabled: boolean;
mode: AutodraftMode;
queueOnly: boolean;
isMyTurn: boolean;
showChevron?: boolean;
}
export function CompactAutodraftBadge({
isEnabled,
mode,
queueOnly,
isMyTurn,
showChevron = false,
}: CompactAutodraftBadgeProps) {
const state = toAutodraftState(isEnabled, mode, queueOnly);
const { label } = OPTIONS[state];
const isOff = state === "off";
return (
<div className="flex items-center gap-2">
<span className="text-xs text-muted-foreground">Autodraft:</span>
<span
className={`text-xs font-medium px-2 py-1 rounded flex items-center gap-1 ${
isOff
? "bg-muted text-muted-foreground border border-border"
: "bg-electric/20 text-electric border border-electric/30"
}`}
>
{label}
{showChevron && <ChevronDown className="h-3 w-3" />}
</span>
{isMyTurn && (
<span className="text-xs text-amber-accent font-medium">Your turn!</span>
)}
</div>
);
}
interface AutodraftBadgeWithPopoverProps {
seasonId: string;
teamId: string;
isEnabled: boolean;
mode: AutodraftMode;
queueOnly: boolean;
isMyTurn: boolean;
onUpdate: (isEnabled: boolean, mode: AutodraftMode, queueOnly: boolean) => void;
}
function AutodraftOptions({
seasonId,
teamId,
isEnabled,
mode,
queueOnly,
isMyTurn,
onUpdate,
}: Omit<AutodraftBadgeWithPopoverProps, "seasonId" | "teamId"> & {
seasonId: string;
teamId: string;
}) {
const [localState, setLocalState] = useState<AutodraftState>(
toAutodraftState(isEnabled, mode, queueOnly)
);
const abortRef = useRef<AbortController | null>(null);
useEffect(() => {
setLocalState(toAutodraftState(isEnabled, mode, queueOnly));
}, [isEnabled, mode, queueOnly]);
const sendUpdate = async (newState: AutodraftState) => {
abortRef.current?.abort();
abortRef.current = new AbortController();
const option = OPTIONS[newState];
try {
const formData = new FormData();
formData.append("seasonId", seasonId);
formData.append("teamId", teamId);
formData.append("isEnabled", option.isEnabled.toString());
formData.append("mode", option.mode);
formData.append("queueOnly", option.queueOnly.toString());
const response = await fetch("/api/autodraft/update", {
method: "POST",
body: formData,
signal: abortRef.current.signal,
});
if (response.ok) {
onUpdate(option.isEnabled, option.mode, option.queueOnly);
toast.success(
option.isEnabled ? `Autodraft set to "${option.label}"` : "Autodraft turned off"
);
} else {
setLocalState(toAutodraftState(isEnabled, mode, queueOnly));
toast.error("Failed to save autodraft settings");
}
} catch (error) {
if (error instanceof Error && error.name === "AbortError") return;
setLocalState(toAutodraftState(isEnabled, mode, queueOnly));
toast.error("Failed to save autodraft settings");
}
};
const handleStateChange = (newState: AutodraftState) => {
if (newState === localState || isMyTurn) return;
setLocalState(newState);
sendUpdate(newState);
};
const isDisabled = isMyTurn;
return (
<div className="flex flex-col rounded-lg border overflow-hidden">
{OPTION_ORDER.map((state) => {
const { label } = OPTIONS[state];
const isActive = localState === state;
return (
<button
key={state}
type="button"
disabled={isDisabled}
onClick={() => handleStateChange(state)}
className={`w-full py-2.5 px-3 text-left transition-colors border-b last:border-b-0 flex items-center justify-between gap-2 ${getButtonClassName(state, isActive)} ${
isDisabled ? "opacity-50 cursor-not-allowed" : "cursor-pointer"
}`}
>
<span className="text-xs font-medium">{label}</span>
{isActive && (
<Check className={`h-3.5 w-3.5 shrink-0 ${getCheckClassName(state)}`} />
)}
</button>
);
})}
{isMyTurn && (
<p className="text-xs text-muted-foreground mt-2 text-center px-3 py-2">
You're on the clock!
</p>
)}
</div>
);
}
export function AutodraftBadgeWithPopover({
seasonId,
teamId,
isEnabled,
mode,
queueOnly,
isMyTurn,
onUpdate,
}: AutodraftBadgeWithPopoverProps) {
const [open, setOpen] = useState(false);
return (
<div className="flex items-center gap-2">
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<button
type="button"
className="hover:bg-muted/40 rounded px-2 py-1 -mx-2 -my-1 transition-colors"
>
<CompactAutodraftBadge
isEnabled={isEnabled}
mode={mode}
queueOnly={queueOnly}
isMyTurn={isMyTurn}
showChevron
/>
</button>
</PopoverTrigger>
<PopoverContent className="w-72 p-0" align="end">
<AutodraftOptions
seasonId={seasonId}
teamId={teamId}
isEnabled={isEnabled}
mode={mode}
queueOnly={queueOnly}
isMyTurn={isMyTurn}
onUpdate={onUpdate}
/>
</PopoverContent>
</Popover>
<Popover>
<PopoverTrigger asChild>
<button
type="button"
className="text-muted-foreground hover:text-foreground transition-colors"
aria-label="Autodraft options explained"
>
<Info className="h-3.5 w-3.5" />
</button>
</PopoverTrigger>
<PopoverContent className="w-72" align="end">
<p className="text-sm font-semibold mb-2.5">Autodraft Options</p>
<div className="space-y-2.5">
{OPTION_ORDER.map((state) => {
const { label, desc } = OPTIONS[state];
return (
<div key={state}>
<p className="text-xs font-medium">{label}</p>
<p className="text-xs text-muted-foreground">{desc}</p>
</div>
);
})}
</div>
</PopoverContent>
</Popover>
</div>
);
}
interface AutodraftSettingsProps { interface AutodraftSettingsProps {
seasonId: string; seasonId: string;
teamId: string; teamId: string;

View file

@ -64,7 +64,7 @@ export function DraftSidebar({
<div <div
className={cn( className={cn(
"relative flex-shrink-0 bg-card border-r border-border transition-all duration-300 flex flex-col", "relative flex-shrink-0 bg-card border-r border-border transition-all duration-300 flex flex-col",
"w-[450px]", "w-[300px]",
// Mobile: fixed overlay, Desktop: normal sidebar // Mobile: fixed overlay, Desktop: normal sidebar
"fixed inset-y-0 left-0 z-50 lg:relative lg:z-auto", "fixed inset-y-0 left-0 z-50 lg:relative lg:z-auto",
className className

View file

@ -2,7 +2,6 @@ import { memo, useCallback, useMemo } from "react";
import { GripVertical } from "lucide-react"; import { GripVertical } from "lucide-react";
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 { AutodraftSettings } from "~/components/AutodraftSettings";
import { import {
DndContext, DndContext,
closestCenter, closestCenter,
@ -31,17 +30,9 @@ interface QueueSectionProps {
name: string; name: string;
sport: { name: string }; sport: { name: string };
}>; }>;
seasonId: string;
teamId: string;
isMyTurn: boolean; isMyTurn: boolean;
canPick: boolean; canPick: boolean;
userAutodraft: {
isEnabled: boolean;
mode: "next_pick" | "while_on";
queueOnly: boolean;
};
onRemoveFromQueue: (queueId: string) => void; onRemoveFromQueue: (queueId: string) => void;
onAutodraftUpdate: (isEnabled: boolean, mode: "next_pick" | "while_on", queueOnly: boolean) => void;
onReorder: (participantIds: string[]) => void; onReorder: (participantIds: string[]) => void;
onMakePick?: (participantId: string) => void; onMakePick?: (participantId: string) => void;
} }
@ -131,13 +122,9 @@ const SortableQueueItem = memo(function SortableQueueItem({
export const QueueSection = memo(function QueueSection({ export const QueueSection = memo(function QueueSection({
queue, queue,
availableParticipants, availableParticipants,
seasonId,
teamId,
isMyTurn, isMyTurn,
canPick, canPick,
userAutodraft,
onRemoveFromQueue, onRemoveFromQueue,
onAutodraftUpdate,
onReorder, onReorder,
onMakePick, onMakePick,
}: QueueSectionProps) { }: QueueSectionProps) {
@ -187,7 +174,7 @@ export const QueueSection = memo(function QueueSection({
items={queueIds} items={queueIds}
strategy={verticalListSortingStrategy} strategy={verticalListSortingStrategy}
> >
<div className="space-y-1.5 mb-4"> <div className="space-y-1.5">
{queue.map((item, index) => { {queue.map((item, index) => {
const participant = participantMap.get(item.participantId); const participant = participantMap.get(item.participantId);
return ( return (
@ -207,17 +194,6 @@ export const QueueSection = memo(function QueueSection({
</SortableContext> </SortableContext>
</DndContext> </DndContext>
)} )}
{/* Autodraft Settings */}
<AutodraftSettings
seasonId={seasonId}
teamId={teamId}
isEnabled={userAutodraft.isEnabled}
mode={userAutodraft.mode}
queueOnly={userAutodraft.queueOnly}
isMyTurn={isMyTurn}
onUpdate={onAutodraftUpdate}
/>
</div> </div>
); );
}); });

View file

@ -27,7 +27,7 @@ import { getTeamForPick } from "~/lib/draft-order";
import { useDraftNotifications } from "~/hooks/useDraftNotifications"; import { useDraftNotifications } from "~/hooks/useDraftNotifications";
import { useMediaQuery } from "~/hooks/useMediaQuery"; import { useMediaQuery } from "~/hooks/useMediaQuery";
import { NotificationSettings } from "~/components/NotificationSettings"; import { NotificationSettings } from "~/components/NotificationSettings";
import { AutodraftSettings, getAutodraftLabel } from "~/components/AutodraftSettings"; import { AutodraftSettings, getAutodraftLabel, AutodraftBadgeWithPopover } from "~/components/AutodraftSettings";
import { toast } from "sonner"; import { toast } from "sonner";
import { formatClockTime, getTimerColorClass } from "~/lib/draft-timer"; import { formatClockTime, getTimerColorClass } from "~/lib/draft-timer";
import { Users, LayoutGrid, ListChecks, Settings, ListOrdered } from "lucide-react"; import { Users, LayoutGrid, ListChecks, Settings, ListOrdered } from "lucide-react";
@ -1581,13 +1581,9 @@ export default function DraftRoom() {
? { ? {
queue, queue,
availableParticipants, availableParticipants,
seasonId: season.id,
teamId: userTeam.id,
isMyTurn, isMyTurn,
canPick, canPick,
userAutodraft,
onRemoveFromQueue: handleRemoveFromQueue, onRemoveFromQueue: handleRemoveFromQueue,
onAutodraftUpdate: handleAutodraftUpdate,
onReorder: handleReorderQueue, onReorder: handleReorderQueue,
onMakePick: handleMakePick, onMakePick: handleMakePick,
} }
@ -1723,6 +1719,38 @@ export default function DraftRoom() {
</div> </div>
)} )}
{/* Desktop Tab Navigation Row */}
<div className={`hidden md:flex flex-shrink-0 items-center justify-between px-4 py-2 border-b transition-all duration-300 ${
isMyTurn && season.status === "draft" && !isDraftComplete
? "bg-electric/25 border-electric shadow-[0_0_24px_0_rgb(0_200_255_/_0.18)]"
: "bg-card"
}`}>
<Tabs
value={activeTab}
onValueChange={(value) =>
setActiveTab(value as "participants" | "board" | "rosters" | "summary")
}
>
<TabsList>
<TabsTrigger value="participants">Available Participants</TabsTrigger>
<TabsTrigger value="board">Draft Board</TabsTrigger>
<TabsTrigger value="rosters">Rosters</TabsTrigger>
<TabsTrigger value="summary">Summary</TabsTrigger>
</TabsList>
</Tabs>
{userTeam && (
<AutodraftBadgeWithPopover
seasonId={season.id}
teamId={userTeam.id}
isEnabled={userAutodraft.isEnabled}
mode={userAutodraft.mode}
queueOnly={userAutodraft.queueOnly}
isMyTurn={isMyTurn}
onUpdate={handleAutodraftUpdate}
/>
)}
</div>
{/* Main Content — single layout tree based on isMobile to avoid duplicate component instances */} {/* Main Content — single layout tree based on isMobile to avoid duplicate component instances */}
<div className="flex-1 overflow-hidden"> <div className="flex-1 overflow-hidden">
{isMobile ? ( {isMobile ? (
@ -1819,44 +1847,18 @@ export default function DraftRoom() {
)} )}
<div className="flex-1 overflow-hidden"> <div className="flex-1 overflow-hidden">
<Tabs {activeTab === "participants" && (
value={activeTab} <AvailableParticipantsSection {...availableParticipantsSectionProps} />
onValueChange={(value) => )}
setActiveTab(value as "participants" | "board" | "rosters" | "summary") {activeTab === "board" && (
} <DraftGridSection {...draftGridSectionProps} />
className="h-full flex flex-col" )}
> {activeTab === "rosters" && (
<div className={`mt-4 transition-all duration-300 ${ <TeamRosterView {...rosterViewProps} />
isMyTurn && season.status === "draft" && !isDraftComplete )}
? "bg-electric/25 border-y-2 border-electric shadow-[0_0_24px_0_rgb(0_200_255_/_0.18)]" {activeTab === "summary" && (
: "" <DraftSummaryView {...summaryViewProps} />
}`}> )}
<div className="flex items-center gap-3 px-4 py-2">
<TabsList>
<TabsTrigger value="participants">Available Participants</TabsTrigger>
<TabsTrigger value="board">Draft Board</TabsTrigger>
<TabsTrigger value="rosters">Rosters</TabsTrigger>
<TabsTrigger value="summary">Summary</TabsTrigger>
</TabsList>
</div>
</div>
<TabsContent value="participants" className="flex-1 overflow-hidden m-0">
<AvailableParticipantsSection {...availableParticipantsSectionProps} />
</TabsContent>
<TabsContent value="board" className="flex-1 overflow-hidden m-0">
<DraftGridSection {...draftGridSectionProps} />
</TabsContent>
<TabsContent value="rosters" className="flex-1 overflow-hidden m-0">
<TeamRosterView {...rosterViewProps} />
</TabsContent>
<TabsContent value="summary" className="flex-1 overflow-hidden m-0">
<DraftSummaryView {...summaryViewProps} />
</TabsContent>
</Tabs>
</div> </div>
</div> </div>
)} )}