Move tab navigation and autodraft to header row, narrow sidebar
This commit is contained in:
parent
5f4b70d342
commit
27ee876fd1
4 changed files with 264 additions and 70 deletions
|
|
@ -1,8 +1,9 @@
|
|||
import { useState, useEffect, useRef } from "react";
|
||||
import { Check, Info } from "lucide-react";
|
||||
import { Check, Info, ChevronDown } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { Label } from "~/components/ui/label";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "~/components/ui/popover";
|
||||
import { Button } from "~/components/ui/button";
|
||||
|
||||
type AutodraftMode = "next_pick" | "while_on";
|
||||
|
||||
|
|
@ -90,6 +91,221 @@ export function getAutodraftLabel(
|
|||
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 {
|
||||
seasonId: string;
|
||||
teamId: string;
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ export function DraftSidebar({
|
|||
<div
|
||||
className={cn(
|
||||
"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
|
||||
"fixed inset-y-0 left-0 z-50 lg:relative lg:z-auto",
|
||||
className
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ import { memo, useCallback, useMemo } from "react";
|
|||
import { GripVertical } from "lucide-react";
|
||||
import { Button } from "~/components/ui/button";
|
||||
import { Badge } from "~/components/ui/badge";
|
||||
import { AutodraftSettings } from "~/components/AutodraftSettings";
|
||||
import {
|
||||
DndContext,
|
||||
closestCenter,
|
||||
|
|
@ -31,17 +30,9 @@ interface QueueSectionProps {
|
|||
name: string;
|
||||
sport: { name: string };
|
||||
}>;
|
||||
seasonId: string;
|
||||
teamId: string;
|
||||
isMyTurn: boolean;
|
||||
canPick: boolean;
|
||||
userAutodraft: {
|
||||
isEnabled: boolean;
|
||||
mode: "next_pick" | "while_on";
|
||||
queueOnly: boolean;
|
||||
};
|
||||
onRemoveFromQueue: (queueId: string) => void;
|
||||
onAutodraftUpdate: (isEnabled: boolean, mode: "next_pick" | "while_on", queueOnly: boolean) => void;
|
||||
onReorder: (participantIds: string[]) => void;
|
||||
onMakePick?: (participantId: string) => void;
|
||||
}
|
||||
|
|
@ -131,13 +122,9 @@ const SortableQueueItem = memo(function SortableQueueItem({
|
|||
export const QueueSection = memo(function QueueSection({
|
||||
queue,
|
||||
availableParticipants,
|
||||
seasonId,
|
||||
teamId,
|
||||
isMyTurn,
|
||||
canPick,
|
||||
userAutodraft,
|
||||
onRemoveFromQueue,
|
||||
onAutodraftUpdate,
|
||||
onReorder,
|
||||
onMakePick,
|
||||
}: QueueSectionProps) {
|
||||
|
|
@ -187,7 +174,7 @@ export const QueueSection = memo(function QueueSection({
|
|||
items={queueIds}
|
||||
strategy={verticalListSortingStrategy}
|
||||
>
|
||||
<div className="space-y-1.5 mb-4">
|
||||
<div className="space-y-1.5">
|
||||
{queue.map((item, index) => {
|
||||
const participant = participantMap.get(item.participantId);
|
||||
return (
|
||||
|
|
@ -207,17 +194,6 @@ export const QueueSection = memo(function QueueSection({
|
|||
</SortableContext>
|
||||
</DndContext>
|
||||
)}
|
||||
|
||||
{/* Autodraft Settings */}
|
||||
<AutodraftSettings
|
||||
seasonId={seasonId}
|
||||
teamId={teamId}
|
||||
isEnabled={userAutodraft.isEnabled}
|
||||
mode={userAutodraft.mode}
|
||||
queueOnly={userAutodraft.queueOnly}
|
||||
isMyTurn={isMyTurn}
|
||||
onUpdate={onAutodraftUpdate}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ import { getTeamForPick } from "~/lib/draft-order";
|
|||
import { useDraftNotifications } from "~/hooks/useDraftNotifications";
|
||||
import { useMediaQuery } from "~/hooks/useMediaQuery";
|
||||
import { NotificationSettings } from "~/components/NotificationSettings";
|
||||
import { AutodraftSettings, getAutodraftLabel } from "~/components/AutodraftSettings";
|
||||
import { AutodraftSettings, getAutodraftLabel, AutodraftBadgeWithPopover } from "~/components/AutodraftSettings";
|
||||
import { toast } from "sonner";
|
||||
import { formatClockTime, getTimerColorClass } from "~/lib/draft-timer";
|
||||
import { Users, LayoutGrid, ListChecks, Settings, ListOrdered } from "lucide-react";
|
||||
|
|
@ -1581,13 +1581,9 @@ export default function DraftRoom() {
|
|||
? {
|
||||
queue,
|
||||
availableParticipants,
|
||||
seasonId: season.id,
|
||||
teamId: userTeam.id,
|
||||
isMyTurn,
|
||||
canPick,
|
||||
userAutodraft,
|
||||
onRemoveFromQueue: handleRemoveFromQueue,
|
||||
onAutodraftUpdate: handleAutodraftUpdate,
|
||||
onReorder: handleReorderQueue,
|
||||
onMakePick: handleMakePick,
|
||||
}
|
||||
|
|
@ -1723,6 +1719,38 @@ export default function DraftRoom() {
|
|||
</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 */}
|
||||
<div className="flex-1 overflow-hidden">
|
||||
{isMobile ? (
|
||||
|
|
@ -1819,44 +1847,18 @@ export default function DraftRoom() {
|
|||
)}
|
||||
|
||||
<div className="flex-1 overflow-hidden">
|
||||
<Tabs
|
||||
value={activeTab}
|
||||
onValueChange={(value) =>
|
||||
setActiveTab(value as "participants" | "board" | "rosters" | "summary")
|
||||
}
|
||||
className="h-full flex flex-col"
|
||||
>
|
||||
<div className={`mt-4 transition-all duration-300 ${
|
||||
isMyTurn && season.status === "draft" && !isDraftComplete
|
||||
? "bg-electric/25 border-y-2 border-electric shadow-[0_0_24px_0_rgb(0_200_255_/_0.18)]"
|
||||
: ""
|
||||
}`}>
|
||||
<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>
|
||||
{activeTab === "participants" && (
|
||||
<AvailableParticipantsSection {...availableParticipantsSectionProps} />
|
||||
)}
|
||||
{activeTab === "board" && (
|
||||
<DraftGridSection {...draftGridSectionProps} />
|
||||
)}
|
||||
{activeTab === "rosters" && (
|
||||
<TeamRosterView {...rosterViewProps} />
|
||||
)}
|
||||
{activeTab === "summary" && (
|
||||
<DraftSummaryView {...summaryViewProps} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue