feat: display room info and network players in mini app nav

When users are in a room (/arcade/rooms/[roomId]/*), the mini app nav now shows:
1. Room name and game type in RoomInfo component
2. Other members' player avatars with "network" indicators
3. Clear distinction between local players and network players

Implementation:
- Created useRoomData hook to fetch room data and listen to real-time updates
- Updated PageWithNav to use room data and compute network players
- Enhanced RoomInfo component to display room name when available
- Network players shown with special borders and connection indicators

The nav automatically detects room context from the URL and fetches:
- Room details (name, game, member count)
- All room members and their players
- Real-time updates via socket events (member-joined, member-left, players-updated)

Network players are filtered to exclude the current user and show each other
member's players with their display names for clear identification.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Thomas Hallock
2025-10-08 09:42:20 -05:00
parent 1e43e6945b
commit 5e3261f3be
4 changed files with 196 additions and 11 deletions

View File

@@ -3,6 +3,8 @@
import React from 'react'
import { useGameMode } from '../contexts/GameModeContext'
import { useArcadeGuard } from '../hooks/useArcadeGuard'
import { useRoomData } from '../hooks/useRoomData'
import { useViewerId } from '../hooks/useViewerId'
import { AppNavBar } from './AppNavBar'
import { GameContextNav } from './nav/GameContextNav'
import { PlayerConfigDialog } from './nav/PlayerConfigDialog'
@@ -30,6 +32,8 @@ export function PageWithNav({
}: PageWithNavProps) {
const { players, activePlayers, setActive, activePlayerCount } = useGameMode()
const { hasActiveSession, activeSession } = useArcadeGuard({ enabled: false }) // Don't redirect, just get info
const { roomData, isInRoom } = useRoomData()
const { data: viewerId } = useViewerId()
const [mounted, setMounted] = React.useState(false)
const [configurePlayerId, setConfigurePlayerId] = React.useState<string | null>(null)
@@ -80,17 +84,33 @@ export function PageWithNav({
// Compute arcade session info for display
const roomInfo =
hasActiveSession && activeSession
isInRoom && roomData
? {
gameName: activeSession.currentGame,
playerCount: activePlayerCount, // TODO: Get actual player count from session when available
roomName: roomData.name,
gameName: roomData.gameName,
playerCount: roomData.members.length,
}
: undefined
: hasActiveSession && activeSession
? {
gameName: activeSession.currentGame,
playerCount: activePlayerCount,
}
: undefined
// Compute network players (other players in the arcade session)
// For now, we don't have this info in activeSession, so return empty array
// TODO: When arcade room system is implemented, fetch other players from session
const networkPlayers: Array<{ id: string; emoji?: string; name?: string }> = []
// Compute network players (other players in the room, excluding current user)
const networkPlayers: Array<{ id: string; emoji?: string; name?: string }> =
isInRoom && roomData
? roomData.members
.filter((member) => member.userId !== viewerId)
.flatMap((member) => {
const memberPlayerList = roomData.memberPlayers[member.userId] || []
return memberPlayerList.map((player) => ({
id: player.id,
emoji: player.emoji,
name: `${player.name} (${member.displayName})`,
}))
})
: []
// Create nav content if title is provided
const navContent = navTitle ? (

View File

@@ -22,6 +22,7 @@ interface NetworkPlayer {
}
interface ArcadeRoomInfo {
roomName?: string
gameName: string
playerCount: number
}
@@ -134,6 +135,7 @@ export function GameContextNav({
{/* Room Info - show when in arcade session */}
{roomInfo && !showFullscreenSelection && (
<RoomInfo
roomName={roomInfo.roomName}
gameName={roomInfo.gameName}
playerCount={roomInfo.playerCount}
shouldEmphasize={shouldEmphasize}

View File

@@ -1,4 +1,5 @@
interface RoomInfoProps {
roomName?: string
gameName: string
playerCount: number
shouldEmphasize: boolean
@@ -7,7 +8,7 @@ interface RoomInfoProps {
/**
* Displays current arcade room/session information
*/
export function RoomInfo({ gameName, playerCount, shouldEmphasize }: RoomInfoProps) {
export function RoomInfo({ roomName, gameName, playerCount, shouldEmphasize }: RoomInfoProps) {
return (
<div
style={{
@@ -53,7 +54,7 @@ export function RoomInfo({ gameName, playerCount, shouldEmphasize }: RoomInfoPro
letterSpacing: '0.5px',
}}
>
Arcade Session
{roomName ? 'Room' : 'Arcade Session'}
</div>
<div
style={{
@@ -61,7 +62,7 @@ export function RoomInfo({ gameName, playerCount, shouldEmphasize }: RoomInfoPro
fontWeight: 'bold',
}}
>
{gameName}
{roomName || gameName}
</div>
</div>

View File

@@ -0,0 +1,162 @@
import { useEffect, useState } from 'react'
import { usePathname } from 'next/navigation'
import { io, type Socket } from 'socket.io-client'
export interface RoomMember {
id: string
userId: string
displayName: string
isOnline: boolean
isCreator: boolean
}
export interface RoomPlayer {
id: string
name: string
emoji: string
color: string
}
export interface RoomData {
id: string
name: string
code: string
gameName: string
members: RoomMember[]
memberPlayers: Record<string, RoomPlayer[]> // userId -> players
}
/**
* Hook to fetch and subscribe to room data when on a room page
* Returns null if not on a room page
*/
export function useRoomData() {
const pathname = usePathname()
const [socket, setSocket] = useState<Socket | null>(null)
const [roomData, setRoomData] = useState<RoomData | null>(null)
const [isLoading, setIsLoading] = useState(false)
// Extract roomId from pathname like /arcade/rooms/[roomId]/...
const roomId = pathname?.match(/\/arcade\/rooms\/([^/]+)/)?.[1]
// Initialize socket connection when on a room page
useEffect(() => {
if (!roomId) {
if (socket) {
socket.disconnect()
setSocket(null)
}
return
}
const sock = io({ path: '/api/socket' })
setSocket(sock)
return () => {
sock.disconnect()
}
}, [roomId])
useEffect(() => {
if (!roomId) {
setRoomData(null)
return
}
setIsLoading(true)
// Fetch initial room data
fetch(`/api/arcade/rooms/${roomId}`)
.then((res) => {
if (!res.ok) throw new Error('Failed to fetch room')
return res.json()
})
.then((data) => {
setRoomData({
id: data.room.id,
name: data.room.name,
code: data.room.code,
gameName: data.room.gameName,
members: data.members || [],
memberPlayers: data.memberPlayers || {},
})
setIsLoading(false)
})
.catch((error) => {
console.error('Failed to fetch room data:', error)
setIsLoading(false)
})
}, [roomId])
// Subscribe to real-time updates via socket
useEffect(() => {
if (!socket || !roomId) return
const handleMemberJoined = (data: {
roomId: string
userId: string
members: RoomMember[]
memberPlayers: Record<string, RoomPlayer[]>
}) => {
if (data.roomId === roomId) {
setRoomData((prev) => {
if (!prev) return null
return {
...prev,
members: data.members,
memberPlayers: data.memberPlayers,
}
})
}
}
const handleMemberLeft = (data: {
roomId: string
userId: string
members: RoomMember[]
memberPlayers: Record<string, RoomPlayer[]>
}) => {
if (data.roomId === roomId) {
setRoomData((prev) => {
if (!prev) return null
return {
...prev,
members: data.members,
memberPlayers: data.memberPlayers,
}
})
}
}
const handleRoomPlayersUpdated = (data: {
roomId: string
memberPlayers: Record<string, RoomPlayer[]>
}) => {
if (data.roomId === roomId) {
setRoomData((prev) => {
if (!prev) return null
return {
...prev,
memberPlayers: data.memberPlayers,
}
})
}
}
socket.on('member-joined', handleMemberJoined)
socket.on('member-left', handleMemberLeft)
socket.on('room-players-updated', handleRoomPlayersUpdated)
return () => {
socket.off('member-joined', handleMemberJoined)
socket.off('member-left', handleMemberLeft)
socket.off('room-players-updated', handleRoomPlayersUpdated)
}
}, [socket, roomId])
return {
roomData,
isLoading,
isInRoom: !!roomId,
}
}