From 61f3fa9a4e9f9ffdd936a40b086ceff2d68f1103 Mon Sep 17 00:00:00 2001 From: Chris Parsons <438676+chrisparsons83@users.noreply.github.com> Date: Tue, 24 Feb 2026 11:31:25 -0800 Subject: [PATCH] fix: optimistic UI and type safety for draft queue handlers (#37) - Add optimistic remove to handleRemoveFromQueue (was missing, causing visible lag) - Fix stale closure on previousQueue in reorder/remove by capturing inside setQueue updater - Replace filter(Boolean) with typed type guard filter in handleReorderQueue - Add QueueItem type alias from schema.$inferSelect, typed useState, remove all `any` casts - Complete temp item fields in handleAddToQueue to satisfy QueueItem type Co-authored-by: Claude Sonnet 4.6 --- .../leagues/$leagueId.draft.$seasonId.tsx | 49 ++++++++++++++----- 1 file changed, 36 insertions(+), 13 deletions(-) diff --git a/app/routes/leagues/$leagueId.draft.$seasonId.tsx b/app/routes/leagues/$leagueId.draft.$seasonId.tsx index 17631dd..4a9552f 100644 --- a/app/routes/leagues/$leagueId.draft.$seasonId.tsx +++ b/app/routes/leagues/$leagueId.draft.$seasonId.tsx @@ -29,6 +29,8 @@ import { formatClockTime, getTimerColorClass } from "~/lib/draft-timer"; import { Users, LayoutGrid, ListChecks, Settings } from "lucide-react"; import type { Route } from "./+types/$leagueId.draft.$seasonId"; +type QueueItem = typeof schema.draftQueue.$inferSelect; + const MOBILE_TABS = [ { id: "lobby" as const, label: "Lobby", Icon: Users }, { id: "board" as const, label: "Board", Icon: LayoutGrid }, @@ -250,7 +252,7 @@ export default function DraftRoom() { const [hideDrafted, setHideDrafted] = useState(true); const [hideIneligible, setHideIneligible] = useState(true); const [sportFilter, setSportFilter] = useState("all"); - const [queue, setQueue] = useState(userQueue); + const [queue, setQueue] = useState(userQueue); const [isPaused, setIsPaused] = useState(season.draftPaused || false); const [isDraftComplete, setIsDraftComplete] = useState( season.status === "active" || season.status === "completed" @@ -566,21 +568,26 @@ export default function DraftRoom() { if (!userTeam) return; // Check if participant is already in queue (client-side) - const alreadyInQueue = queue.some((item: any) => item.participantId === participantId); + const alreadyInQueue = queue.some((item) => item.participantId === participantId); if (alreadyInQueue) { toast.error("This participant is already in your queue"); return; } // Optimistic update - create a temporary queue item - const tempQueueItem = { - id: `temp-${Date.now()}`, // Temporary ID + const now = new Date(); + const tempQueueItem: QueueItem = { + id: `temp-${Date.now()}`, participantId, queuePosition: queue.length + 1, + seasonId: season.id, + teamId: userTeam.id, + createdAt: now, + updatedAt: now, }; // Immediately update the UI - setQueue((prev: any) => [...prev, tempQueueItem]); + setQueue((prev) => [...prev, tempQueueItem]); const formData = new FormData(); formData.append("seasonId", season.id); @@ -596,25 +603,21 @@ export default function DraftRoom() { if (response.ok) { const data = await response.json(); // Replace temp item with real item from server - setQueue((prev: any) => - prev.map((item: any) => + setQueue((prev) => + prev.map((item) => item.id === tempQueueItem.id ? data.queueItem : item ) ); } else { // Revert the optimistic update on error - setQueue((prev: any) => - prev.filter((item: any) => item.id !== tempQueueItem.id) - ); + setQueue((prev) => prev.filter((item) => item.id !== tempQueueItem.id)); const error = await response.json(); toast.error(error.error || "Failed to add to queue"); } } catch (error) { // Revert the optimistic update on network error - setQueue((prev: any) => - prev.filter((item: any) => item.id !== tempQueueItem.id) - ); + setQueue((prev) => prev.filter((item) => item.id !== tempQueueItem.id)); toast.error("Network error - failed to add to queue"); } }; @@ -622,6 +625,13 @@ export default function DraftRoom() { const handleRemoveFromQueue = async (queueId: string) => { if (!userTeam) return; + // Optimistically remove the item immediately + let previousQueue: QueueItem[] = []; + setQueue((prev) => { + previousQueue = prev; + return prev.filter((item) => item.id !== queueId); + }); + const formData = new FormData(); formData.append("queueId", queueId); formData.append("teamId", userTeam.id); @@ -636,10 +646,12 @@ export default function DraftRoom() { const data = await response.json(); setQueue(data.queue); } else { + setQueue(previousQueue); const error = await response.json(); toast.error(error.error || "Failed to remove from queue"); } } catch { + setQueue(previousQueue); toast.error("Network error - failed to remove from queue"); } }; @@ -647,6 +659,15 @@ export default function DraftRoom() { const handleReorderQueue = async (participantIds: string[]) => { if (!userTeam) return; + // Optimistically reorder the queue immediately + let previousQueue: QueueItem[] = []; + setQueue((prev) => { + previousQueue = prev; + return participantIds + .map((pid) => prev.find((item) => item.participantId === pid)) + .filter((item): item is QueueItem => item !== undefined); + }); + const formData = new FormData(); formData.append("teamId", userTeam.id); formData.append("seasonId", season.id); @@ -663,9 +684,11 @@ export default function DraftRoom() { setQueue(data.queue); } else { const error = await response.json(); + setQueue(previousQueue); toast.error(error.error || "Failed to reorder queue"); } } catch { + setQueue(previousQueue); toast.error("Network error - failed to reorder queue"); } };