diff --git a/app/hooks/useDraftSocket.ts b/app/hooks/useDraftSocket.ts new file mode 100644 index 0000000..88f771f --- /dev/null +++ b/app/hooks/useDraftSocket.ts @@ -0,0 +1,101 @@ +import { useEffect, useState, useCallback } from "react"; +import { io, Socket } from "socket.io-client"; + +const SOCKET_URL = typeof window !== "undefined" + ? window.location.origin + : "http://localhost:3000"; + +export interface DraftSocketEvents { + // Events emitted by server + "pick-made": (data: any) => void; + "timer-update": (data: any) => void; + "current-pick-changed": (data: any) => void; + "draft-started": (data: any) => void; + "draft-paused": (data: any) => void; + "draft-completed": (data: any) => void; + "queue-updated": (data: any) => void; + "user-joined": (data: any) => void; +} + +export function useDraftSocket(seasonId: string | null) { + const [socket, setSocket] = useState(null); + const [isConnected, setIsConnected] = useState(false); + + useEffect(() => { + if (!seasonId) return; + + // Create socket connection + const newSocket = io(SOCKET_URL, { + transports: ["websocket", "polling"], + }); + + newSocket.on("connect", () => { + console.log("Socket connected:", newSocket.id); + setIsConnected(true); + + // Join the draft room + newSocket.emit("join-draft", seasonId); + }); + + newSocket.on("disconnect", () => { + console.log("Socket disconnected"); + setIsConnected(false); + }); + + newSocket.on("connect_error", (error) => { + console.error("Socket connection error:", error); + setIsConnected(false); + }); + + setSocket(newSocket); + + // Cleanup on unmount + return () => { + if (newSocket) { + newSocket.emit("leave-draft", seasonId); + newSocket.close(); + } + }; + }, [seasonId]); + + // Helper to emit events + const emit = useCallback((event: string, data?: any) => { + if (socket && isConnected) { + socket.emit(event, data); + } else { + console.warn(`Cannot emit ${event}: socket not connected`); + } + }, [socket, isConnected]); + + // Helper to listen to events + const on = useCallback(( + event: K, + handler: DraftSocketEvents[K] + ) => { + if (socket) { + socket.on(event as string, handler as any); + } + }, [socket]); + + // Helper to remove event listeners + const off = useCallback(( + event: K, + handler?: DraftSocketEvents[K] + ) => { + if (socket) { + if (handler) { + socket.off(event as string, handler as any); + } else { + socket.off(event as string); + } + } + }, [socket]); + + return { + socket, + isConnected, + emit, + on, + off, + }; +} diff --git a/package-lock.json b/package-lock.json index 8f81a3c..c7f983d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -34,6 +34,8 @@ "react-day-picker": "^9.11.1", "react-dom": "^19.1.0", "react-router": "^7.7.1", + "socket.io": "^4.8.1", + "socket.io-client": "^4.8.1", "sonner": "^2.0.7", "svix": "^1.77.0", "tailwind-merge": "^3.3.1" @@ -51,7 +53,7 @@ "@types/react-dom": "^19.1.2", "dotenv-cli": "^8.0.0", "tailwindcss": "^4.1.4", - "tsx": "^4.19.2", + "tsx": "^4.20.6", "tw-animate-css": "^1.4.0", "typescript": "^5.8.3", "vite": "^6.3.3", @@ -2863,6 +2865,12 @@ "win32" ] }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", + "license": "MIT" + }, "node_modules/@stablelib/base64": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@stablelib/base64/-/base64-1.0.1.tgz", @@ -3178,6 +3186,15 @@ "@types/node": "*" } }, + "node_modules/@types/cors": { + "version": "2.8.19", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", + "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -3238,7 +3255,6 @@ "version": "20.19.20", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.20.tgz", "integrity": "sha512-2Q7WS25j4pS1cS8yw3d6buNCVJukOTeQ39bAnwR6sOJbaxvyCGebzTMypDFN82CxBLnl+lSWVdCCWbRY6y9yZQ==", - "devOptional": true, "license": "MIT", "dependencies": { "undici-types": "~6.21.0" @@ -3410,6 +3426,15 @@ "dev": true, "license": "MIT" }, + "node_modules/base64id": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", + "license": "MIT", + "engines": { + "node": "^4.5.0 || >= 5.9" + } + }, "node_modules/baseline-browser-mapping": { "version": "2.8.16", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.16.tgz", @@ -3735,6 +3760,19 @@ "node": ">=6.6.0" } }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -4083,6 +4121,125 @@ "node": ">= 0.8" } }, + "node_modules/engine.io": { + "version": "6.6.4", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.4.tgz", + "integrity": "sha512-ZCkIjSYNDyGn0R6ewHDtXgns/Zre/NT6Agvq1/WobF7JXgFff4SeDroKiCO3fNJreU9YG429Sc81o4w5ok/W5g==", + "license": "MIT", + "dependencies": { + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.7.2", + "cors": "~2.8.5", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.17.1" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/engine.io-client": { + "version": "6.6.3", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.3.tgz", + "integrity": "sha512-T0iLjnyNWahNyv/lcjS2y4oE358tVS/SYQNxYXGAJ9/GLgH4VCvOQ/mhTjqU88mLZCQgiG8RIegFHYCdVC+j5w==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.17.1", + "xmlhttprequest-ssl": "~2.1.1" + } + }, + "node_modules/engine.io-client/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/engine.io/node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/engine.io/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/engine.io/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/engine.io/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/engine.io/node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/enhanced-resolve": { "version": "5.18.3", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz", @@ -5257,6 +5414,15 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/object-inspect": { "version": "1.13.4", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", @@ -6065,6 +6231,173 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/socket.io": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.1.tgz", + "integrity": "sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "cors": "~2.8.5", + "debug": "~4.3.2", + "engine.io": "~6.6.0", + "socket.io-adapter": "~2.5.2", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/socket.io-adapter": { + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz", + "integrity": "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==", + "license": "MIT", + "dependencies": { + "debug": "~4.3.4", + "ws": "~8.17.1" + } + }, + "node_modules/socket.io-adapter/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io-client": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.1.tgz", + "integrity": "sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.2", + "engine.io-client": "~6.6.1", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-client/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io-parser": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", + "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io/node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/socket.io/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/socket.io/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/socket.io/node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/sonner": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/sonner/-/sonner-2.0.7.tgz", @@ -6899,7 +7232,6 @@ "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "devOptional": true, "license": "MIT" }, "node_modules/unpipe": { @@ -7730,6 +8062,35 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "license": "ISC" }, + "node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xmlhttprequest-ssl": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz", + "integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", diff --git a/package.json b/package.json index 3de31db..38e9a3a 100644 --- a/package.json +++ b/package.json @@ -6,9 +6,9 @@ "build": "react-router build", "db:generate": "dotenv -- drizzle-kit generate", "db:migrate": "dotenv -- drizzle-kit migrate", - "dev": "dotenv -- node server.js", - "start": "node server.js", - "start:production": "drizzle-kit migrate && rm -rf drizzle drizzle.config.ts database && node server.js", + "dev": "dotenv -- tsx server/index.ts", + "start": "tsx server/index.ts", + "start:production": "drizzle-kit migrate && rm -rf drizzle drizzle.config.ts database && tsx server/index.ts", "typecheck": "react-router typegen && tsc -b" }, "dependencies": { @@ -40,6 +40,8 @@ "react-day-picker": "^9.11.1", "react-dom": "^19.1.0", "react-router": "^7.7.1", + "socket.io": "^4.8.1", + "socket.io-client": "^4.8.1", "sonner": "^2.0.7", "svix": "^1.77.0", "tailwind-merge": "^3.3.1" @@ -57,7 +59,7 @@ "@types/react-dom": "^19.1.2", "dotenv-cli": "^8.0.0", "tailwindcss": "^4.1.4", - "tsx": "^4.19.2", + "tsx": "^4.20.6", "tw-animate-css": "^1.4.0", "typescript": "^5.8.3", "vite": "^6.3.3", diff --git a/server/app.ts b/server/app.ts index 9a60fc7..bca8cca 100644 --- a/server/app.ts +++ b/server/app.ts @@ -4,9 +4,9 @@ import express from "express"; import postgres from "postgres"; import { RouterContextProvider } from "react-router"; -import { DatabaseContext } from "~/database/context"; -import * as schema from "~/database/schema"; -import { expressValueContext } from "~/contexts/express"; +import { DatabaseContext } from "../database/context.js"; +import * as schema from "../database/schema.js"; +import { expressValueContext } from "../app/contexts/express.js"; export const app = express(); @@ -22,7 +22,7 @@ app.use( getLoadContext() { const context = new RouterContextProvider(); context.set(expressValueContext, "Hello from Express"); - return context; + return context as any; }, }), ); diff --git a/server.js b/server/index.ts similarity index 79% rename from server.js rename to server/index.ts index 50fea2b..2d10f29 100644 --- a/server.js +++ b/server/index.ts @@ -1,5 +1,6 @@ import compression from "compression"; import express from "express"; +import { createServer } from "http"; import morgan from "morgan"; // Short-circuit the type-checking of the built output. @@ -8,6 +9,7 @@ const DEVELOPMENT = process.env.NODE_ENV === "development"; const PORT = Number.parseInt(process.env.PORT || "3000"); const app = express(); +const httpServer = createServer(app); app.use(compression()); app.disable("x-powered-by"); @@ -42,6 +44,13 @@ if (DEVELOPMENT) { app.use(await import(BUILD_PATH).then((mod) => mod.app)); } -app.listen(PORT, () => { +// Initialize Socket.IO +const socketModule = await import("./socket.js"); +const io = socketModule.setupSocketIO(httpServer); +socketModule.setSocketIO(io); +console.log("Socket.IO initialized"); + +httpServer.listen(PORT, () => { console.log(`Server is running on http://localhost:${PORT}`); + console.log(`Socket.IO is ready for connections`); }); diff --git a/server/socket.ts b/server/socket.ts new file mode 100644 index 0000000..8f45ee9 --- /dev/null +++ b/server/socket.ts @@ -0,0 +1,59 @@ +import { Server as HTTPServer } from "http"; +import { Server, Socket } from "socket.io"; + +export type SocketServer = Server; +export type SocketClient = Socket; + +export function setupSocketIO(httpServer: HTTPServer): SocketServer { + const io = new Server(httpServer, { + cors: { + origin: process.env.CLIENT_URL || "*", + methods: ["GET", "POST"], + }, + }); + + io.on("connection", (socket: SocketClient) => { + console.log(`Client connected: ${socket.id}`); + + // Join a draft room + socket.on("join-draft", (seasonId: string) => { + const roomName = `draft-${seasonId}`; + socket.join(roomName); + console.log(`Socket ${socket.id} joined draft room: ${roomName}`); + + // Notify others in the room + socket.to(roomName).emit("user-joined", { socketId: socket.id }); + }); + + // Leave a draft room + socket.on("leave-draft", (seasonId: string) => { + const roomName = `draft-${seasonId}`; + socket.leave(roomName); + console.log(`Socket ${socket.id} left draft room: ${roomName}`); + }); + + // Handle disconnection + socket.on("disconnect", () => { + console.log(`Client disconnected: ${socket.id}`); + }); + + // Draft-specific events will be added in Phase 9 + // For now, we're just setting up the infrastructure + }); + + return io; +} + +// Export a singleton instance that will be set by the server +let ioInstance: SocketServer | null = null; + +export function setSocketIO(io: SocketServer) { + ioInstance = io; +} + +export function getSocketIO(): SocketServer { + if (!ioInstance) { + throw new Error("Socket.IO has not been initialized"); + } + return ioInstance; +} diff --git a/server/types.d.ts b/server/types.d.ts new file mode 100644 index 0000000..65fd064 --- /dev/null +++ b/server/types.d.ts @@ -0,0 +1,7 @@ +// Type declarations for server-side modules + +declare module "virtual:react-router/server-build" { + import type { ServerBuild } from "@react-router/node"; + const serverBuild: ServerBuild; + export = serverBuild; +} diff --git a/tsconfig.node.json b/tsconfig.node.json index 12107b4..afaea84 100644 --- a/tsconfig.node.json +++ b/tsconfig.node.json @@ -1,6 +1,11 @@ { "extends": "./tsconfig.json", - "include": ["server.js", "vite.config.ts"], + "include": [ + "server/**/*.ts", + "vite.config.ts", + "database/**/*.ts", + "app/contexts/**/*.ts" + ], "compilerOptions": { "composite": true, "strict": true,