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 { Badge } from "~/components/ui/badge";
|
||||||
import { ListPlus, ListX } from "lucide-react";
|
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 {
|
interface AvailableParticipantsSectionProps {
|
||||||
participants: Array<{
|
participants: Array<{
|
||||||
id: string;
|
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"
|
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 */}
|
{/* Sport Filter Dropdown */}
|
||||||
<select
|
<select
|
||||||
value={sportFilter}
|
value={sportFilter}
|
||||||
onChange={(e) => onSportFilterChange(e.target.value)}
|
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>
|
<option value="all">All Sports</option>
|
||||||
{uniqueSports.map((sport) => (
|
{uniqueSports.map((sport) => (
|
||||||
|
|
@ -108,9 +126,117 @@ export function AvailableParticipantsSection({
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Table - This will scroll independently */}
|
{/* Mobile Card List - visible on mobile only */}
|
||||||
<div className="flex-1 overflow-hidden">
|
<div className="flex md:hidden flex-1 overflow-y-auto flex-col gap-2 px-4 py-2">
|
||||||
<div className="h-full overflow-y-auto">
|
{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">
|
<table className="w-full text-sm">
|
||||||
<thead className="bg-muted sticky top-0">
|
<thead className="bg-muted sticky top-0">
|
||||||
<tr>
|
<tr>
|
||||||
|
|
@ -133,17 +259,8 @@ export function AvailableParticipantsSection({
|
||||||
</tr>
|
</tr>
|
||||||
) : (
|
) : (
|
||||||
participants.map((participant) => {
|
participants.map((participant) => {
|
||||||
const isDrafted = draftedParticipantIds.has(participant.id);
|
const { isDrafted, isInQueue, isEligible, ineligibleReason } =
|
||||||
const isInQueue = queue.some(
|
getParticipantState(participant, draftedParticipantIds, queue, eligibility);
|
||||||
(item) => item.participantId === participant.id
|
|
||||||
);
|
|
||||||
|
|
||||||
// Check eligibility
|
|
||||||
const isEligible = eligibility
|
|
||||||
? eligibility.eligibleSportIds.has(participant.sport.id)
|
|
||||||
: true;
|
|
||||||
const ineligibleReason =
|
|
||||||
eligibility?.ineligibleReasons[participant.sport.id];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<tr
|
<tr
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,25 @@
|
||||||
import { useMemo } from "react";
|
import { useMemo, useState } from "react";
|
||||||
import {
|
import {
|
||||||
ContextMenu,
|
ContextMenu,
|
||||||
ContextMenuContent,
|
ContextMenuContent,
|
||||||
ContextMenuItem,
|
ContextMenuItem,
|
||||||
ContextMenuTrigger,
|
ContextMenuTrigger,
|
||||||
} from "~/components/ui/context-menu";
|
} 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";
|
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 {
|
interface DraftGridSectionProps {
|
||||||
draftSlots: Array<{
|
draftSlots: Array<{
|
||||||
id: string;
|
id: string;
|
||||||
|
|
@ -60,6 +73,8 @@ export function DraftGridSection({
|
||||||
onRollbackToPick,
|
onRollbackToPick,
|
||||||
ownerMap = {},
|
ownerMap = {},
|
||||||
}: DraftGridSectionProps) {
|
}: DraftGridSectionProps) {
|
||||||
|
const [mobileSheet, setMobileSheet] = useState<MobileSheetData | null>(null);
|
||||||
|
|
||||||
const currentTeamId = useMemo(
|
const currentTeamId = useMemo(
|
||||||
() => draftGrid.flat().find((c) => c.pickNumber === currentPick)?.teamId ?? null,
|
() => draftGrid.flat().find((c) => c.pickNumber === currentPick)?.teamId ?? null,
|
||||||
[draftGrid, currentPick]
|
[draftGrid, currentPick]
|
||||||
|
|
@ -69,16 +84,18 @@ export function DraftGridSection({
|
||||||
<div className="h-full overflow-auto p-4">
|
<div className="h-full overflow-auto p-4">
|
||||||
<h2 className="text-xl font-semibold mb-4">Draft Grid</h2>
|
<h2 className="text-xl font-semibold mb-4">Draft Grid</h2>
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto">
|
||||||
<div className="w-full">
|
<div className="inline-block min-w-full">
|
||||||
{/* Team Headers */}
|
{/* 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) => {
|
{draftSlots.map((slot) => {
|
||||||
const teamTime = teamTimers[slot.team.id];
|
const teamTime = teamTimers[slot.team.id];
|
||||||
const isAutodraft = autodraftStatus[slot.team.id] || false;
|
const isAutodraft = autodraftStatus[slot.team.id] || false;
|
||||||
const isConnected = connectedTeams.has(slot.team.id);
|
const isConnected = connectedTeams.has(slot.team.id);
|
||||||
|
|
||||||
const headerInner = (
|
const headerInner = (
|
||||||
<>
|
<div className="relative">
|
||||||
<div
|
<div
|
||||||
className={`font-semibold text-sm truncate px-2 ${
|
className={`font-semibold text-sm truncate px-2 ${
|
||||||
slot.team.id === currentTeamId
|
slot.team.id === currentTeamId
|
||||||
|
|
@ -91,7 +108,7 @@ export function DraftGridSection({
|
||||||
{ownerMap[slot.team.id] || slot.team.name}
|
{ownerMap[slot.team.id] || slot.team.name}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className={`text-xs font-mono ${getTimerColorClass(teamTime)}`}
|
className={`text-xs font-mono px-2 ${getTimerColorClass(teamTime)}`}
|
||||||
>
|
>
|
||||||
{formatClockTime(teamTime)}
|
{formatClockTime(teamTime)}
|
||||||
{isAutodraft && (
|
{isAutodraft && (
|
||||||
|
|
@ -100,7 +117,15 @@ export function DraftGridSection({
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</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) {
|
if (isCommissioner && onAdjustTimeBankOpen) {
|
||||||
|
|
@ -140,12 +165,16 @@ export function DraftGridSection({
|
||||||
: roundPicks;
|
: roundPicks;
|
||||||
|
|
||||||
return (
|
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) => {
|
{displayPicks.map((cell) => {
|
||||||
const isCurrent = cell.pickNumber === currentPick;
|
const isCurrent = cell.pickNumber === currentPick;
|
||||||
const isPicked = !!cell.pick;
|
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
|
isCurrent
|
||||||
? "border-electric bg-electric/20 shadow-lg shadow-electric/25 ring-1 ring-electric/40"
|
? "border-electric bg-electric/20 shadow-lg shadow-electric/25 ring-1 ring-electric/40"
|
||||||
: isPicked
|
: isPicked
|
||||||
|
|
@ -173,6 +202,23 @@ export function DraftGridSection({
|
||||||
On Clock
|
On Clock
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : 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>
|
</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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -86,7 +86,7 @@ export function TeamsDraftedGrid({
|
||||||
<table className="w-full border-collapse">
|
<table className="w-full border-collapse">
|
||||||
<thead className="sticky top-0 bg-background z-10">
|
<thead className="sticky top-0 bg-background z-10">
|
||||||
<tr>
|
<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
|
Sport
|
||||||
</th>
|
</th>
|
||||||
{draftSlots.map((slot, index) => {
|
{draftSlots.map((slot, index) => {
|
||||||
|
|
@ -115,7 +115,7 @@ export function TeamsDraftedGrid({
|
||||||
const isLastRow = sportIndex === sports.length - 1;
|
const isLastRow = sportIndex === sports.length - 1;
|
||||||
return (
|
return (
|
||||||
<tr key={sport.id}>
|
<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}
|
{sport.name}
|
||||||
</td>
|
</td>
|
||||||
{draftSlots.map((slot, slotIndex) => {
|
{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 { calculateDraftEligibility } from "~/lib/draft-eligibility";
|
||||||
import { getTeamForPick } from "~/lib/draft-order";
|
import { getTeamForPick } from "~/lib/draft-order";
|
||||||
import { useDraftNotifications } from "~/hooks/useDraftNotifications";
|
import { useDraftNotifications } from "~/hooks/useDraftNotifications";
|
||||||
|
import { useMediaQuery } from "~/hooks/useMediaQuery";
|
||||||
import { NotificationSettings } from "~/components/NotificationSettings";
|
import { NotificationSettings } from "~/components/NotificationSettings";
|
||||||
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 } from "lucide-react";
|
||||||
import type { Route } from "./+types/$leagueId.draft.$seasonId";
|
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) {
|
export async function loader(args: Route.LoaderArgs) {
|
||||||
const { params } = args;
|
const { params } = args;
|
||||||
const { seasonId, leagueId } = params;
|
const { seasonId, leagueId } = params;
|
||||||
|
|
@ -242,6 +251,10 @@ export default function DraftRoom() {
|
||||||
return stored ? JSON.parse(stored) : false;
|
return stored ? JSON.parse(stored) : false;
|
||||||
});
|
});
|
||||||
const [activeTab, setActiveTab] = useState<"participants" | "board" | "teams">("participants");
|
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
|
// Track autodraft status for all teams
|
||||||
const [autodraftStatus, setAutodraftStatus] = useState<Record<string, boolean>>(() => {
|
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
|
// Calculate current round
|
||||||
const currentRound = draftSlots.length > 0 ? Math.ceil(currentPick / draftSlots.length) : 1;
|
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]
|
[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 (
|
return (
|
||||||
<div className="h-screen bg-background flex flex-col overflow-hidden">
|
<div className="h-screen bg-background flex flex-col overflow-hidden">
|
||||||
{/* Draft Completion Banner */}
|
{/* Draft Completion Banner */}
|
||||||
|
|
@ -1060,7 +1143,7 @@ export default function DraftRoom() {
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-3xl font-bold">
|
<h1 className="text-lg md:text-3xl font-bold">
|
||||||
{season.league.name} - {season.year} Draft
|
{season.league.name} - {season.year} Draft
|
||||||
</h1>
|
</h1>
|
||||||
<div className="flex gap-4 text-sm text-muted-foreground mt-1">
|
<div className="flex gap-4 text-sm text-muted-foreground mt-1">
|
||||||
|
|
@ -1073,13 +1156,13 @@ export default function DraftRoom() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
{/* Commissioner Controls */}
|
{/* Commissioner Controls - hidden on mobile (shown in Controls tab) */}
|
||||||
{isCommissioner && season.status === "pre_draft" && (
|
{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 && (
|
{isCommissioner && season.status === "draft" && !isDraftComplete && (
|
||||||
<>
|
<div className="hidden md:flex">
|
||||||
{isPaused ? (
|
{isPaused ? (
|
||||||
<Button onClick={handleResumeDraft} variant="default">
|
<Button onClick={handleResumeDraft} variant="default">
|
||||||
Resume Draft
|
Resume Draft
|
||||||
|
|
@ -1089,7 +1172,7 @@ export default function DraftRoom() {
|
||||||
Pause Draft
|
Pause Draft
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
|
|
@ -1098,11 +1181,11 @@ export default function DraftRoom() {
|
||||||
isConnected ? "bg-emerald-500" : "bg-coral-accent"
|
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"}
|
{isConnected ? "Connected" : "Disconnected"}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<Button variant="outline" asChild>
|
<Button variant="outline" asChild className="hidden md:flex">
|
||||||
<Link to={`/leagues/${season.leagueId}`}>Exit Draft Room</Link>
|
<Link to={`/leagues/${season.leagueId}`}>Exit Draft Room</Link>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -1110,153 +1193,201 @@ export default function DraftRoom() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Main Content - Flex Row: Sidebar + Tabs */}
|
{/* Mobile On Clock bar */}
|
||||||
<div className="flex-1 flex overflow-hidden">
|
{season.status === "draft" && !isDraftComplete && currentDraftSlot && (
|
||||||
{/* Sidebar */}
|
<div className={`md:hidden flex-shrink-0 flex items-center justify-between px-4 py-2 border-b ${
|
||||||
{userTeam && (
|
isMyTurn
|
||||||
<DraftSidebar
|
? "bg-electric/25 border-electric"
|
||||||
collapsed={sidebarCollapsed}
|
: "bg-muted/30"
|
||||||
onCollapsedChange={setSidebarCollapsed}
|
}`}>
|
||||||
queueSection={
|
<div>
|
||||||
<QueueSection
|
<div className={`text-xs font-bold uppercase tracking-wider ${isMyTurn ? "text-electric" : "text-muted-foreground"}`}>
|
||||||
queue={queue}
|
{isMyTurn ? "Your Turn!" : "On the Clock"}
|
||||||
availableParticipants={availableParticipants}
|
</div>
|
||||||
eligibility={eligibility}
|
<div className="text-sm font-bold">{currentDraftSlot.team.name}</div>
|
||||||
seasonId={season.id}
|
</div>
|
||||||
teamId={userTeam.id}
|
{isPaused ? (
|
||||||
isMyTurn={isMyTurn}
|
<span className="text-sm font-bold text-amber-accent">Paused</span>
|
||||||
userAutodraft={userAutodraft}
|
) : (
|
||||||
onRemoveFromQueue={handleRemoveFromQueue}
|
<span className={`text-xl font-mono font-bold tabular-nums ${currentClockColor}`}>
|
||||||
onAutodraftUpdate={(isEnabled, mode) => {
|
{formatClockTime(currentClockTime)}
|
||||||
setUserAutodraft({ isEnabled, mode });
|
</span>
|
||||||
}}
|
)}
|
||||||
onReorder={handleReorderQueue}
|
</div>
|
||||||
/>
|
)}
|
||||||
}
|
|
||||||
recentPicksSection={
|
|
||||||
<SidebarRecentPicks picks={picks} />
|
|
||||||
}
|
|
||||||
settingsSection={
|
|
||||||
<NotificationSettings
|
|
||||||
enabled={notificationsEnabled}
|
|
||||||
onEnabledChange={setNotificationsEnabled}
|
|
||||||
mode={notificationsMode}
|
|
||||||
onModeChange={setNotificationsMode}
|
|
||||||
permissionState={notificationsPermission}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Main Tabbed Content */}
|
{/* 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">
|
||||||
<Tabs
|
{isMobile ? (
|
||||||
value={activeTab}
|
<div className="flex h-full overflow-hidden flex-col">
|
||||||
onValueChange={(value) =>
|
{mobileTab === "lobby" && (
|
||||||
setActiveTab(value as "participants" | "board" | "teams")
|
<AvailableParticipantsSection {...availableParticipantsSectionProps} />
|
||||||
}
|
)}
|
||||||
className="h-full flex flex-col"
|
{mobileTab === "board" && (
|
||||||
>
|
<DraftGridSection {...draftGridSectionProps} />
|
||||||
<div className={`mt-4 transition-all duration-300 ${
|
)}
|
||||||
isMyTurn && season.status === "draft" && !isDraftComplete
|
{mobileTab === "roster" && (
|
||||||
? "bg-electric/25 border-y-2 border-electric shadow-[0_0_24px_0_rgb(0_200_255_/_0.18)]"
|
<TeamsDraftedGrid {...teamsDraftedGridProps} />
|
||||||
: ""
|
)}
|
||||||
}`}>
|
{mobileTab === "controls" && (
|
||||||
<div className="flex items-center gap-3 px-4 py-2">
|
<div className="h-full overflow-y-auto p-4 space-y-6">
|
||||||
<TabsList>
|
{isCommissioner && season.status === "pre_draft" && (
|
||||||
<TabsTrigger value="participants">Available Participants</TabsTrigger>
|
<Button className="w-full min-h-[48px]" onClick={handleStartDraft}>
|
||||||
<TabsTrigger value="board">Draft Board</TabsTrigger>
|
Start Draft
|
||||||
<TabsTrigger value="teams">Teams Drafted</TabsTrigger>
|
</Button>
|
||||||
</TabsList>
|
)}
|
||||||
|
{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 && (
|
{queueSectionProps && (
|
||||||
<div className={`ml-auto flex items-center gap-2.5 rounded-lg px-3 py-1.5 flex-shrink-0 transition-all ${
|
<section>
|
||||||
isMyTurn
|
<h2 className="font-semibold text-sm mb-2">My Queue</h2>
|
||||||
? "bg-electric/30 border-2 border-electric shadow-md shadow-electric/30"
|
<QueueSection {...queueSectionProps} />
|
||||||
: "border border-border/50"
|
</section>
|
||||||
}`}>
|
)}
|
||||||
<div className="leading-tight">
|
|
||||||
<div className={`text-xs font-bold uppercase tracking-wider ${isMyTurn ? "text-electric" : "text-muted-foreground"}`}>
|
<section>
|
||||||
{isMyTurn ? "Your Turn!" : "On the Clock"}
|
<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>
|
||||||
<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>
|
||||||
|
</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>
|
||||||
</div>
|
<span className="text-[10px] font-medium">{tab.label}</span>
|
||||||
|
</button>
|
||||||
<TabsContent value="participants" className="flex-1 overflow-hidden m-0">
|
);
|
||||||
<div className="h-full">
|
})}
|
||||||
<AvailableParticipantsSection
|
</nav>
|
||||||
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>
|
|
||||||
|
|
||||||
{/* Force Manual Pick Dialog */}
|
{/* Force Manual Pick Dialog */}
|
||||||
<Dialog
|
<Dialog
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue