Make draft room fully usable on mobile (#30)

- Add bottom nav bar (Lobby/Board/Roster/Controls) with 56px touch targets
- Add mobile card list view in AvailableParticipantsSection with 44px Draft/Queue buttons
- Add round labels and sticky header row to DraftGridSection grid
- Add mobile Sheet for commissioner actions in DraftGridSection (replaces right-click context menu)
- Use useMediaQuery to render a single layout tree, eliminating duplicate component
  instances (previously both desktop and mobile layouts were always in the DOM)
- Add compact header on mobile (text-lg vs text-3xl); hide commissioner buttons
  and Exit link in header on mobile — surfaced in Controls tab instead
- Add "On Clock" bar on mobile showing current team and timer during active draft
- Add "your turn" indicator dot on Lobby bottom nav tab
- Default mobile tab to "board" for commissioner-only viewers
- Fix sticky corner cell z-index in TeamsDraftedGrid (z-10 → z-20)
- Fix round column spacer in DraftGridSection header not being sticky-left
- Extract shared prop objects to eliminate copy-paste between layouts
- Extract getParticipantState() helper to deduplicate participant render logic
- Move MobileSheetData type and MOBILE_TABS array to module scope
- Remove unnecessary non-null assertions in DraftGridSection Sheet handlers

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Chris Parsons 2026-02-22 22:36:12 -08:00 committed by GitHub
parent 01f1480159
commit 77e408cad8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 550 additions and 174 deletions

View file

@ -2,6 +2,24 @@ import { Button } from "~/components/ui/button";
import { Badge } from "~/components/ui/badge";
import { ListPlus, ListX } from "lucide-react";
function getParticipantState(
participant: { id: string; sport: { id: string } },
draftedParticipantIds: Set<string>,
queue: Array<{ participantId: string }>,
eligibility: {
eligibleSportIds: Set<string>;
ineligibleReasons: Record<string, string>;
} | null
) {
const isDrafted = draftedParticipantIds.has(participant.id);
const isInQueue = queue.some((item) => item.participantId === participant.id);
const isEligible = eligibility
? eligibility.eligibleSportIds.has(participant.sport.id)
: true;
const ineligibleReason = eligibility?.ineligibleReasons[participant.sport.id];
return { isDrafted, isInQueue, isEligible, ineligibleReason };
}
interface AvailableParticipantsSectionProps {
participants: Array<{
id: string;
@ -66,12 +84,12 @@ export function AvailableParticipantsSection({
className="w-full px-3 py-2 border rounded-md text-sm bg-background text-foreground"
/>
<div className="flex gap-3">
<div className="flex flex-wrap gap-2">
{/* Sport Filter Dropdown */}
<select
value={sportFilter}
onChange={(e) => onSportFilterChange(e.target.value)}
className="flex-1 px-3 py-2 border rounded-md text-sm bg-background text-foreground"
className="flex-1 min-w-[120px] px-3 py-2 border rounded-md text-sm bg-background text-foreground"
>
<option value="all">All Sports</option>
{uniqueSports.map((sport) => (
@ -108,9 +126,117 @@ export function AvailableParticipantsSection({
</div>
</div>
{/* Table - This will scroll independently */}
<div className="flex-1 overflow-hidden">
<div className="h-full overflow-y-auto">
{/* Mobile Card List - visible on mobile only */}
<div className="flex md:hidden flex-1 overflow-y-auto flex-col gap-2 px-4 py-2">
{participants.length === 0 ? (
<div className="text-center py-8 text-muted-foreground text-sm">
No participants found
</div>
) : (
participants.map((participant) => {
const { isDrafted, isInQueue, isEligible, ineligibleReason } =
getParticipantState(participant, draftedParticipantIds, queue, eligibility);
return (
<div
key={participant.id}
className={`bg-card border rounded-lg p-3 flex items-start justify-between gap-3 transition-colors ${
isDrafted
? "bg-muted/50 opacity-60"
: !isEligible
? "bg-destructive/10 opacity-75"
: ""
}`}
>
<div className="flex flex-col gap-1 min-w-0">
<span
className={`font-semibold text-sm truncate ${!isEligible && !isDrafted ? "text-muted-foreground" : ""}`}
>
{participant.name}
</span>
<div className="flex flex-wrap gap-1 items-center">
<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 && !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 queueItem = queue.find(
(item) => item.participantId === participant.id
);
if (queueItem) {
onRemoveFromQueue(queueItem.id);
}
}}
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>
@ -133,17 +259,8 @@ export function AvailableParticipantsSection({
</tr>
) : (
participants.map((participant) => {
const isDrafted = draftedParticipantIds.has(participant.id);
const isInQueue = queue.some(
(item) => item.participantId === participant.id
);
// Check eligibility
const isEligible = eligibility
? eligibility.eligibleSportIds.has(participant.sport.id)
: true;
const ineligibleReason =
eligibility?.ineligibleReasons[participant.sport.id];
const { isDrafted, isInQueue, isEligible, ineligibleReason } =
getParticipantState(participant, draftedParticipantIds, queue, eligibility);
return (
<tr

View file

@ -1,12 +1,25 @@
import { useMemo } from "react";
import { useMemo, useState } from "react";
import {
ContextMenu,
ContextMenuContent,
ContextMenuItem,
ContextMenuTrigger,
} from "~/components/ui/context-menu";
import {
Sheet,
SheetContent,
SheetHeader,
SheetTitle,
} from "~/components/ui/sheet";
import { Button } from "~/components/ui/button";
import { MoreVertical } from "lucide-react";
import { formatClockTime, getTimerColorClass } from "~/lib/draft-timer";
type MobileSheetData =
| { type: "team"; teamId: string }
| { type: "current-cell"; pickNumber: number; teamId: string }
| { type: "picked-cell"; pickNumber: number; teamId: string };
interface DraftGridSectionProps {
draftSlots: Array<{
id: string;
@ -60,6 +73,8 @@ export function DraftGridSection({
onRollbackToPick,
ownerMap = {},
}: DraftGridSectionProps) {
const [mobileSheet, setMobileSheet] = useState<MobileSheetData | null>(null);
const currentTeamId = useMemo(
() => draftGrid.flat().find((c) => c.pickNumber === currentPick)?.teamId ?? null,
[draftGrid, currentPick]
@ -69,16 +84,18 @@ export function DraftGridSection({
<div className="h-full overflow-auto p-4">
<h2 className="text-xl font-semibold mb-4">Draft Grid</h2>
<div className="overflow-x-auto">
<div className="w-full">
<div className="inline-block min-w-full">
{/* Team Headers */}
<div className="flex gap-2 mb-2">
<div className="flex gap-2 mb-2 sticky top-0 z-10 bg-background/95 backdrop-blur-sm py-1">
{/* Spacer for round column — sticky so it covers the corner when scrolling both axes */}
<div className="w-8 flex-shrink-0 sticky left-0 z-[6] bg-background/95" />
{draftSlots.map((slot) => {
const teamTime = teamTimers[slot.team.id];
const isAutodraft = autodraftStatus[slot.team.id] || false;
const isConnected = connectedTeams.has(slot.team.id);
const headerInner = (
<>
<div className="relative">
<div
className={`font-semibold text-sm truncate px-2 ${
slot.team.id === currentTeamId
@ -91,7 +108,7 @@ export function DraftGridSection({
{ownerMap[slot.team.id] || slot.team.name}
</div>
<div
className={`text-xs font-mono ${getTimerColorClass(teamTime)}`}
className={`text-xs font-mono px-2 ${getTimerColorClass(teamTime)}`}
>
{formatClockTime(teamTime)}
{isAutodraft && (
@ -100,7 +117,15 @@ export function DraftGridSection({
</span>
)}
</div>
</>
{isCommissioner && onAdjustTimeBankOpen && (
<button
className="absolute top-0 right-0 md:hidden p-1 min-w-[32px] min-h-[32px] flex items-center justify-center rounded hover:bg-muted"
onClick={() => setMobileSheet({ type: "team", teamId: slot.team.id })}
>
<MoreVertical className="h-3.5 w-3.5" />
</button>
)}
</div>
);
if (isCommissioner && onAdjustTimeBankOpen) {
@ -140,12 +165,16 @@ export function DraftGridSection({
: roundPicks;
return (
<div key={roundIndex} className="flex gap-2">
<div key={roundIndex} className="flex gap-2 items-stretch">
{/* Round label */}
<div className="w-8 flex-shrink-0 sticky left-0 z-[5] bg-background/95 flex items-center justify-center">
<span className="text-xs font-mono text-muted-foreground">R{round}</span>
</div>
{displayPicks.map((cell) => {
const isCurrent = cell.pickNumber === currentPick;
const isPicked = !!cell.pick;
const cellClassName = `flex-1 min-w-32 h-20 border-2 rounded-lg p-2 transition-all ${
const cellClassName = `relative flex-1 min-w-32 h-20 border-2 rounded-lg p-2 transition-all ${
isCurrent
? "border-electric bg-electric/20 shadow-lg shadow-electric/25 ring-1 ring-electric/40"
: isPicked
@ -173,6 +202,23 @@ export function DraftGridSection({
On Clock
</div>
) : null}
{/* Mobile commissioner button */}
{isCommissioner && !isPicked && isCurrent && (
<button
className="absolute top-1 right-1 md:hidden p-1 min-w-[32px] min-h-[32px] flex items-center justify-center rounded hover:bg-muted"
onClick={() => setMobileSheet({ type: "current-cell", pickNumber: cell.pickNumber, teamId: cell.teamId })}
>
<MoreVertical className="h-3.5 w-3.5" />
</button>
)}
{isCommissioner && isPicked && (onReplacePick || onRollbackToPick) && (
<button
className="absolute top-1 right-1 md:hidden p-1 min-w-[32px] min-h-[32px] flex items-center justify-center rounded hover:bg-muted"
onClick={() => setMobileSheet({ type: "picked-cell", pickNumber: cell.pickNumber, teamId: cell.teamId })}
>
<MoreVertical className="h-3.5 w-3.5" />
</button>
)}
</>
);
@ -262,6 +308,73 @@ export function DraftGridSection({
</div>
</div>
</div>
{/* Mobile Commissioner Sheet */}
<Sheet open={!!mobileSheet} onOpenChange={(open) => !open && setMobileSheet(null)}>
<SheetContent side="bottom" className="pb-8">
<SheetHeader>
<SheetTitle>Commissioner Actions</SheetTitle>
</SheetHeader>
<div className="flex flex-col gap-3 p-4">
{mobileSheet?.type === "team" && onAdjustTimeBankOpen && (
<Button
onClick={() => {
onAdjustTimeBankOpen(mobileSheet.teamId);
setMobileSheet(null);
}}
>
Adjust Time Bank...
</Button>
)}
{mobileSheet?.type === "current-cell" && (
<>
<Button
onClick={() => {
onForceAutopick(mobileSheet.pickNumber, mobileSheet.teamId);
setMobileSheet(null);
}}
>
Force Auto Pick
</Button>
<Button
variant="outline"
onClick={() => {
onForceManualPickOpen(mobileSheet.pickNumber, mobileSheet.teamId);
setMobileSheet(null);
}}
>
Force Manual Pick
</Button>
</>
)}
{mobileSheet?.type === "picked-cell" && (
<>
{onReplacePick && (
<Button
onClick={() => {
onReplacePick(mobileSheet.pickNumber, mobileSheet.teamId);
setMobileSheet(null);
}}
>
Replace Pick
</Button>
)}
{onRollbackToPick && (
<Button
variant="destructive"
onClick={() => {
onRollbackToPick(mobileSheet.pickNumber);
setMobileSheet(null);
}}
>
Roll Back to This Pick
</Button>
)}
</>
)}
</div>
</SheetContent>
</Sheet>
</div>
);
}

View file

@ -86,7 +86,7 @@ export function TeamsDraftedGrid({
<table className="w-full border-collapse">
<thead className="sticky top-0 bg-background z-10">
<tr>
<th className="border-r border-b border-border p-2 text-left font-semibold min-w-[150px] bg-muted/50">
<th className="border-r border-b border-border p-2 text-left font-semibold min-w-[150px] bg-muted/50 sticky left-0 z-20">
Sport
</th>
{draftSlots.map((slot, index) => {
@ -115,7 +115,7 @@ export function TeamsDraftedGrid({
const isLastRow = sportIndex === sports.length - 1;
return (
<tr key={sport.id}>
<td className={`border-r border-border p-2 font-medium bg-muted/30 ${!isLastRow ? 'border-b' : ''}`}>
<td className={`border-r border-border p-2 font-medium bg-muted/30 sticky left-0 z-10 ${!isLastRow ? 'border-b' : ''}`}>
{sport.name}
</td>
{draftSlots.map((slot, slotIndex) => {

View file

@ -0,0 +1,15 @@
import { useState, useEffect } from "react";
export function useMediaQuery(query: string): boolean {
const [matches, setMatches] = useState(false);
useEffect(() => {
const mq = window.matchMedia(query);
setMatches(mq.matches);
const handler = (e: MediaQueryListEvent) => setMatches(e.matches);
mq.addEventListener("change", handler);
return () => mq.removeEventListener("change", handler);
}, [query]);
return matches;
}

View file

@ -22,11 +22,20 @@ import { ConnectionOverlay } from "~/components/draft/ConnectionOverlay";
import { calculateDraftEligibility } from "~/lib/draft-eligibility";
import { getTeamForPick } from "~/lib/draft-order";
import { useDraftNotifications } from "~/hooks/useDraftNotifications";
import { useMediaQuery } from "~/hooks/useMediaQuery";
import { NotificationSettings } from "~/components/NotificationSettings";
import { toast } from "sonner";
import { formatClockTime, getTimerColorClass } from "~/lib/draft-timer";
import { Users, LayoutGrid, ListChecks, Settings } from "lucide-react";
import type { Route } from "./+types/$leagueId.draft.$seasonId";
const MOBILE_TABS = [
{ id: "lobby" as const, label: "Lobby", Icon: Users },
{ id: "board" as const, label: "Board", Icon: LayoutGrid },
{ id: "roster" as const, label: "Roster", Icon: ListChecks },
{ id: "controls" as const, label: "Controls", Icon: Settings },
];
export async function loader(args: Route.LoaderArgs) {
const { params } = args;
const { seasonId, leagueId } = params;
@ -242,6 +251,10 @@ export default function DraftRoom() {
return stored ? JSON.parse(stored) : false;
});
const [activeTab, setActiveTab] = useState<"participants" | "board" | "teams">("participants");
const [mobileTab, setMobileTab] = useState<"lobby" | "board" | "roster" | "controls">(
!userTeam && isCommissioner ? "board" : "lobby"
);
const isMobile = useMediaQuery("(max-width: 767px)");
// Track autodraft status for all teams
const [autodraftStatus, setAutodraftStatus] = useState<Record<string, boolean>>(() => {
@ -880,6 +893,13 @@ export default function DraftRoom() {
}
};
const handleForceManualPickOpen = (pickNumber: number, teamId: string) => {
setSelectedPickSlot({ pickNumber, teamId });
setDialogSearchQuery("");
setDialogSportFilter("all");
setForcePickDialogOpen(true);
};
// Calculate current round
const currentRound = draftSlots.length > 0 ? Math.ceil(currentPick / draftSlots.length) : 1;
@ -1008,6 +1028,69 @@ export default function DraftRoom() {
[availableParticipants, hideDrafted, hideIneligible, eligibility, draftedParticipantIds, searchQuery, sportFilter]
);
// Shared component props — defined once to avoid duplication between desktop and mobile layouts
const availableParticipantsSectionProps = {
participants: filteredParticipants,
searchQuery,
sportFilter,
hideDrafted,
hideIneligible,
uniqueSports,
draftedParticipantIds,
queue,
eligibility,
canPick,
hasTeam: !!userTeam,
onSearchChange: setSearchQuery,
onSportFilterChange: setSportFilter,
onHideDraftedChange: setHideDrafted,
onHideIneligibleChange: setHideIneligible,
onMakePick: handleMakePick,
onAddToQueue: handleAddToQueue,
onRemoveFromQueue: handleRemoveFromQueue,
};
const draftGridSectionProps = {
draftSlots,
draftGrid,
currentPick,
teamTimers,
autodraftStatus,
connectedTeams,
isCommissioner,
ownerMap,
onAdjustTimeBankOpen: isCommissioner ? handleAdjustTimeBankOpen : undefined,
onForceAutopick: handleForceAutopick,
onForceManualPickOpen: handleForceManualPickOpen,
onReplacePick: isCommissioner ? handleReplacePickOpen : undefined,
onRollbackToPick: isCommissioner && !isDraftComplete ? handleRollbackToPick : undefined,
};
const teamsDraftedGridProps = {
draftSlots,
picks,
sports: seasonSportsData,
season: { numFlexPicks },
ownerMap,
};
const queueSectionProps = userTeam
? {
queue,
availableParticipants,
eligibility,
seasonId: season.id,
teamId: userTeam.id,
isMyTurn,
userAutodraft,
onRemoveFromQueue: handleRemoveFromQueue,
onAutodraftUpdate: (isEnabled: boolean, mode: "next_pick" | "while_on") => {
setUserAutodraft({ isEnabled, mode });
},
onReorder: handleReorderQueue,
}
: null;
return (
<div className="h-screen bg-background flex flex-col overflow-hidden">
{/* Draft Completion Banner */}
@ -1060,7 +1143,7 @@ export default function DraftRoom() {
</Button>
)}
<div>
<h1 className="text-3xl font-bold">
<h1 className="text-lg md:text-3xl font-bold">
{season.league.name} - {season.year} Draft
</h1>
<div className="flex gap-4 text-sm text-muted-foreground mt-1">
@ -1073,13 +1156,13 @@ export default function DraftRoom() {
</div>
</div>
<div className="flex items-center gap-3">
{/* Commissioner Controls */}
{/* Commissioner Controls - hidden on mobile (shown in Controls tab) */}
{isCommissioner && season.status === "pre_draft" && (
<Button onClick={handleStartDraft}>Start Draft</Button>
<Button className="hidden md:flex" onClick={handleStartDraft}>Start Draft</Button>
)}
{isCommissioner && season.status === "draft" && !isDraftComplete && (
<>
<div className="hidden md:flex">
{isPaused ? (
<Button onClick={handleResumeDraft} variant="default">
Resume Draft
@ -1089,7 +1172,7 @@ export default function DraftRoom() {
Pause Draft
</Button>
)}
</>
</div>
)}
<div className="flex items-center gap-2">
@ -1098,11 +1181,11 @@ export default function DraftRoom() {
isConnected ? "bg-emerald-500" : "bg-coral-accent"
}`}
/>
<span className="text-sm font-medium">
<span className="hidden sm:inline text-sm font-medium">
{isConnected ? "Connected" : "Disconnected"}
</span>
</div>
<Button variant="outline" asChild>
<Button variant="outline" asChild className="hidden md:flex">
<Link to={`/leagues/${season.leagueId}`}>Exit Draft Room</Link>
</Button>
</div>
@ -1110,153 +1193,201 @@ export default function DraftRoom() {
</div>
</div>
{/* Main Content - Flex Row: Sidebar + Tabs */}
<div className="flex-1 flex overflow-hidden">
{/* Sidebar */}
{userTeam && (
<DraftSidebar
collapsed={sidebarCollapsed}
onCollapsedChange={setSidebarCollapsed}
queueSection={
<QueueSection
queue={queue}
availableParticipants={availableParticipants}
eligibility={eligibility}
seasonId={season.id}
teamId={userTeam.id}
isMyTurn={isMyTurn}
userAutodraft={userAutodraft}
onRemoveFromQueue={handleRemoveFromQueue}
onAutodraftUpdate={(isEnabled, mode) => {
setUserAutodraft({ isEnabled, mode });
}}
onReorder={handleReorderQueue}
/>
}
recentPicksSection={
<SidebarRecentPicks picks={picks} />
}
settingsSection={
<NotificationSettings
enabled={notificationsEnabled}
onEnabledChange={setNotificationsEnabled}
mode={notificationsMode}
onModeChange={setNotificationsMode}
permissionState={notificationsPermission}
/>
}
/>
)}
{/* Mobile On Clock bar */}
{season.status === "draft" && !isDraftComplete && currentDraftSlot && (
<div className={`md:hidden flex-shrink-0 flex items-center justify-between px-4 py-2 border-b ${
isMyTurn
? "bg-electric/25 border-electric"
: "bg-muted/30"
}`}>
<div>
<div className={`text-xs font-bold uppercase tracking-wider ${isMyTurn ? "text-electric" : "text-muted-foreground"}`}>
{isMyTurn ? "Your Turn!" : "On the Clock"}
</div>
<div className="text-sm font-bold">{currentDraftSlot.team.name}</div>
</div>
{isPaused ? (
<span className="text-sm font-bold text-amber-accent">Paused</span>
) : (
<span className={`text-xl font-mono font-bold tabular-nums ${currentClockColor}`}>
{formatClockTime(currentClockTime)}
</span>
)}
</div>
)}
{/* Main Tabbed Content */}
<div className="flex-1 overflow-hidden">
<Tabs
value={activeTab}
onValueChange={(value) =>
setActiveTab(value as "participants" | "board" | "teams")
}
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="teams">Teams Drafted</TabsTrigger>
</TabsList>
{/* Main Content — single layout tree based on isMobile to avoid duplicate component instances */}
<div className="flex-1 overflow-hidden">
{isMobile ? (
<div className="flex h-full overflow-hidden flex-col">
{mobileTab === "lobby" && (
<AvailableParticipantsSection {...availableParticipantsSectionProps} />
)}
{mobileTab === "board" && (
<DraftGridSection {...draftGridSectionProps} />
)}
{mobileTab === "roster" && (
<TeamsDraftedGrid {...teamsDraftedGridProps} />
)}
{mobileTab === "controls" && (
<div className="h-full overflow-y-auto p-4 space-y-6">
{isCommissioner && season.status === "pre_draft" && (
<Button className="w-full min-h-[48px]" onClick={handleStartDraft}>
Start Draft
</Button>
)}
{isCommissioner && season.status === "draft" && !isDraftComplete && (
isPaused ? (
<Button className="w-full min-h-[48px]" onClick={handleResumeDraft}>
Resume Draft
</Button>
) : (
<Button className="w-full min-h-[48px]" variant="outline" onClick={handlePauseDraft}>
Pause Draft
</Button>
)
)}
{season.status === "draft" && !isDraftComplete && currentDraftSlot && (
<div className={`ml-auto flex items-center gap-2.5 rounded-lg px-3 py-1.5 flex-shrink-0 transition-all ${
isMyTurn
? "bg-electric/30 border-2 border-electric shadow-md shadow-electric/30"
: "border border-border/50"
}`}>
<div className="leading-tight">
<div className={`text-xs font-bold uppercase tracking-wider ${isMyTurn ? "text-electric" : "text-muted-foreground"}`}>
{isMyTurn ? "Your Turn!" : "On the Clock"}
{queueSectionProps && (
<section>
<h2 className="font-semibold text-sm mb-2">My Queue</h2>
<QueueSection {...queueSectionProps} />
</section>
)}
<section>
<h2 className="font-semibold text-sm mb-2">Recent Picks</h2>
<SidebarRecentPicks picks={picks} />
</section>
<NotificationSettings
enabled={notificationsEnabled}
onEnabledChange={setNotificationsEnabled}
mode={notificationsMode}
onModeChange={setNotificationsMode}
permissionState={notificationsPermission}
/>
<Button variant="outline" asChild className="w-full min-h-[48px]">
<Link to={`/leagues/${season.leagueId}`}>Exit Draft Room</Link>
</Button>
</div>
)}
</div>
) : (
<div className="flex h-full overflow-hidden">
{userTeam && queueSectionProps && (
<DraftSidebar
collapsed={sidebarCollapsed}
onCollapsedChange={setSidebarCollapsed}
queueSection={<QueueSection {...queueSectionProps} />}
recentPicksSection={<SidebarRecentPicks picks={picks} />}
settingsSection={
<NotificationSettings
enabled={notificationsEnabled}
onEnabledChange={setNotificationsEnabled}
mode={notificationsMode}
onModeChange={setNotificationsMode}
permissionState={notificationsPermission}
/>
}
/>
)}
<div className="flex-1 overflow-hidden">
<Tabs
value={activeTab}
onValueChange={(value) =>
setActiveTab(value as "participants" | "board" | "teams")
}
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="teams">Teams Drafted</TabsTrigger>
</TabsList>
{season.status === "draft" && !isDraftComplete && currentDraftSlot && (
<div className={`ml-auto flex items-center gap-2.5 rounded-lg px-3 py-1.5 flex-shrink-0 transition-all ${
isMyTurn
? "bg-electric/30 border-2 border-electric shadow-md shadow-electric/30"
: "border border-border/50"
}`}>
<div className="leading-tight">
<div className={`text-xs font-bold uppercase tracking-wider ${isMyTurn ? "text-electric" : "text-muted-foreground"}`}>
{isMyTurn ? "Your Turn!" : "On the Clock"}
</div>
<div className={`text-sm font-bold ${isMyTurn ? "text-foreground" : "text-muted-foreground"}`}>
{currentDraftSlot.team.name}
</div>
</div>
{isPaused ? (
<span className="text-sm font-bold text-amber-accent">Paused</span>
) : (
<span className={`text-2xl font-mono font-bold tabular-nums ${currentClockColor}`}>
{formatClockTime(currentClockTime)}
</span>
)}
</div>
<div className={`text-sm font-bold ${isMyTurn ? "text-foreground" : "text-muted-foreground"}`}>
{currentDraftSlot.team.name}
</div>
</div>
{isPaused ? (
<span className="text-sm font-bold text-amber-accent">Paused</span>
) : (
<span className={`text-2xl font-mono font-bold tabular-nums ${currentClockColor}`}>
{formatClockTime(currentClockTime)}
</span>
)}
</div>
</div>
<TabsContent value="participants" className="flex-1 overflow-hidden m-0">
<div className="h-full">
<AvailableParticipantsSection {...availableParticipantsSectionProps} />
</div>
</TabsContent>
<TabsContent value="board" className="flex-1 overflow-hidden m-0">
<DraftGridSection {...draftGridSectionProps} />
</TabsContent>
<TabsContent value="teams" className="flex-1 overflow-hidden m-0">
<div className="h-full">
<TeamsDraftedGrid {...teamsDraftedGridProps} />
</div>
</TabsContent>
</Tabs>
</div>
</div>
)}
</div>
{/* Mobile Bottom Nav */}
<nav className="md:hidden flex-shrink-0 border-t bg-card grid grid-cols-4">
{MOBILE_TABS.map((tab) => {
const showTurnIndicator =
tab.id === "lobby" &&
isMyTurn &&
season.status === "draft" &&
!isDraftComplete;
return (
<button
key={tab.id}
onClick={() => setMobileTab(tab.id)}
className={`relative flex flex-col items-center justify-center py-2 gap-0.5 min-h-[56px] transition-colors ${
mobileTab === tab.id ? "text-electric" : "text-muted-foreground"
}`}
>
<div className="relative">
<tab.Icon className="h-5 w-5" />
{showTurnIndicator && (
<span className="absolute -top-1 -right-1 w-2 h-2 rounded-full bg-electric" />
)}
</div>
</div>
<TabsContent value="participants" className="flex-1 overflow-hidden m-0">
<div className="h-full">
<AvailableParticipantsSection
participants={filteredParticipants}
searchQuery={searchQuery}
sportFilter={sportFilter}
hideDrafted={hideDrafted}
hideIneligible={hideIneligible}
uniqueSports={uniqueSports}
draftedParticipantIds={draftedParticipantIds}
queue={queue}
eligibility={eligibility}
canPick={canPick}
hasTeam={!!userTeam}
onSearchChange={setSearchQuery}
onSportFilterChange={setSportFilter}
onHideDraftedChange={setHideDrafted}
onHideIneligibleChange={setHideIneligible}
onMakePick={handleMakePick}
onAddToQueue={handleAddToQueue}
onRemoveFromQueue={handleRemoveFromQueue}
/>
</div>
</TabsContent>
<TabsContent value="board" className="flex-1 overflow-hidden m-0">
<DraftGridSection
draftSlots={draftSlots}
draftGrid={draftGrid}
currentPick={currentPick}
teamTimers={teamTimers}
autodraftStatus={autodraftStatus}
connectedTeams={connectedTeams}
isCommissioner={isCommissioner}
ownerMap={ownerMap}
onAdjustTimeBankOpen={isCommissioner ? handleAdjustTimeBankOpen : undefined}
onForceAutopick={handleForceAutopick}
onForceManualPickOpen={(pickNumber, teamId) => {
setSelectedPickSlot({ pickNumber, teamId });
setDialogSearchQuery("");
setDialogSportFilter("all");
setForcePickDialogOpen(true);
}}
onReplacePick={isCommissioner ? handleReplacePickOpen : undefined}
onRollbackToPick={isCommissioner && !isDraftComplete ? handleRollbackToPick : undefined}
/>
</TabsContent>
<TabsContent value="teams" className="flex-1 overflow-hidden m-0">
<div className="h-full">
<TeamsDraftedGrid
draftSlots={draftSlots}
picks={picks}
sports={seasonSportsData}
season={{ numFlexPicks }}
ownerMap={ownerMap}
/>
</div>
</TabsContent>
</Tabs>
</div>
</div>
<span className="text-[10px] font-medium">{tab.label}</span>
</button>
);
})}
</nav>
{/* Force Manual Pick Dialog */}
<Dialog