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 { 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;
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue