diff --git a/apps/web/.claude/settings.local.json b/apps/web/.claude/settings.local.json index e62377f3..a352ee6e 100644 --- a/apps/web/.claude/settings.local.json +++ b/apps/web/.claude/settings.local.json @@ -32,7 +32,9 @@ "Bash(git commit -m \"$(cat <<''EOF''\nfeat: add Biome + ESLint linting setup\n\nAdd Biome for formatting and general linting, with minimal ESLint\nconfiguration for React Hooks rules only. This provides:\n\n- Fast formatting via Biome (10-100x faster than Prettier)\n- General JS/TS linting via Biome\n- React Hooks validation via ESLint (rules-of-hooks)\n- Import organization via Biome\n\nConfiguration files:\n- biome.jsonc: Biome config with custom rule overrides\n- eslint.config.js: Minimal flat config for React Hooks only\n- .gitignore: Added Biome cache exclusion\n- LINTING.md: Documentation for the setup\n\nScripts added to package.json:\n- npm run lint: Check all files\n- npm run lint:fix: Auto-fix issues\n- npm run format: Format all files\n- npm run check: Full Biome check\n\n🤖 Generated with [Claude Code](https://claude.com/claude-code)\n\nCo-Authored-By: Claude \nEOF\n)\")", "Bash(git commit:*)", "Bash(npm run pre-commit:*)", - "Bash(npm run:*)" + "Bash(npm run:*)", + "Bash(git pull:*)", + "Bash(git stash:*)" ], "deny": [], "ask": [] diff --git a/apps/web/socket-server.ts b/apps/web/socket-server.ts index 262ea9b8..0e32d848 100644 --- a/apps/web/socket-server.ts +++ b/apps/web/socket-server.ts @@ -1,5 +1,6 @@ import type { Server as HTTPServer } from 'http' import { Server as SocketIOServer } from 'socket.io' +import type { Server as SocketIOServerType } from 'socket.io' import { applyGameMove, createArcadeSession, @@ -18,8 +19,19 @@ import { getRoomActivePlayers } from './src/lib/arcade/player-manager' import type { GameMove, GameName } from './src/lib/arcade/validation' import { matchingGameValidator } from './src/lib/arcade/validation/MatchingGameValidator' +// Global socket.io server instance +let io: SocketIOServerType | null = null + +/** + * Get the socket.io server instance + * Returns null if not initialized + */ +export function getSocketIO(): SocketIOServerType | null { + return io +} + export function initializeSocketServer(httpServer: HTTPServer) { - const io = new SocketIOServer(httpServer, { + io = new SocketIOServer(httpServer, { path: '/api/socket', cors: { origin: process.env.NEXT_PUBLIC_URL || 'http://localhost:3000', diff --git a/apps/web/src/app/api/arcade/rooms/[roomId]/join/route.ts b/apps/web/src/app/api/arcade/rooms/[roomId]/join/route.ts index bc0c5976..ebc5a7c6 100644 --- a/apps/web/src/app/api/arcade/rooms/[roomId]/join/route.ts +++ b/apps/web/src/app/api/arcade/rooms/[roomId]/join/route.ts @@ -1,8 +1,9 @@ import { type NextRequest, NextResponse } from 'next/server' import { getRoomById, touchRoom } from '@/lib/arcade/room-manager' -import { addRoomMember } from '@/lib/arcade/room-membership' -import { getActivePlayers } from '@/lib/arcade/player-manager' +import { addRoomMember, getOnlineRoomMembers } from '@/lib/arcade/room-membership' +import { getActivePlayers, getRoomActivePlayers } from '@/lib/arcade/player-manager' import { getViewerId } from '@/lib/viewer' +import { getSocketIO } from '../../../../../../socket-server' type RouteContext = { params: Promise<{ roomId: string }> @@ -56,6 +57,34 @@ export async function POST(req: NextRequest, context: RouteContext) { // Update room activity to refresh TTL await touchRoom(roomId) + // Broadcast to all users in the room via socket + const io = getSocketIO() + if (io) { + try { + const onlineMembers = await getOnlineRoomMembers(roomId) + const memberPlayers = await getRoomActivePlayers(roomId) + + // Convert memberPlayers Map to object for JSON serialization + const memberPlayersObj: Record = {} + for (const [uid, players] of memberPlayers.entries()) { + memberPlayersObj[uid] = players + } + + // Broadcast to all users in this room + io.to(`room:${roomId}`).emit('member-joined', { + roomId, + userId: viewerId, + onlineMembers, + memberPlayers: memberPlayersObj, + }) + + console.log(`[Join API] Broadcasted member-joined for user ${viewerId} in room ${roomId}`) + } catch (socketError) { + // Log but don't fail the request if socket broadcast fails + console.error('[Join API] Failed to broadcast member-joined:', socketError) + } + } + // Build response with auto-leave info if applicable return NextResponse.json( { diff --git a/apps/web/src/app/api/arcade/rooms/[roomId]/leave/route.ts b/apps/web/src/app/api/arcade/rooms/[roomId]/leave/route.ts index 8a186718..d9ac0afe 100644 --- a/apps/web/src/app/api/arcade/rooms/[roomId]/leave/route.ts +++ b/apps/web/src/app/api/arcade/rooms/[roomId]/leave/route.ts @@ -1,7 +1,9 @@ import { type NextRequest, NextResponse } from 'next/server' import { getRoomById } from '@/lib/arcade/room-manager' -import { isMember, removeMember } from '@/lib/arcade/room-membership' +import { getOnlineRoomMembers, isMember, removeMember } from '@/lib/arcade/room-membership' +import { getRoomActivePlayers } from '@/lib/arcade/player-manager' import { getViewerId } from '@/lib/viewer' +import { getSocketIO } from '../../../../../../socket-server' type RouteContext = { params: Promise<{ roomId: string }> @@ -31,6 +33,34 @@ export async function POST(_req: NextRequest, context: RouteContext) { // Remove member await removeMember(roomId, viewerId) + // Broadcast to all remaining users in the room via socket + const io = getSocketIO() + if (io) { + try { + const onlineMembers = await getOnlineRoomMembers(roomId) + const memberPlayers = await getRoomActivePlayers(roomId) + + // Convert memberPlayers Map to object for JSON serialization + const memberPlayersObj: Record = {} + for (const [uid, players] of memberPlayers.entries()) { + memberPlayersObj[uid] = players + } + + // Broadcast to all users in this room + io.to(`room:${roomId}`).emit('member-left', { + roomId, + userId: viewerId, + onlineMembers, + memberPlayers: memberPlayersObj, + }) + + console.log(`[Leave API] Broadcasted member-left for user ${viewerId} in room ${roomId}`) + } catch (socketError) { + // Log but don't fail the request if socket broadcast fails + console.error('[Leave API] Failed to broadcast member-left:', socketError) + } + } + return NextResponse.json({ success: true }) } catch (error) { console.error('Failed to leave room:', error)