From 285981ab9ded82bfc9a3bae164a885536f521f9d Mon Sep 17 00:00:00 2001 From: Chris Parsons <438676+chrisparsons83@users.noreply.github.com> Date: Fri, 20 Feb 2026 21:50:27 -0800 Subject: [PATCH] Improve draft room UX with better error handling and UI refinements (#17) * Remove pause/resume controls when draft is complete, rename Live to Connected - Hide Pause/Resume Draft buttons when isDraftComplete is true - Change 'Live' status indicator to 'Connected' in both draft room and draft board views https://claude.ai/code/session_01AUaKzx465NrY29Qv6MVwjC * Fix code review issues in draft room: security, bugs, and quality Security: - Fix inconsistent commissioner check: draft.start, force-autopick, force-manual-pick, and make-pick all now query the commissioners table instead of league.createdBy, so co-commissioners have consistent access to all draft controls Bugs: - canPick now includes !isPaused so the UI correctly blocks picks during a pause - isDraftComplete initial state now covers 'completed' season status, not just 'active' - Guard JSON.parse in queue.reorder.ts with try/catch to return 400 instead of 500 Code quality: - Add error handling (try/catch + toast) to handlePauseDraft, handleResumeDraft, handleRemoveFromQueue, and handleReorderQueue - Replace alert() with toast.error() in handleMakePick, handleForceAutopick, handleForceManualPick for consistent UX - Memoize filteredParticipants with useMemo to avoid recomputing on every render - Replace custom force-pick dialog div with ShadCN Dialog component for proper keyboard support (Escape to close, focus trap, accessible markup); add dialog.tsx - Remove console.log debug statements from socket event handlers and API routes - Replace (global as any).__socketIO with getSocketIO() across all API routes - Replace window.location.reload() in handleStartDraft with useRevalidator https://claude.ai/code/session_01AUaKzx465NrY29Qv6MVwjC --------- Co-authored-by: Claude --- app/components/ui/dialog.tsx | 133 ++++++ app/routes/api/draft.force-autopick.ts | 16 +- app/routes/api/draft.force-manual-pick.ts | 30 +- app/routes/api/draft.make-pick.ts | 30 +- app/routes/api/draft.pause.ts | 4 +- app/routes/api/draft.resume.ts | 4 +- app/routes/api/draft.start.ts | 20 +- app/routes/api/queue.reorder.ts | 7 +- .../$leagueId.draft-board.$seasonId.tsx | 2 +- .../leagues/$leagueId.draft.$seasonId.tsx | 385 +++++++++--------- 10 files changed, 393 insertions(+), 238 deletions(-) create mode 100644 app/components/ui/dialog.tsx diff --git a/app/components/ui/dialog.tsx b/app/components/ui/dialog.tsx new file mode 100644 index 0000000..323680d --- /dev/null +++ b/app/components/ui/dialog.tsx @@ -0,0 +1,133 @@ +import * as React from "react" +import * as DialogPrimitive from "@radix-ui/react-dialog" +import { XIcon } from "lucide-react" + +import { cn } from "~/lib/utils" + +function Dialog({ + ...props +}: React.ComponentProps) { + return +} + +function DialogTrigger({ + ...props +}: React.ComponentProps) { + return +} + +function DialogPortal({ + ...props +}: React.ComponentProps) { + return +} + +function DialogClose({ + ...props +}: React.ComponentProps) { + return +} + +function DialogOverlay({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DialogContent({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + + + + {children} + + + Close + + + + ) +} + +function DialogHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function DialogFooter({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function DialogTitle({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DialogDescription({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogOverlay, + DialogPortal, + DialogTitle, + DialogTrigger, +} diff --git a/app/routes/api/draft.force-autopick.ts b/app/routes/api/draft.force-autopick.ts index 0df9e8a..c5679d7 100644 --- a/app/routes/api/draft.force-autopick.ts +++ b/app/routes/api/draft.force-autopick.ts @@ -1,7 +1,7 @@ import { getAuth } from "@clerk/react-router/server"; import { database } from "~/database/context"; import * as schema from "~/database/schema"; -import { eq } from "drizzle-orm"; +import { eq, and } from "drizzle-orm"; import { executeAutoPick } from "~/models/draft-utils"; export async function action(args: any) { @@ -27,9 +27,6 @@ export async function action(args: any) { // Get season details to verify commissioner permissions const season = await db.query.seasons.findFirst({ where: eq(schema.seasons.id, seasonId), - with: { - league: true, - }, }); if (!season) { @@ -37,8 +34,15 @@ export async function action(args: any) { } // Check if user is commissioner - if (season.league.createdBy !== userId) { - return Response.json({ error: "Only commissioner can force autopick" }, { status: 403 }); + const isCommissioner = await db.query.commissioners.findFirst({ + where: and( + eq(schema.commissioners.leagueId, season.leagueId), + eq(schema.commissioners.userId, userId) + ), + }); + + if (!isCommissioner) { + return Response.json({ error: "Only commissioners can force autopick" }, { status: 403 }); } // Execute the autopick using the unified function diff --git a/app/routes/api/draft.force-manual-pick.ts b/app/routes/api/draft.force-manual-pick.ts index dd13424..7566923 100644 --- a/app/routes/api/draft.force-manual-pick.ts +++ b/app/routes/api/draft.force-manual-pick.ts @@ -6,6 +6,7 @@ import { calculateDraftEligibility } from "~/lib/draft-eligibility"; import { getDraftPicksWithSports, getTeamDraftPicksWithSports } from "~/models/draft-pick"; import { getParticipantsForSeasonWithSports } from "~/models/participant"; import { getSeasonSportsSimple } from "~/models/season-sport"; +import { getSocketIO } from "../../../server/socket"; export async function action(args: any) { const { request } = args; @@ -31,9 +32,6 @@ export async function action(args: any) { // Get season details const season = await db.query.seasons.findFirst({ where: eq(schema.seasons.id, seasonId), - with: { - league: true, - }, }); if (!season) { @@ -41,8 +39,15 @@ export async function action(args: any) { } // Check if user is commissioner - if (season.league.createdBy !== userId) { - return Response.json({ error: "Only commissioner can force manual pick" }, { status: 403 }); + const isCommissioner = await db.query.commissioners.findFirst({ + where: and( + eq(schema.commissioners.leagueId, season.leagueId), + eq(schema.commissioners.userId, userId) + ), + }); + + if (!isCommissioner) { + return Response.json({ error: "Only commissioners can force a manual pick" }, { status: 403 }); } // Check if participant is already drafted @@ -156,8 +161,7 @@ export async function action(args: any) { // Emit timer update to all clients try { - const io = (global as any).__socketIO; - io.to(`draft-${seasonId}`).emit("timer-update", { + getSocketIO().to(`draft-${seasonId}`).emit("timer-update", { seasonId, teamId, timeRemaining: newTimeRemaining, @@ -186,10 +190,6 @@ export async function action(args: any) { const nextTeamId = nextDraftSlot.teamId; const initialTime = season.draftInitialTime || 120; - console.log( - `[ForceManualPick] Initializing timer for next team ${nextTeamId} with ${initialTime}s (pick ${nextPickNumber})` - ); - // Check if timer already exists for next team const nextTimer = await db.query.draftTimers.findFirst({ where: and( @@ -218,8 +218,7 @@ export async function action(args: any) { // Emit timer update for next team try { - const io = (global as any).__socketIO; - io.to(`draft-${seasonId}`).emit("timer-update", { + getSocketIO().to(`draft-${seasonId}`).emit("timer-update", { seasonId, teamId: nextTeamId, timeRemaining: initialTime, @@ -252,8 +251,7 @@ export async function action(args: any) { // Notify all clients that this participant was removed from queues try { - const io = (global as any).__socketIO; - io.to(`draft-${seasonId}`).emit("participant-removed-from-queues", { + getSocketIO().to(`draft-${seasonId}`).emit("participant-removed-from-queues", { participantId, }); } catch (error) { @@ -262,7 +260,7 @@ export async function action(args: any) { // Emit socket event try { - const io = (global as any).__socketIO; + const io = getSocketIO(); const team = draftSlots.find((slot) => slot.team.id === teamId)?.team; io.to(`draft-${seasonId}`).emit("pick-made", { diff --git a/app/routes/api/draft.make-pick.ts b/app/routes/api/draft.make-pick.ts index 09b877b..f2e9f9e 100644 --- a/app/routes/api/draft.make-pick.ts +++ b/app/routes/api/draft.make-pick.ts @@ -6,6 +6,7 @@ import { calculateDraftEligibility } from "~/lib/draft-eligibility"; import { getDraftPicksWithSports, getTeamDraftPicksWithSports } from "~/models/draft-pick"; import { getParticipantsForSeasonWithSports } from "~/models/participant"; import { getSeasonSportsSimple } from "~/models/season-sport"; +import { getSocketIO } from "../../../server/socket"; export async function action(args: any) { const { request } = args; @@ -29,9 +30,6 @@ export async function action(args: any) { // Get season details const season = await db.query.seasons.findFirst({ where: eq(schema.seasons.id, seasonId), - with: { - league: true, - }, }); if (!season) { @@ -67,7 +65,13 @@ export async function action(args: any) { // Check permissions: must be team owner or commissioner const isTeamOwner = currentDraftSlot.team.ownerId === userId; - const isCommissioner = season.league.createdBy === userId; + const commissionerRecord = await db.query.commissioners.findFirst({ + where: and( + eq(schema.commissioners.leagueId, season.leagueId), + eq(schema.commissioners.userId, userId) + ), + }); + const isCommissioner = !!commissionerRecord; if (!isTeamOwner && !isCommissioner) { return Response.json({ error: "Not your turn to pick" }, { status: 403 }); @@ -153,8 +157,7 @@ export async function action(args: any) { // Notify all clients that this participant was removed from queues try { - const io = (global as any).__socketIO; - io.to(`draft-${seasonId}`).emit("participant-removed-from-queues", { + getSocketIO().to(`draft-${seasonId}`).emit("participant-removed-from-queues", { participantId, }); } catch (error) { @@ -187,8 +190,7 @@ export async function action(args: any) { // Emit timer update to all clients try { - const io = (global as any).__socketIO; - io.to(`draft-${seasonId}`).emit("timer-update", { + getSocketIO().to(`draft-${seasonId}`).emit("timer-update", { seasonId, teamId: currentDraftSlot.teamId, timeRemaining: newTimeRemaining, @@ -217,10 +219,6 @@ export async function action(args: any) { const nextTeamId = nextDraftSlot.teamId; const initialTime = season.draftInitialTime || 120; - console.log( - `[MakePick] Initializing timer for next team ${nextTeamId} with ${initialTime}s (pick ${nextPickNumber})` - ); - // Check if timer already exists for next team const nextTimer = await db.query.draftTimers.findFirst({ where: and( @@ -249,8 +247,7 @@ export async function action(args: any) { // Emit timer update for next team try { - const io = (global as any).__socketIO; - io.to(`draft-${seasonId}`).emit("timer-update", { + getSocketIO().to(`draft-${seasonId}`).emit("timer-update", { seasonId, teamId: nextTeamId, timeRemaining: initialTime, @@ -273,8 +270,7 @@ export async function action(args: any) { // Emit socket event try { - const io = (global as any).__socketIO; - io.to(`draft-${seasonId}`).emit("pick-made", { + getSocketIO().to(`draft-${seasonId}`).emit("pick-made", { pick: { ...draftPick, team: currentDraftSlot.team, @@ -289,7 +285,7 @@ export async function action(args: any) { }); if (isDraftComplete) { - io.to(`draft-${seasonId}`).emit("draft-completed"); + getSocketIO().to(`draft-${seasonId}`).emit("draft-completed"); } } catch (error) { console.error("Socket.IO error:", error); diff --git a/app/routes/api/draft.pause.ts b/app/routes/api/draft.pause.ts index e816c5f..d11c9bc 100644 --- a/app/routes/api/draft.pause.ts +++ b/app/routes/api/draft.pause.ts @@ -2,6 +2,7 @@ import { getAuth } from "@clerk/react-router/server"; import { eq, and } from "drizzle-orm"; import { database } from "~/database/context"; import * as schema from "~/database/schema"; +import { getSocketIO } from "../../../server/socket"; export async function action(args: any) { const { request } = args; @@ -61,8 +62,7 @@ export async function action(args: any) { // Emit socket event try { - const io = (global as any).__socketIO; - io.to(`draft-${seasonId}`).emit("draft-paused", { + getSocketIO().to(`draft-${seasonId}`).emit("draft-paused", { seasonId, paused: true, }); diff --git a/app/routes/api/draft.resume.ts b/app/routes/api/draft.resume.ts index 57c31fe..9ecc499 100644 --- a/app/routes/api/draft.resume.ts +++ b/app/routes/api/draft.resume.ts @@ -2,6 +2,7 @@ import { getAuth } from "@clerk/react-router/server"; import { eq, and } from "drizzle-orm"; import { database } from "~/database/context"; import * as schema from "~/database/schema"; +import { getSocketIO } from "../../../server/socket"; export async function action(args: any) { const { request } = args; @@ -61,8 +62,7 @@ export async function action(args: any) { // Emit socket event try { - const io = (global as any).__socketIO; - io.to(`draft-${seasonId}`).emit("draft-resumed", { + getSocketIO().to(`draft-${seasonId}`).emit("draft-resumed", { seasonId, paused: false, }); diff --git a/app/routes/api/draft.start.ts b/app/routes/api/draft.start.ts index 77d0d3b..b5ffca0 100644 --- a/app/routes/api/draft.start.ts +++ b/app/routes/api/draft.start.ts @@ -1,7 +1,8 @@ import { getAuth } from "@clerk/react-router/server"; import { database } from "~/database/context"; import * as schema from "~/database/schema"; -import { eq } from "drizzle-orm"; +import { eq, and } from "drizzle-orm"; +import { getSocketIO } from "../../../server/socket"; export async function action(args: any) { const { request } = args; @@ -24,9 +25,6 @@ export async function action(args: any) { // Get season details const season = await db.query.seasons.findFirst({ where: eq(schema.seasons.id, seasonId), - with: { - league: true, - }, }); if (!season) { @@ -34,8 +32,15 @@ export async function action(args: any) { } // Check if user is commissioner - if (season.league.createdBy !== userId) { - return Response.json({ error: "Only commissioner can start draft" }, { status: 403 }); + const isCommissioner = await db.query.commissioners.findFirst({ + where: and( + eq(schema.commissioners.leagueId, season.leagueId), + eq(schema.commissioners.userId, userId) + ), + }); + + if (!isCommissioner) { + return Response.json({ error: "Only commissioners can start the draft" }, { status: 403 }); } // Check if draft already started @@ -77,8 +82,7 @@ export async function action(args: any) { // Emit socket event try { - const io = (global as any).__socketIO; - io.to(`draft-${seasonId}`).emit("draft-started", { + getSocketIO().to(`draft-${seasonId}`).emit("draft-started", { seasonId, currentPickNumber: 1, }); diff --git a/app/routes/api/queue.reorder.ts b/app/routes/api/queue.reorder.ts index f7a6837..a7accf3 100644 --- a/app/routes/api/queue.reorder.ts +++ b/app/routes/api/queue.reorder.ts @@ -22,7 +22,12 @@ export async function action(args: any) { return Response.json({ error: "Missing required fields" }, { status: 400 }); } - const participantIds = JSON.parse(participantIdsJson as string); + let participantIds: string[]; + try { + participantIds = JSON.parse(participantIdsJson as string); + } catch { + return Response.json({ error: "Invalid participantIds format" }, { status: 400 }); + } const db = database(); diff --git a/app/routes/leagues/$leagueId.draft-board.$seasonId.tsx b/app/routes/leagues/$leagueId.draft-board.$seasonId.tsx index 4bc4c84..80f284e 100644 --- a/app/routes/leagues/$leagueId.draft-board.$seasonId.tsx +++ b/app/routes/leagues/$leagueId.draft-board.$seasonId.tsx @@ -165,7 +165,7 @@ export default function DraftBoard() { }`} /> - {isConnected ? "Live" : "Disconnected"} + {isConnected ? "Connected" : "Disconnected"}
)} diff --git a/app/routes/leagues/$leagueId.draft.$seasonId.tsx b/app/routes/leagues/$leagueId.draft.$seasonId.tsx index 1381130..3098151 100644 --- a/app/routes/leagues/$leagueId.draft.$seasonId.tsx +++ b/app/routes/leagues/$leagueId.draft.$seasonId.tsx @@ -1,4 +1,4 @@ -import { useLoaderData, Link } from "react-router"; +import { useLoaderData, Link, useRevalidator } from "react-router"; import { eq, and, asc, desc, inArray } from "drizzle-orm"; import { database } from "~/database/context"; import * as schema from "~/database/schema"; @@ -8,6 +8,7 @@ import { Card } from "~/components/ui/card"; import { Badge } from "~/components/ui/badge"; import { Button } from "~/components/ui/button"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "~/components/ui/tabs"; +import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "~/components/ui/dialog"; import { getAuth } from "@clerk/react-router/server"; import { getTeamQueue } from "~/models/draft-queue"; import { DraftSidebar } from "~/components/DraftSidebar"; @@ -195,6 +196,7 @@ export default function DraftRoom() { currentUserId, numFlexPicks, } = useLoaderData(); + const { revalidate } = useRevalidator(); const { isConnected, connectionError, isReconnecting, on, off } = useDraftSocket(season.id, userTeam?.id); const { permissionState: notificationsPermission, @@ -223,7 +225,9 @@ export default function DraftRoom() { const [sportFilter, setSportFilter] = useState("all"); const [queue, setQueue] = useState(userQueue); const [isPaused, setIsPaused] = useState(season.draftPaused || false); - const [isDraftComplete, setIsDraftComplete] = useState(season.status === "active"); + const [isDraftComplete, setIsDraftComplete] = useState( + season.status === "active" || season.status === "completed" + ); // Sidebar and tabs state const [sidebarCollapsed, setSidebarCollapsed] = useState(() => { @@ -349,7 +353,6 @@ export default function DraftRoom() { // Listen for new picks from other users useEffect(() => { const handlePickMade = (data: any) => { - console.log("Pick made:", data); setPicks((prev: any) => [...prev, data.pick]); setCurrentPick(data.nextPickNumber); @@ -399,7 +402,6 @@ export default function DraftRoom() { }; const handleAutodraftUpdated = (data: { teamId: string; isEnabled: boolean; mode: "next_pick" | "while_on" }) => { - console.log("Autodraft updated:", data); setAutodraftStatus((prev) => ({ ...prev, [data.teamId]: data.isEnabled, @@ -415,12 +417,10 @@ export default function DraftRoom() { }; const handleTeamConnected = (data: { teamId: string }) => { - console.log("Team connected:", data.teamId); setConnectedTeams((prev) => new Set(prev).add(data.teamId)); }; const handleTeamDisconnected = (data: { teamId: string }) => { - console.log("Team disconnected:", data.teamId); setConnectedTeams((prev) => { const newSet = new Set(prev); newSet.delete(data.teamId); @@ -429,12 +429,10 @@ export default function DraftRoom() { }; const handleConnectedTeamsList = (data: { teamIds: string[] }) => { - console.log("Received connected teams list:", data.teamIds); setConnectedTeams(new Set(data.teamIds)); }; const handleParticipantRemovedFromQueues = (data: { participantId: string }) => { - console.log("Participant removed from queues:", data.participantId); setQueue((prev: any) => prev.filter((item: any) => item.participantId !== data.participantId) ); @@ -537,14 +535,21 @@ export default function DraftRoom() { formData.append("queueId", queueId); formData.append("teamId", userTeam.id); - const response = await fetch("/api/queue/remove", { - method: "POST", - body: formData, - }); + try { + const response = await fetch("/api/queue/remove", { + method: "POST", + body: formData, + }); - if (response.ok) { - const data = await response.json(); - setQueue(data.queue); + if (response.ok) { + const data = await response.json(); + setQueue(data.queue); + } else { + const error = await response.json(); + toast.error(error.error || "Failed to remove from queue"); + } + } catch { + toast.error("Network error - failed to remove from queue"); } }; @@ -556,14 +561,21 @@ export default function DraftRoom() { formData.append("seasonId", season.id); formData.append("participantIds", JSON.stringify(participantIds)); - const response = await fetch("/api/queue/reorder", { - method: "POST", - body: formData, - }); + try { + const response = await fetch("/api/queue/reorder", { + method: "POST", + body: formData, + }); - if (response.ok) { - const data = await response.json(); - setQueue(data.queue); + if (response.ok) { + const data = await response.json(); + setQueue(data.queue); + } else { + const error = await response.json(); + toast.error(error.error || "Failed to reorder queue"); + } + } catch { + toast.error("Network error - failed to reorder queue"); } }; @@ -577,7 +589,10 @@ export default function DraftRoom() { }); if (response.ok) { - window.location.reload(); // Reload to get updated season status + revalidate(); + } else { + const error = await response.json(); + toast.error(error.error || "Failed to start draft"); } }; @@ -585,13 +600,20 @@ export default function DraftRoom() { const formData = new FormData(); formData.append("seasonId", season.id); - const response = await fetch("/api/draft/pause", { - method: "POST", - body: formData, - }); + try { + const response = await fetch("/api/draft/pause", { + method: "POST", + body: formData, + }); - if (response.ok) { - setIsPaused(true); + if (response.ok) { + setIsPaused(true); + } else { + const error = await response.json(); + toast.error(error.error || "Failed to pause draft"); + } + } catch { + toast.error("Network error - failed to pause draft"); } }; @@ -599,13 +621,20 @@ export default function DraftRoom() { const formData = new FormData(); formData.append("seasonId", season.id); - const response = await fetch("/api/draft/resume", { - method: "POST", - body: formData, - }); + try { + const response = await fetch("/api/draft/resume", { + method: "POST", + body: formData, + }); - if (response.ok) { - setIsPaused(false); + if (response.ok) { + setIsPaused(false); + } else { + const error = await response.json(); + toast.error(error.error || "Failed to resume draft"); + } + } catch { + toast.error("Network error - failed to resume draft"); } }; @@ -619,11 +648,9 @@ export default function DraftRoom() { body: formData, }); - if (response.ok) { - // Pick will be broadcast via socket, no need to manually update - } else { + if (!response.ok) { const error = await response.json(); - alert(error.error || "Failed to make pick"); + toast.error(error.error || "Failed to make pick"); } }; @@ -638,11 +665,9 @@ export default function DraftRoom() { body: formData, }); - if (response.ok) { - // Pick will be broadcast via socket - } else { + if (!response.ok) { const error = await response.json(); - alert(error.error || "Failed to force autopick"); + toast.error(error.error || "Failed to force autopick"); } }; @@ -665,7 +690,7 @@ export default function DraftRoom() { setSelectedPickSlot(null); } else { const error = await response.json(); - alert(error.error || "Failed to force manual pick"); + toast.error(error.error || "Failed to force manual pick"); } }; @@ -677,7 +702,7 @@ export default function DraftRoom() { const isMyTurn = !!( userTeam && currentDraftSlot && currentDraftSlot.team.id === userTeam.id ); - const canPick = isMyTurn && season.status === "draft"; // Only team owner on their turn + const canPick = isMyTurn && season.status === "draft" && !isPaused; // Only team owner on their turn when draft is active // Generate snake draft grid structure const draftGrid = useMemo(() => { @@ -746,32 +771,34 @@ export default function DraftRoom() { ); // Filter participants based on search, sport, drafted status, and eligibility - const filteredParticipants = availableParticipants.filter( - (participant: any) => { - // Drafted filter - hide drafted participants by default - if (hideDrafted && draftedParticipantIds.has(participant.id)) { - return false; - } - // Eligibility filter - hide ineligible participants by default (if user has a team) - if (hideIneligible && eligibility) { - const isEligible = eligibility.eligibleSportIds.has(participant.sport.id); - if (!isEligible) { + const filteredParticipants = useMemo( + () => + availableParticipants.filter((participant: any) => { + // Drafted filter - hide drafted participants by default + if (hideDrafted && draftedParticipantIds.has(participant.id)) { return false; } - } - // Search filter - if ( - searchQuery && - !participant.name.toLowerCase().includes(searchQuery.toLowerCase()) - ) { - return false; - } - // Sport filter - if (sportFilter !== "all" && participant.sport.name !== sportFilter) { - return false; - } - return true; - } + // Eligibility filter - hide ineligible participants by default (if user has a team) + if (hideIneligible && eligibility) { + const isEligible = eligibility.eligibleSportIds.has(participant.sport.id); + if (!isEligible) { + return false; + } + } + // Search filter + if ( + searchQuery && + !participant.name.toLowerCase().includes(searchQuery.toLowerCase()) + ) { + return false; + } + // Sport filter + if (sportFilter !== "all" && participant.sport.name !== sportFilter) { + return false; + } + return true; + }), + [availableParticipants, hideDrafted, hideIneligible, eligibility, draftedParticipantIds, searchQuery, sportFilter] ); return ( @@ -844,7 +871,7 @@ export default function DraftRoom() { )} - {isCommissioner && season.status === "draft" && ( + {isCommissioner && season.status === "draft" && !isDraftComplete && ( <> {isPaused ? (
- + Draft + + + + ); + })} + + - - - )} + + + + + + + {/* Connection Overlay - blocks interaction until socket connects */}