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:
parent
01f1480159
commit
77e408cad8
5 changed files with 550 additions and 174 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
15
app/hooks/useMediaQuery.ts
Normal file
15
app/hooks/useMediaQuery.ts
Normal 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;
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue