feat: add arcade room/session info and network players to nav
Add visual indicators for arcade sessions and other players: Components added: - NetworkPlayerIndicator: Shows network players with special "network" frame border (animated pulse, network icon badge) - RoomInfo: Displays current arcade session info (game name, player count) Modified: - GameContextNav: Accept and render networkPlayers and roomInfo - PageWithNav: Fetch arcade session info via useArcadeGuard and pass to GameContextNav Visual features: - Network players have gradient border and pulsing connection indicator - Room info shows game name and player count in styled container - Network players are visually distinct from local players - Only shown when in active arcade session This provides visibility into multiplayer state and prepares for full room system implementation. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
|
||||
import React from 'react'
|
||||
import { useGameMode } from '../contexts/GameModeContext'
|
||||
import { useArcadeGuard } from '../hooks/useArcadeGuard'
|
||||
import { AppNavBar } from './AppNavBar'
|
||||
import { GameContextNav } from './nav/GameContextNav'
|
||||
import { PlayerConfigDialog } from './nav/PlayerConfigDialog'
|
||||
@@ -28,6 +29,7 @@ export function PageWithNav({
|
||||
children,
|
||||
}: PageWithNavProps) {
|
||||
const { players, activePlayers, setActive, activePlayerCount } = useGameMode()
|
||||
const { hasActiveSession, activeSession } = useArcadeGuard({ enabled: false }) // Don't redirect, just get info
|
||||
const [mounted, setMounted] = React.useState(false)
|
||||
const [configurePlayerId, setConfigurePlayerId] = React.useState<string | null>(null)
|
||||
|
||||
@@ -76,6 +78,19 @@ export function PageWithNav({
|
||||
const shouldEmphasize = emphasizeGameContext && mounted
|
||||
const showFullscreenSelection = shouldEmphasize && activePlayerCount === 0
|
||||
|
||||
// Compute arcade session info for display
|
||||
const roomInfo = hasActiveSession && activeSession
|
||||
? {
|
||||
gameName: activeSession.currentGame,
|
||||
playerCount: activePlayerCount, // TODO: Get actual player count from session when available
|
||||
}
|
||||
: 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 }> = []
|
||||
|
||||
// Create nav content if title is provided
|
||||
const navContent = navTitle ? (
|
||||
<GameContextNav
|
||||
@@ -93,6 +108,8 @@ export function PageWithNav({
|
||||
onSetup={onSetup}
|
||||
onNewGame={onNewGame}
|
||||
canModifyPlayers={canModifyPlayers}
|
||||
roomInfo={roomInfo}
|
||||
networkPlayers={networkPlayers}
|
||||
/>
|
||||
) : null
|
||||
|
||||
|
||||
@@ -4,6 +4,8 @@ import { AddPlayerButton } from './AddPlayerButton'
|
||||
import { FullscreenPlayerSelection } from './FullscreenPlayerSelection'
|
||||
import { GameControlButtons } from './GameControlButtons'
|
||||
import { GameModeIndicator } from './GameModeIndicator'
|
||||
import { NetworkPlayerIndicator } from './NetworkPlayerIndicator'
|
||||
import { RoomInfo } from './RoomInfo'
|
||||
|
||||
type GameMode = 'none' | 'single' | 'battle' | 'tournament'
|
||||
|
||||
@@ -13,6 +15,17 @@ interface Player {
|
||||
emoji: string
|
||||
}
|
||||
|
||||
interface NetworkPlayer {
|
||||
id: string
|
||||
emoji?: string
|
||||
name?: string
|
||||
}
|
||||
|
||||
interface ArcadeRoomInfo {
|
||||
gameName: string
|
||||
playerCount: number
|
||||
}
|
||||
|
||||
interface GameContextNavProps {
|
||||
navTitle: string
|
||||
navEmoji?: string
|
||||
@@ -28,6 +41,9 @@ interface GameContextNavProps {
|
||||
onSetup?: () => void
|
||||
onNewGame?: () => void
|
||||
canModifyPlayers?: boolean
|
||||
// Arcade session info
|
||||
networkPlayers?: NetworkPlayer[]
|
||||
roomInfo?: ArcadeRoomInfo
|
||||
}
|
||||
|
||||
export function GameContextNav({
|
||||
@@ -45,6 +61,8 @@ export function GameContextNav({
|
||||
onSetup,
|
||||
onNewGame,
|
||||
canModifyPlayers = true,
|
||||
networkPlayers = [],
|
||||
roomInfo,
|
||||
}: GameContextNavProps) {
|
||||
const [_isTransitioning, setIsTransitioning] = React.useState(false)
|
||||
const [layoutMode, setLayoutMode] = React.useState<'column' | 'row'>(
|
||||
@@ -113,6 +131,34 @@ export function GameContextNav({
|
||||
showFullscreenSelection={showFullscreenSelection}
|
||||
/>
|
||||
|
||||
{/* Room Info - show when in arcade session */}
|
||||
{roomInfo && !showFullscreenSelection && (
|
||||
<RoomInfo
|
||||
gameName={roomInfo.gameName}
|
||||
playerCount={roomInfo.playerCount}
|
||||
shouldEmphasize={shouldEmphasize}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Network Players - show other players in the room */}
|
||||
{networkPlayers.length > 0 && !showFullscreenSelection && (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: shouldEmphasize ? '12px' : '6px',
|
||||
}}
|
||||
>
|
||||
{networkPlayers.map((player) => (
|
||||
<NetworkPlayerIndicator
|
||||
key={player.id}
|
||||
player={player}
|
||||
shouldEmphasize={shouldEmphasize}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Game Control Buttons - only show during active game */}
|
||||
{!showFullscreenSelection && !canModifyPlayers && (
|
||||
<GameControlButtons onSetup={onSetup} onNewGame={onNewGame} onQuit={onExitSession} />
|
||||
|
||||
121
apps/web/src/components/nav/NetworkPlayerIndicator.tsx
Normal file
121
apps/web/src/components/nav/NetworkPlayerIndicator.tsx
Normal file
@@ -0,0 +1,121 @@
|
||||
import React from 'react'
|
||||
|
||||
interface NetworkPlayer {
|
||||
id: string
|
||||
emoji?: string
|
||||
name?: string
|
||||
}
|
||||
|
||||
interface NetworkPlayerIndicatorProps {
|
||||
player: NetworkPlayer
|
||||
shouldEmphasize: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays a network player with a special "network" frame border
|
||||
* to distinguish them from local players
|
||||
*/
|
||||
export function NetworkPlayerIndicator({ player, shouldEmphasize }: NetworkPlayerIndicatorProps) {
|
||||
const [isHovered, setIsHovered] = React.useState(false)
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: 'relative',
|
||||
fontSize: shouldEmphasize ? '48px' : '20px',
|
||||
lineHeight: 1,
|
||||
transition: 'all 0.4s cubic-bezier(0.4, 0, 0.2, 1)',
|
||||
cursor: 'default',
|
||||
}}
|
||||
title={player.name || `Network Player ${player.id.slice(0, 8)}`}
|
||||
onMouseEnter={() => setIsHovered(true)}
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
>
|
||||
{/* Network frame border */}
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
inset: '-6px',
|
||||
borderRadius: '8px',
|
||||
background: `
|
||||
linear-gradient(135deg,
|
||||
rgba(59, 130, 246, 0.4),
|
||||
rgba(147, 51, 234, 0.4),
|
||||
rgba(236, 72, 153, 0.4))
|
||||
`,
|
||||
opacity: isHovered ? 1 : 0.7,
|
||||
transition: 'opacity 0.2s ease',
|
||||
zIndex: -1,
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Animated network signal indicator */}
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: '-8px',
|
||||
right: '-8px',
|
||||
width: '12px',
|
||||
height: '12px',
|
||||
borderRadius: '50%',
|
||||
background: 'rgba(34, 197, 94, 0.9)',
|
||||
boxShadow: '0 0 8px rgba(34, 197, 94, 0.6)',
|
||||
animation: 'networkPulse 2s ease-in-out infinite',
|
||||
zIndex: 1,
|
||||
}}
|
||||
title="Connected"
|
||||
/>
|
||||
|
||||
{/* Player emoji or fallback */}
|
||||
<div
|
||||
style={{
|
||||
position: 'relative',
|
||||
filter: shouldEmphasize ? 'drop-shadow(0 4px 8px rgba(0,0,0,0.25))' : 'none',
|
||||
}}
|
||||
>
|
||||
{player.emoji || '🌐'}
|
||||
</div>
|
||||
|
||||
{/* Network icon badge */}
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
bottom: '-4px',
|
||||
left: '-4px',
|
||||
width: '16px',
|
||||
height: '16px',
|
||||
borderRadius: '50%',
|
||||
border: '2px solid white',
|
||||
background: 'linear-gradient(135deg, #3b82f6, #8b5cf6)',
|
||||
color: 'white',
|
||||
fontSize: '8px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
boxShadow: '0 2px 6px rgba(0,0,0,0.3)',
|
||||
zIndex: 1,
|
||||
}}
|
||||
title="Network Player"
|
||||
>
|
||||
📡
|
||||
</div>
|
||||
|
||||
<style
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
@keyframes networkPulse {
|
||||
0%, 100% {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
50% {
|
||||
opacity: 0.5;
|
||||
transform: scale(1.2);
|
||||
}
|
||||
}
|
||||
`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
89
apps/web/src/components/nav/RoomInfo.tsx
Normal file
89
apps/web/src/components/nav/RoomInfo.tsx
Normal file
@@ -0,0 +1,89 @@
|
||||
import React from 'react'
|
||||
|
||||
interface RoomInfoProps {
|
||||
gameName: string
|
||||
playerCount: number
|
||||
shouldEmphasize: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays current arcade room/session information
|
||||
*/
|
||||
export function RoomInfo({ gameName, playerCount, shouldEmphasize }: RoomInfoProps) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px',
|
||||
padding: shouldEmphasize ? '8px 16px' : '4px 12px',
|
||||
background: 'linear-gradient(135deg, rgba(59, 130, 246, 0.2), rgba(147, 51, 234, 0.2))',
|
||||
borderRadius: '12px',
|
||||
border: '2px solid rgba(59, 130, 246, 0.4)',
|
||||
fontSize: shouldEmphasize ? '16px' : '14px',
|
||||
fontWeight: '600',
|
||||
color: 'rgba(255, 255, 255, 0.95)',
|
||||
transition: 'all 0.4s cubic-bezier(0.4, 0, 0.2, 1)',
|
||||
boxShadow: '0 4px 12px rgba(59, 130, 246, 0.2)',
|
||||
}}
|
||||
title="Active Arcade Session"
|
||||
>
|
||||
{/* Room icon */}
|
||||
<div
|
||||
style={{
|
||||
fontSize: shouldEmphasize ? '20px' : '16px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
🎮
|
||||
</div>
|
||||
|
||||
{/* Room details */}
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '2px',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
fontSize: shouldEmphasize ? '14px' : '12px',
|
||||
opacity: 0.8,
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.5px',
|
||||
}}
|
||||
>
|
||||
Arcade Session
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
fontSize: shouldEmphasize ? '16px' : '14px',
|
||||
fontWeight: 'bold',
|
||||
}}
|
||||
>
|
||||
{gameName}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Player count badge */}
|
||||
<div
|
||||
style={{
|
||||
marginLeft: '8px',
|
||||
padding: '4px 8px',
|
||||
background: 'rgba(255, 255, 255, 0.2)',
|
||||
borderRadius: '8px',
|
||||
fontSize: shouldEmphasize ? '14px' : '12px',
|
||||
fontWeight: 'bold',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '4px',
|
||||
}}
|
||||
>
|
||||
<span>👥</span>
|
||||
<span>{playerCount}</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user