feat: improve arcade nav player grouping and add room join code display

Reorder navigation to group all player indicators together by moving game control buttons before network players, creating clearer visual separation. Add room join code display as a second line under room name for easier sharing.

🤖 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-11 07:16:32 -05:00
parent 4008cd75ff
commit 8e9980dc82
3 changed files with 197 additions and 195 deletions

View File

@@ -1,23 +1,23 @@
"use client";
'use client'
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";
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'
interface PageWithNavProps {
navTitle?: string;
navEmoji?: string;
emphasizeGameContext?: boolean;
onExitSession?: () => void;
onSetup?: () => void;
onNewGame?: () => void;
canModifyPlayers?: boolean;
children: React.ReactNode;
navTitle?: string
navEmoji?: string
emphasizeGameContext?: boolean
onExitSession?: () => void
onSetup?: () => void
onNewGame?: () => void
canModifyPlayers?: boolean
children: React.ReactNode
}
export function PageWithNav({
@@ -30,64 +30,59 @@ export function PageWithNav({
canModifyPlayers = true,
children,
}: PageWithNavProps) {
const { players, activePlayers, setActive, activePlayerCount } =
useGameMode();
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);
}) // 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)
// Delay mounting animation slightly for smooth transition
React.useEffect(() => {
const timer = setTimeout(() => setMounted(true), 50);
return () => clearTimeout(timer);
}, []);
const timer = setTimeout(() => setMounted(true), 50)
return () => clearTimeout(timer)
}, [])
const handleRemovePlayer = (playerId: string) => {
if (!canModifyPlayers) return;
setActive(playerId, false);
};
if (!canModifyPlayers) return
setActive(playerId, false)
}
const handleAddPlayer = (playerId: string) => {
if (!canModifyPlayers) return;
setActive(playerId, true);
};
if (!canModifyPlayers) return
setActive(playerId, true)
}
const handleConfigurePlayer = (playerId: string) => {
setConfigurePlayerId(playerId);
};
setConfigurePlayerId(playerId)
}
// Get active and inactive players as arrays
// Only show LOCAL players in the active/inactive lists (remote players shown separately in networkPlayers)
const activePlayerList = Array.from(activePlayers)
.map((id) => players.get(id))
.filter(
(p): p is NonNullable<typeof p> => p !== undefined && p.isLocal !== false,
); // Filter out remote players
.filter((p): p is NonNullable<typeof p> => p !== undefined && p.isLocal !== false) // Filter out remote players
const inactivePlayerList = Array.from(players.values()).filter(
(p) => !activePlayers.has(p.id) && p.isLocal !== false,
); // Filter out remote players
(p) => !activePlayers.has(p.id) && p.isLocal !== false
) // Filter out remote players
// Compute game mode from active player count
const gameMode =
activePlayerCount === 0
? "none"
? 'none'
: activePlayerCount === 1
? "single"
? 'single'
: activePlayerCount === 2
? "battle"
? 'battle'
: activePlayerCount >= 3
? "tournament"
: "none";
? 'tournament'
: 'none'
const shouldEmphasize = emphasizeGameContext && mounted;
const showFullscreenSelection = shouldEmphasize && activePlayerCount === 0;
const shouldEmphasize = emphasizeGameContext && mounted
const showFullscreenSelection = shouldEmphasize && activePlayerCount === 0
// Compute arcade session info for display
const roomInfo =
@@ -96,37 +91,37 @@ export function PageWithNav({
roomName: roomData.name,
gameName: roomData.gameName,
playerCount: roomData.members.length,
joinCode: roomData.code,
}
: hasActiveSession && activeSession
? {
gameName: activeSession.currentGame,
playerCount: activePlayerCount,
}
: undefined;
: undefined
// Compute network players (other players in the room, excluding current user)
const networkPlayers: Array<{
id: string;
emoji?: string;
name?: string;
color?: string;
memberName?: string;
id: string
emoji?: string
name?: string
color?: string
memberName?: string
}> =
isInRoom && roomData
? roomData.members
.filter((member) => member.userId !== viewerId)
.flatMap((member) => {
const memberPlayerList =
roomData.memberPlayers[member.userId] || [];
const memberPlayerList = roomData.memberPlayers[member.userId] || []
return memberPlayerList.map((player) => ({
id: player.id,
emoji: player.emoji,
name: player.name,
color: player.color,
memberName: member.displayName,
}));
}))
})
: [];
: []
// Create nav content if title is provided
const navContent = navTitle ? (
@@ -148,7 +143,7 @@ export function PageWithNav({
roomInfo={roomInfo}
networkPlayers={networkPlayers}
/>
) : null;
) : null
return (
<>
@@ -161,5 +156,5 @@ export function PageWithNav({
/>
)}
</>
);
)
}

View File

@@ -1,50 +1,51 @@
import React from "react";
import { ActivePlayersList } from "./ActivePlayersList";
import { AddPlayerButton } from "./AddPlayerButton";
import { FullscreenPlayerSelection } from "./FullscreenPlayerSelection";
import { GameControlButtons } from "./GameControlButtons";
import { GameModeIndicator } from "./GameModeIndicator";
import { NetworkPlayerIndicator } from "./NetworkPlayerIndicator";
import { RoomInfo } from "./RoomInfo";
import React from 'react'
import { ActivePlayersList } from './ActivePlayersList'
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";
type GameMode = 'none' | 'single' | 'battle' | 'tournament'
interface Player {
id: string;
name: string;
emoji: string;
id: string
name: string
emoji: string
}
interface NetworkPlayer {
id: string;
emoji?: string;
name?: string;
id: string
emoji?: string
name?: string
}
interface ArcadeRoomInfo {
roomName?: string;
gameName: string;
playerCount: number;
roomName?: string
gameName: string
playerCount: number
joinCode?: string
}
interface GameContextNavProps {
navTitle: string;
navEmoji?: string;
gameMode: GameMode;
activePlayers: Player[];
inactivePlayers: Player[];
shouldEmphasize: boolean;
showFullscreenSelection: boolean;
onAddPlayer: (playerId: string) => void;
onRemovePlayer: (playerId: string) => void;
onConfigurePlayer: (playerId: string) => void;
onExitSession?: () => void;
onSetup?: () => void;
onNewGame?: () => void;
canModifyPlayers?: boolean;
navTitle: string
navEmoji?: string
gameMode: GameMode
activePlayers: Player[]
inactivePlayers: Player[]
shouldEmphasize: boolean
showFullscreenSelection: boolean
onAddPlayer: (playerId: string) => void
onRemovePlayer: (playerId: string) => void
onConfigurePlayer: (playerId: string) => void
onExitSession?: () => void
onSetup?: () => void
onNewGame?: () => void
canModifyPlayers?: boolean
// Arcade session info
networkPlayers?: NetworkPlayer[];
roomInfo?: ArcadeRoomInfo;
networkPlayers?: NetworkPlayer[]
roomInfo?: ArcadeRoomInfo
}
export function GameContextNav({
@@ -65,61 +66,61 @@ export function GameContextNav({
networkPlayers = [],
roomInfo,
}: GameContextNavProps) {
const [_isTransitioning, setIsTransitioning] = React.useState(false);
const [layoutMode, setLayoutMode] = React.useState<"column" | "row">(
showFullscreenSelection ? "column" : "row",
);
const [_isTransitioning, setIsTransitioning] = React.useState(false)
const [layoutMode, setLayoutMode] = React.useState<'column' | 'row'>(
showFullscreenSelection ? 'column' : 'row'
)
const [containerWidth, setContainerWidth] = React.useState<string>(
showFullscreenSelection ? "100%" : "auto",
);
showFullscreenSelection ? '100%' : 'auto'
)
React.useEffect(() => {
if (showFullscreenSelection) {
// Switching to fullscreen - change layout and width immediately
setLayoutMode("column");
setContainerWidth("100%");
setLayoutMode('column')
setContainerWidth('100%')
} else {
// Switching away from fullscreen - delay layout change until transition completes
setIsTransitioning(true);
setContainerWidth("auto");
setIsTransitioning(true)
setContainerWidth('auto')
const timer = setTimeout(() => {
setLayoutMode("row");
setIsTransitioning(false);
}, 400); // Match transition duration
return () => clearTimeout(timer);
setLayoutMode('row')
setIsTransitioning(false)
}, 400) // Match transition duration
return () => clearTimeout(timer)
}
}, [showFullscreenSelection]);
}, [showFullscreenSelection])
return (
<div
style={{
display: "flex",
display: 'flex',
flexDirection: layoutMode,
alignItems: showFullscreenSelection ? "stretch" : "center",
gap: shouldEmphasize ? "16px" : "12px",
alignItems: showFullscreenSelection ? 'stretch' : 'center',
gap: shouldEmphasize ? '16px' : '12px',
width: containerWidth,
transition: "gap 0.4s cubic-bezier(0.4, 0, 0.2, 1)",
transition: 'gap 0.4s cubic-bezier(0.4, 0, 0.2, 1)',
}}
>
{/* Header row */}
<div
style={{
display: "flex",
alignItems: "center",
gap: shouldEmphasize ? "16px" : "12px",
justifyContent: showFullscreenSelection ? "center" : "flex-start",
width: showFullscreenSelection ? "100%" : "auto",
display: 'flex',
alignItems: 'center',
gap: shouldEmphasize ? '16px' : '12px',
justifyContent: showFullscreenSelection ? 'center' : 'flex-start',
width: showFullscreenSelection ? '100%' : 'auto',
}}
>
<h1
style={{
fontSize: showFullscreenSelection ? "32px" : "18px",
fontWeight: "bold",
background: "linear-gradient(135deg, #60a5fa, #a78bfa, #f472b6)",
backgroundClip: "text",
color: "transparent",
fontSize: showFullscreenSelection ? '32px' : '18px',
fontWeight: 'bold',
background: 'linear-gradient(135deg, #60a5fa, #a78bfa, #f472b6)',
backgroundClip: 'text',
color: 'transparent',
margin: 0,
transition: "font-size 0.4s cubic-bezier(0.4, 0, 0.2, 1)",
transition: 'font-size 0.4s cubic-bezier(0.4, 0, 0.2, 1)',
}}
>
{navEmoji && `${navEmoji} `}
@@ -138,17 +139,23 @@ export function GameContextNav({
roomName={roomInfo.roomName}
gameName={roomInfo.gameName}
playerCount={roomInfo.playerCount}
joinCode={roomInfo.joinCode}
shouldEmphasize={shouldEmphasize}
/>
)}
{/* Game Control Buttons - only show during active game */}
{!showFullscreenSelection && !canModifyPlayers && (
<GameControlButtons onSetup={onSetup} onNewGame={onNewGame} onQuit={onExitSession} />
)}
{/* Network Players - show other players in the room */}
{networkPlayers.length > 0 && !showFullscreenSelection && (
<div
style={{
display: "flex",
alignItems: "center",
gap: shouldEmphasize ? "12px" : "6px",
display: 'flex',
alignItems: 'center',
gap: shouldEmphasize ? '12px' : '6px',
}}
>
{networkPlayers.map((player) => (
@@ -161,40 +168,27 @@ export function GameContextNav({
</div>
)}
{/* Game Control Buttons - only show during active game */}
{!showFullscreenSelection && !canModifyPlayers && (
<GameControlButtons
onSetup={onSetup}
onNewGame={onNewGame}
onQuit={onExitSession}
/>
)}
{/* Active Players + Add Button */}
{(activePlayers.length > 0 ||
(shouldEmphasize &&
inactivePlayers.length > 0 &&
canModifyPlayers)) && (
(shouldEmphasize && inactivePlayers.length > 0 && canModifyPlayers)) && (
<div
style={{
display: "flex",
alignItems: "center",
gap: shouldEmphasize ? "12px" : "2px",
padding: shouldEmphasize ? "12px 20px" : "0",
display: 'flex',
alignItems: 'center',
gap: shouldEmphasize ? '12px' : '2px',
padding: shouldEmphasize ? '12px 20px' : '0',
background: shouldEmphasize
? "linear-gradient(135deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0.08))"
: "transparent",
borderRadius: shouldEmphasize ? "16px" : "0",
border: shouldEmphasize
? "3px solid rgba(255, 255, 255, 0.25)"
: "none",
? 'linear-gradient(135deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0.08))'
: 'transparent',
borderRadius: shouldEmphasize ? '16px' : '0',
border: shouldEmphasize ? '3px solid rgba(255, 255, 255, 0.25)' : 'none',
boxShadow: shouldEmphasize
? "0 6px 20px rgba(0, 0, 0, 0.15), inset 0 1px 0 rgba(255,255,255,0.3)"
: "none",
transition: "all 0.4s cubic-bezier(0.4, 0, 0.2, 1)",
transform: shouldEmphasize ? "scale(1.05)" : "scale(1)",
? '0 6px 20px rgba(0, 0, 0, 0.15), inset 0 1px 0 rgba(255,255,255,0.3)'
: 'none',
transition: 'all 0.4s cubic-bezier(0.4, 0, 0.2, 1)',
transform: shouldEmphasize ? 'scale(1.05)' : 'scale(1)',
opacity: canModifyPlayers ? 1 : 0.6,
pointerEvents: canModifyPlayers ? "auto" : "none",
pointerEvents: canModifyPlayers ? 'auto' : 'none',
}}
>
<ActivePlayersList
@@ -251,5 +245,5 @@ export function GameContextNav({
}}
/>
</div>
);
)
}

View File

@@ -1,8 +1,9 @@
interface RoomInfoProps {
roomName?: string;
gameName: string;
playerCount: number;
shouldEmphasize: boolean;
roomName?: string
gameName: string
playerCount: number
joinCode?: string
shouldEmphasize: boolean
}
/**
@@ -12,33 +13,33 @@ export function RoomInfo({
roomName,
gameName,
playerCount,
joinCode,
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)",
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",
fontSize: shouldEmphasize ? '20px' : '16px',
display: 'flex',
alignItems: 'center',
}}
>
🎮
@@ -47,48 +48,60 @@ export function RoomInfo({
{/* Room details */}
<div
style={{
display: "flex",
flexDirection: "column",
gap: "2px",
display: 'flex',
flexDirection: 'column',
gap: '2px',
}}
>
<div
style={{
fontSize: shouldEmphasize ? "14px" : "12px",
fontSize: shouldEmphasize ? '14px' : '12px',
opacity: 0.8,
textTransform: "uppercase",
letterSpacing: "0.5px",
textTransform: 'uppercase',
letterSpacing: '0.5px',
}}
>
{roomName ? "Room" : "Arcade Session"}
{roomName ? 'Room' : 'Arcade Session'}
</div>
<div
style={{
fontSize: shouldEmphasize ? "16px" : "14px",
fontWeight: "bold",
fontSize: shouldEmphasize ? '16px' : '14px',
fontWeight: 'bold',
}}
>
{roomName || gameName}
</div>
{joinCode && (
<div
style={{
fontSize: shouldEmphasize ? '12px' : '11px',
opacity: 0.7,
fontFamily: 'monospace',
letterSpacing: '0.5px',
}}
>
Code: {joinCode}
</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",
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>
);
)
}