From 5b10548f138ba1ff6b9df01d36e07d6ae2dcdb9e Mon Sep 17 00:00:00 2001 From: Chris Parsons Date: Sun, 26 Oct 2025 21:01:12 -0700 Subject: [PATCH] feat: Add ConnectionOverlay component for improved socket connection handling and user feedback --- app/components/AutodraftSettings.tsx | 7 -- app/components/draft/ConnectionOverlay.tsx | 103 ++++++++++++++++++ app/hooks/useDraftSocket.ts | 40 ++++++- .../leagues/$leagueId.draft.$seasonId.tsx | 10 +- 4 files changed, 149 insertions(+), 11 deletions(-) create mode 100644 app/components/draft/ConnectionOverlay.tsx diff --git a/app/components/AutodraftSettings.tsx b/app/components/AutodraftSettings.tsx index 39568a7..9006589 100644 --- a/app/components/AutodraftSettings.tsx +++ b/app/components/AutodraftSettings.tsx @@ -22,7 +22,6 @@ export function AutodraftSettings({ }: AutodraftSettingsProps) { const [localEnabled, setLocalEnabled] = useState(isEnabled); const [localMode, setLocalMode] = useState<"next_pick" | "while_on">(mode); - const [isUpdating, setIsUpdating] = useState(false); // Sync local state with props when they change (from socket events) useEffect(() => { @@ -35,7 +34,6 @@ export function AutodraftSettings({ const handleToggle = async (checked: boolean) => { setLocalEnabled(checked); - setIsUpdating(true); try { const formData = new FormData(); @@ -60,14 +58,11 @@ export function AutodraftSettings({ // Revert on error setLocalEnabled(!checked); console.error("Error updating autodraft settings:", error); - } finally { - setIsUpdating(false); } }; const handleModeChange = async (newMode: "next_pick" | "while_on") => { setLocalMode(newMode); - setIsUpdating(true); try { const formData = new FormData(); @@ -92,8 +87,6 @@ export function AutodraftSettings({ // Revert on error setLocalMode(localMode); console.error("Error updating autodraft mode:", error); - } finally { - setIsUpdating(false); } }; diff --git a/app/components/draft/ConnectionOverlay.tsx b/app/components/draft/ConnectionOverlay.tsx new file mode 100644 index 0000000..044d34b --- /dev/null +++ b/app/components/draft/ConnectionOverlay.tsx @@ -0,0 +1,103 @@ +import { Button } from "~/components/ui/button"; +import { Card } from "~/components/ui/card"; + +interface ConnectionOverlayProps { + isConnected: boolean; + isReconnecting: boolean; + connectionError: string | null; +} + +export function ConnectionOverlay({ + isConnected, + isReconnecting, + connectionError, +}: ConnectionOverlayProps) { + // Don't show overlay if connected + if (isConnected) { + return null; + } + + return ( +
+ +
+ {/* Spinner or Error Icon */} + {connectionError ? ( +
+ + + +
+ ) : ( +
+
+
+
+ )} + + {/* Title */} +
+

+ {connectionError + ? "Connection Error" + : isReconnecting + ? "Reconnecting..." + : "Connecting to Draft"} +

+ + {/* Message */} +

+ {connectionError ? ( + connectionError + ) : isReconnecting ? ( + "Lost connection to the draft server. Attempting to reconnect..." + ) : ( + "Please wait while we connect you to the live draft room." + )} +

+
+ + {/* Action Button (only on persistent error) */} + {connectionError && ( + + )} + + {/* Loading Dots */} + {!connectionError && ( +
+
+
+
+
+ )} +
+
+
+ ); +} diff --git a/app/hooks/useDraftSocket.ts b/app/hooks/useDraftSocket.ts index 16bbabe..12480d3 100644 --- a/app/hooks/useDraftSocket.ts +++ b/app/hooks/useDraftSocket.ts @@ -4,6 +4,8 @@ import { io, Socket } from "socket.io-client"; interface UseDraftSocketReturn { socket: Socket | null; isConnected: boolean; + connectionError: string | null; + isReconnecting: boolean; on: (event: string, callback: (...args: any[]) => void) => void; off: (event: string, callback?: (...args: any[]) => void) => void; } @@ -11,6 +13,8 @@ interface UseDraftSocketReturn { export function useDraftSocket(seasonId: string, teamId?: string): UseDraftSocketReturn { const socketRef = useRef(null); const [isConnected, setIsConnected] = useState(false); + const [connectionError, setConnectionError] = useState(null); + const [isReconnecting, setIsReconnecting] = useState(false); useEffect(() => { // Connect to Socket.IO server @@ -24,17 +28,40 @@ export function useDraftSocket(seasonId: string, teamId?: string): UseDraftSocke socket.on("connect", () => { console.log("Connected to Socket.IO:", socket.id); setIsConnected(true); + setConnectionError(null); + setIsReconnecting(false); // Join the draft room with optional teamId socket.emit("join-draft", seasonId, teamId); }); - socket.on("disconnect", () => { - console.log("Disconnected from Socket.IO"); + socket.on("disconnect", (reason) => { + console.log("Disconnected from Socket.IO:", reason); setIsConnected(false); + if (reason === "io server disconnect") { + // Server disconnected the socket, need to manually reconnect + setConnectionError("Server disconnected. Please refresh the page."); + } else { + // Client disconnected or network issue, will auto-reconnect + setIsReconnecting(true); + } }); socket.on("connect_error", (error) => { console.error("Socket.IO connection error:", error); + setConnectionError(error.message || "Failed to connect to draft server"); + setIsReconnecting(false); + }); + + socket.io.on("reconnect_attempt", () => { + console.log("Attempting to reconnect..."); + setIsReconnecting(true); + setConnectionError(null); + }); + + socket.io.on("reconnect_failed", () => { + console.error("Reconnection failed"); + setConnectionError("Failed to reconnect. Please refresh the page."); + setIsReconnecting(false); }); // Cleanup on unmount @@ -57,5 +84,12 @@ export function useDraftSocket(seasonId: string, teamId?: string): UseDraftSocke socketRef.current?.off(event, callback); }; - return { socket: socketRef.current, isConnected, on, off }; + return { + socket: socketRef.current, + isConnected, + connectionError, + isReconnecting, + on, + off + }; } diff --git a/app/routes/leagues/$leagueId.draft.$seasonId.tsx b/app/routes/leagues/$leagueId.draft.$seasonId.tsx index a566361..bc9d1a0 100644 --- a/app/routes/leagues/$leagueId.draft.$seasonId.tsx +++ b/app/routes/leagues/$leagueId.draft.$seasonId.tsx @@ -16,6 +16,7 @@ import { QueueSection } from "~/components/draft/QueueSection"; import { AvailableParticipantsSection } from "~/components/draft/AvailableParticipantsSection"; import { SidebarRecentPicks } from "~/components/draft/SidebarRecentPicks"; import { DraftGridSection } from "~/components/draft/DraftGridSection"; +import { ConnectionOverlay } from "~/components/draft/ConnectionOverlay"; import { calculateDraftEligibility, getEligibilitySummary } from "~/lib/draft-eligibility"; import { toast } from "sonner"; @@ -196,7 +197,7 @@ export default function DraftRoom() { userAutodraftSettings, isCommissioner, } = useLoaderData(); - const { isConnected, on, off } = useDraftSocket(season.id, userTeam?.id); + const { isConnected, connectionError, isReconnecting, on, off } = useDraftSocket(season.id, userTeam?.id); const [picks, setPicks] = useState(draftPicks); const [currentPick, setCurrentPick] = useState(season.currentPickNumber || 1); const [searchQuery, setSearchQuery] = useState(""); @@ -1099,6 +1100,13 @@ export default function DraftRoom() { )} + + {/* Connection Overlay - blocks interaction until socket connects */} + ); }