# Draft Room Implementation Plan ## Overview Build a real-time draft room using Socket.IO for live updates, with snake draft grid, player selection, per-team queue, and chess clock timers. --- ## Phase 1: Database Schema & Models ### 1.1 New Tables **`draft_picks`** ```sql - id (uuid, primary key) - season_id (uuid, references seasons) - team_id (uuid, references teams) - participant_id (uuid, references participants) - pick_number (integer) - overall pick number - round (integer) - pick_in_round (integer) - picked_by_user_id (varchar) - Clerk user ID - picked_by_type (enum: 'owner', 'commissioner', 'auto') - who made the pick - time_used (integer) - seconds used for this pick - created_at (timestamp) ``` **`draft_queue`** ```sql - id (uuid, primary key) - season_id (uuid, references seasons) - team_id (uuid, references teams) - participant_id (uuid, references participants) - queue_position (integer) - created_at (timestamp) - updated_at (timestamp) ``` **`draft_timers`** ```sql - id (uuid, primary key) - season_id (uuid, references seasons) - team_id (uuid, references teams) - time_remaining (integer) - seconds remaining - updated_at (timestamp) ``` ### 1.2 Schema Updates **Update `seasons` table:** ```sql - draft_initial_time (integer) - seconds, default 120 (2 minutes) - draft_increment_time (integer) - seconds, default 30 - current_pick_number (integer) - track current pick - draft_started_at (timestamp) - draft_paused (boolean, default false) ``` ### 1.3 Model Functions - `createDraftPick()` - `getDraftPicks(seasonId)` - `getCurrentPick(seasonId)` - `addToQueue(teamId, participantId)` - `removeFromQueue(queueId)` - `reorderQueue(teamId, participantIds[])` - `getTeamQueue(teamId)` - `updateTeamTimer(teamId, timeRemaining)` - `getTeamTimers(seasonId)` - `initializeDraftTimers(seasonId)` - set initial time for all teams - `autoPickForTeam(teamId)` - pick from queue or top EV --- ## Phase 2: League Settings Updates ### 2.1 Add Timer Configuration to Settings Page - Add fields to "Draft Rounds" card: - Initial Time (seconds) - default 120 - Increment Time (seconds) - default 30 - Update action handler to save these values - Show in league info panel --- ## Phase 3: Socket.IO Setup ### Overview Socket.IO requires access to the HTTP server instance (not just the Express app). The key pattern is: ```javascript const server = http.createServer(app); const io = socketIo(server); server.listen(port); // NOT app.listen() ``` ### Architecture Decision **Option A: Modify server.js (Recommended)** - Change `app.listen()` to `http.createServer(app)` + `server.listen()` - Initialize Socket.IO with the HTTP server - Keep everything in one process - ✅ Simple, standard pattern - ✅ Socket.IO and Express share same port - ✅ Works with React Router v7 SSR - ❌ Requires modifying server.js **Option B: Separate Socket Server** - Run Socket.IO on different port (e.g., 3001) - Keep server.js untouched - ✅ No changes to existing server - ❌ CORS complexity - ❌ Two processes to manage - ❌ Different ports for HTTP and WebSocket **Decision: Use Option A** - Industry standard, simpler deployment, better DX ### 3.1 Server Setup #### Step 1: Modify server.js to use HTTP server **File: `server.js`** Change from: ```javascript app.listen(PORT, () => { console.log(`Server is running on http://localhost:${PORT}`); }); ``` To: ```javascript import { createServer } from "http"; const httpServer = createServer(app); // Import and initialize Socket.IO const { initializeSocketIO } = await import("./server/socket.js"); initializeSocketIO(httpServer); httpServer.listen(PORT, () => { console.log(`Server is running on http://localhost:${PORT}`); }); ``` #### Step 2: Create Socket.IO module **File: `server/socket.js`** (use .js not .ts to avoid bundling issues) ```javascript import { Server as SocketIOServer } from "socket.io"; let io; export function initializeSocketIO(httpServer) { if (io) { console.log("Socket.IO already initialized"); return io; } io = new SocketIOServer(httpServer, { cors: { origin: process.env.NODE_ENV === "production" ? process.env.APP_URL : `http://localhost:${process.env.PORT || 3000}`, credentials: true, }, }); io.on("connection", (socket) => { console.log("Client connected:", socket.id); // Join draft room socket.on("join-draft", (seasonId) => { socket.join(`draft-${seasonId}`); console.log(`Socket ${socket.id} joined draft-${seasonId}`); }); // Leave draft room socket.on("leave-draft", (seasonId) => { socket.leave(`draft-${seasonId}`); console.log(`Socket ${socket.id} left draft-${seasonId}`); }); socket.on("disconnect", () => { console.log("Client disconnected:", socket.id); }); }); // Store globally for access in route handlers global.__socketIO = io; console.log("Socket.IO initialized"); return io; } export function getSocketIO() { if (!io && !global.__socketIO) { throw new Error("Socket.IO not initialized"); } return io || global.__socketIO; } ``` #### Step 3: Add global type definition **File: `server/types.d.ts`** ```typescript import type { Server as SocketIOServer } from "socket.io"; declare global { var __socketIO: SocketIOServer | undefined; } export {}; ``` #### Step 4: Update tsconfig.node.json Add server directory to includes: ```json { "include": [ "server.js", "server/**/*.js", "server/**/*.ts", "vite.config.ts" ] } ``` ### 3.2 Socket Events (Server) **Listen for:** - `join-draft` - user joins draft room - `leave-draft` - user leaves draft room - `make-pick` - user makes a pick (handled via HTTP POST, not socket) - `add-to-queue` - add player to queue (handled via HTTP POST) - `remove-from-queue` - remove from queue (handled via HTTP POST) - `reorder-queue` - reorder queue (handled via HTTP POST) **Note:** Write operations (picks, queue changes) should use HTTP POST requests for reliability and error handling. Socket.IO is for broadcasting updates to all clients. **Emit (from server to clients):** - `pick-made` - broadcast new pick to room - `timer-update` - broadcast timer updates (every second) - `current-pick-changed` - notify whose turn it is - `draft-started` - draft has begun - `draft-paused` - draft paused/resumed - `draft-completed` - all picks made - `queue-updated` - queue changed (only to team owner) ### 3.3 Client Hook **File: `app/hooks/useDraftSocket.ts`** ```typescript import { useEffect, useRef, useState } from "react"; import { io, Socket } from "socket.io-client"; export function useDraftSocket(seasonId: string) { const socketRef = useRef(null); const [isConnected, setIsConnected] = useState(false); useEffect(() => { // Connect to Socket.IO server const socket = io({ path: "/socket.io", transports: ["websocket", "polling"], }); socketRef.current = socket; socket.on("connect", () => { console.log("Connected to Socket.IO"); setIsConnected(true); // Join the draft room socket.emit("join-draft", seasonId); }); socket.on("disconnect", () => { console.log("Disconnected from Socket.IO"); setIsConnected(false); }); // Cleanup on unmount return () => { if (socket) { socket.emit("leave-draft", seasonId); socket.disconnect(); } }; }, [seasonId]); // Helper to subscribe to events const on = (event: string, callback: (...args: any[]) => void) => { socketRef.current?.on(event, callback); }; // Helper to unsubscribe from events const off = (event: string, callback?: (...args: any[]) => void) => { socketRef.current?.off(event, callback); }; return { socket: socketRef.current, isConnected, on, off }; } ``` ### 3.4 Using Socket in Route Handlers When a pick is made via HTTP POST, broadcast to all clients: **Example in `app/routes/api/draft-pick.ts`:** ```typescript import { getSocketIO } from "~/server/socket.js"; export async function action({ request }: ActionFunctionArgs) { // ... validate and create pick ... const pick = await createDraftPick({...}); // Broadcast to all clients in the draft room const io = getSocketIO(); io.to(`draft-${seasonId}`).emit("pick-made", { pick, nextPickNumber, isDraftComplete, }); return json({ success: true, pick }); } ``` ### 3.5 Testing Socket.IO 1. Start dev server: `npm run dev` 2. Check console for "Socket.IO initialized" 3. Open browser console on draft page 4. Should see "Connected to Socket.IO" 5. Check server logs for "Client connected: [socket-id]" ### 3.6 Gotchas & Important Notes #### ⚠️ Gotcha #1: Import path in route handlers ```typescript // ❌ WRONG - This won't work in React Router v7 import { getSocketIO } from "~/server/socket.js"; // ✅ CORRECT - Use relative path or configure alias import { getSocketIO } from "../../../server/socket.js"; ``` **Why:** React Router v7 runs server code, but `~` alias typically only works for app code. You may need to configure the import path or use relative imports. #### ⚠️ Gotcha #2: Client hook creates new connection on every render The current `useDraftSocket` hook will reconnect if `seasonId` changes. This is correct, but be aware: - Don't call this hook multiple times in the same component tree - Don't pass a changing `seasonId` unless you want to switch rooms - Consider memoizing the `seasonId` prop #### ⚠️ Gotcha #3: Event listener cleanup ```typescript // ❌ WRONG - Listeners not cleaned up useEffect(() => { socket.on("pick-made", handlePickMade); }, []); // ✅ CORRECT - Clean up listeners useEffect(() => { socket.on("pick-made", handlePickMade); return () => { socket.off("pick-made", handlePickMade); }; }, [handlePickMade]); ``` #### ⚠️ Gotcha #4: Socket.IO path in production If you deploy behind a reverse proxy (nginx, etc.), you may need to configure the Socket.IO path: ```javascript // Client const socket = io({ path: "/socket.io", // Make sure this matches your proxy config }); // Server (if needed) const io = new SocketIOServer(httpServer, { path: "/socket.io", }); ``` #### ⚠️ Gotcha #5: CORS might not be needed Since Socket.IO runs on the same server/port as your app, CORS might be unnecessary for same-origin requests. You can simplify: ```javascript // Simpler config if same-origin const io = new SocketIOServer(httpServer); ``` #### ⚠️ Gotcha #6: Global storage in development The global `__socketIO` pattern works, but in development with HMR, you might get multiple instances. The `if (io)` check helps, but be aware of potential stale connections during hot reloads. #### ⚠️ Gotcha #7: TypeScript import in route handlers ```typescript // This might cause issues because socket.js is JavaScript import { getSocketIO } from "~/server/socket.js"; // Consider making it TypeScript with proper types // Or use @ts-ignore if necessary // @ts-ignore import { getSocketIO } from "~/server/socket.js"; ``` ### 3.7 Production Considerations - Socket.IO will automatically upgrade from polling to WebSocket - Ensure load balancer supports WebSocket (sticky sessions required!) - Consider Redis adapter for multi-server deployments (future) - Monitor connection counts and memory usage - Set up proper error handling for disconnections - Consider implementing reconnection logic with exponential backoff --- ## Phase 4: Draft Room Route & Layout ### 4.1 Route Setup - Create `/app/routes/leagues/$leagueId.draft.tsx` - Loader: - Verify user access (team owner or commissioner) - Load season, teams, draft slots, participants - Load existing draft picks - Load user's team queue - Load timer states - No action needed (all via Socket.IO) ### 4.2 Layout Structure ``` ┌─────────────────────────────────────────────────┐ │ Draft Room Header │ │ League Name | Status | Commissioner Controls │ └─────────────────────────────────────────────────┘ ┌─────────────────────────────────────────────────┐ │ Draft Grid (horizontally scrollable) │ │ Team 1 Team 2 Team 3 ... │ │ 5:23 4:12 3:45 ... (timers) │ │ ┌────┐ ┌────┐ ┌────┐ │ │ │1.01│ │1.02│ │1.03│ ... │ │ └────┘ └────┘ └────┘ │ │ ┌────┐ ┌────┐ ┌────┐ │ │ │2.03│ │2.02│ │2.01│ ... (snake) │ │ └────┘ └────┘ └────┘ │ └─────────────────────────────────────────────────┘ ┌──────────────────────┬──────────────────────────┐ │ Available Players │ My Queue │ │ [Search: ____] │ 1. Player A │ │ [x] Hide Drafted │ 2. Player B │ │ │ 3. Player C │ │ □ Player 1 - NFL │ [Clear Queue] │ │ EV: 125 │ │ │ □ Player 2 - NBA │ │ │ EV: 118 │ │ │ ⊠ Player 3 - MLB │ │ │ (DRAFTED) │ │ └──────────────────────┴──────────────────────────┘ ``` ### 4.3 No Navigation Bar - Create standalone layout without main nav - Add "Exit Draft Room" button to return to league page --- ## Phase 5: Draft Grid Component ### 5.1 Calculate Snake Order - Use existing draft slots for team order - Generate pick order: - Round 1: slots in order (1→N) - Round 2: slots reversed (N→1) - Round 3: slots in order (1→N) - etc. ### 5.2 Grid Display - Show team names as column headers - Show timer under each team name - Render grid cells with pick numbers (1.01, 1.02, etc.) - Highlight current pick (border, background color) - Show drafted participant in completed picks - Responsive: horizontal scroll on smaller screens ### 5.3 Pick Cell Component ```tsx ``` --- ## Phase 6: Player List Component ### 6.1 Player List Features - Display all participants from season sports - Show: "Participant Name - Sport" - Show expected value - Sort by: EV (desc), then alphabetical - Search/filter by name - Toggle to hide/show drafted players - Click to add to queue (if not drafted) - Visual indicator for drafted players ### 6.2 Player Item Component ```tsx ``` --- ## Phase 7: Queue Component ### 7.1 Queue Features - Show only current user's team queue - Display in order (1, 2, 3, ...) - Drag & drop to reorder - Remove button for each item - "Clear Queue" button - Real-time sync via Socket.IO ### 7.2 Queue Actions - Add player → emit `add-to-queue` → update DB → emit `queue-updated` to team - Remove → emit `remove-from-queue` → update DB → emit `queue-updated` - Reorder → emit `reorder-queue` → update DB → emit `queue-updated` --- ## Phase 8: Timer System ### 8.1 Timer Display - Show under each team name in grid - Format: - "M:SS" for times under 1 hour (e.g., "5:23") - "H:MM:SS" for times 1 hour or more (e.g., "2:15:30") - Color coding: - Green: > 60s - Yellow: 30-60s - Red: < 30s - Flashing red: < 10s ### 8.2 Timer Logic (Server-Side) - When draft starts: initialize all timers with `draft_initial_time` - During a team's turn: - Their timer counts down every second - Other teams' timers remain paused - When pick is made: - Stop countdown for current team - Add `draft_increment_time` to current team's timer - Move to next pick - Start next team's timer countdown - Emit `timer-update` every second to all clients - When timer reaches 0: trigger `autoPickForTeam()` ### 8.3 Auto-Pick Function ```typescript async function autoPickForTeam(teamId: string, seasonId: string) { // 1. Check queue - pick first item // 2. If queue empty, pick highest EV participant not drafted // 3. Create draft pick with picked_by_type='auto' // 4. Emit pick-made event // 5. Move to next pick } ``` --- ## Phase 9: Draft Actions & Permissions ### 9.1 Making a Pick - Only current team owner or commissioner can pick - Click participant in player list (if current turn) - Validate: participant not already drafted - Create draft pick record - Emit `pick-made` to room - Update current pick - Update timers ### 9.2 Commissioner Controls - Start Draft button (if not started) - Pause/Resume button - Force Pick button (for current team) - Show in header, only visible to commissioners ### 9.3 Draft Start Logic - Can be started manually by commissioner - Or auto-start when `draftDateTime` is reached (background job?) - Change season status to "drafting" - Initialize timers - Emit `draft-started` ### 9.4 Draft Completion - When all picks made (picks = teams × rounds): - Change season status to "active" - Emit `draft-completed` - Stop timers --- ## Phase 10: Real-Time Updates ### 10.1 Client State Management - Use React state for: - Draft picks array - Current pick number - Team timers - User's queue - Available participants - Update state when Socket.IO events received ### 10.2 Optimistic Updates - When user makes pick: update UI immediately - If server rejects: revert and show error - For queue operations: update immediately --- ## Phase 11: Mobile Responsiveness ### 11.1 Mobile Layout Adjustments - Draft grid: horizontal scroll - Stack player list and queue vertically - Collapsible sections for player list/queue - Larger touch targets for buttons - Simplified timer display --- ## Implementation Order 1. **Phase 1**: Database schema & migrations 2. **Phase 2**: League settings timer config 3. **Phase 3**: Socket.IO setup (server + client hook) 4. **Phase 4**: Draft room route & basic layout 5. **Phase 5**: Draft grid component (static first) 6. **Phase 6**: Player list component 7. **Phase 7**: Queue component 8. **Phase 8**: Timer system (server-side logic) 9. **Phase 9**: Draft actions & permissions 10. **Phase 10**: Wire up real-time updates 11. **Phase 11**: Mobile polish --- ## Open Questions / Future Considerations - **Background job for auto-start**: How to trigger draft start at `draftDateTime`? Cron job? Polling? - **Timer persistence**: What if server restarts during draft? Store timer state in DB? - **Undo picks**: Not in initial scope, but may want later - **Draft history/log**: Separate view to see all picks in order? - **Notifications**: Alert users when it's their turn? (email, push, etc.) --- ## Requirements Summary ### User Requirements - Route: `/leagues/$leagueId/draft` - Queue: Per-team, private (stored in database) - Timer: Chess clock (initial time + increment per pick) - Real-time: WebSocket (Socket.IO) - Mobile: Should work on mobile - Commissioner: Can force picks - Participant sorting: By EV (desc), then alphabetical - Search: Filter participants by name - Toggle: Show/hide drafted participants - Teams: 6-16 teams typical (12 average) - Timer config: Configurable per league/season - Auto-pick: From queue, or top EV if queue empty - Draft state: "drafting" status - Start: Auto-start at draft date/time, or manual by commissioner - Pause/Resume: Commissioner can pause/resume - Pick log: Track who made each pick (owner/commissioner/auto) - Grid: Horizontally scrollable - Player display: "Player Name - Sport" - Pre-draft: Room viewable before draft starts - Post-draft: Change status to "active" when complete