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:
@@ -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 ? (
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
162
apps/web/src/hooks/useRoomData.ts
Normal file
162
apps/web/src/hooks/useRoomData.ts
Normal 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,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user