feat: update nav components for UUID players

- Update all player ID types from number to string
- Remove switch statements for player lookups
- Use Map/Set operations instead of array methods
- Support arbitrary number of players
- PlayerConfigDialog now accepts string IDs
This commit is contained in:
Thomas Hallock 2025-10-04 17:06:54 -05:00
parent 2b94cad11b
commit e85d0415f2
6 changed files with 62 additions and 120 deletions

View File

@ -3,7 +3,6 @@
import React from 'react'
import { AppNavBar } from './AppNavBar'
import { useGameMode } from '../contexts/GameModeContext'
import { useUserProfile } from '../contexts/UserProfileContext'
import { GameContextNav } from './nav/GameContextNav'
import { PlayerConfigDialog } from './nav/PlayerConfigDialog'
@ -15,10 +14,9 @@ interface PageWithNavProps {
}
export function PageWithNav({ navTitle, navEmoji, emphasizeGameContext = false, children }: PageWithNavProps) {
const { players, activePlayerCount, updatePlayer } = useGameMode()
const { profile } = useUserProfile()
const { players, activePlayers, setActive, activePlayerCount } = useGameMode()
const [mounted, setMounted] = React.useState(false)
const [configurePlayerId, setConfigurePlayerId] = React.useState<1 | 2 | 3 | 4 | null>(null)
const [configurePlayerId, setConfigurePlayerId] = React.useState<string | null>(null)
// Delay mounting animation slightly for smooth transition
React.useEffect(() => {
@ -26,57 +24,27 @@ export function PageWithNav({ navTitle, navEmoji, emphasizeGameContext = false,
return () => clearTimeout(timer)
}, [])
const handleRemovePlayer = (playerId: number) => {
updatePlayer(playerId, { isActive: false })
const handleRemovePlayer = (playerId: string) => {
setActive(playerId, false)
}
const handleAddPlayer = (playerId: number) => {
updatePlayer(playerId, { isActive: true })
const handleAddPlayer = (playerId: string) => {
setActive(playerId, true)
}
const handleConfigurePlayer = (playerId: number) => {
// Support configuring all players (1-4)
if (playerId >= 1 && playerId <= 4) {
setConfigurePlayerId(playerId as 1 | 2 | 3 | 4)
}
const handleConfigurePlayer = (playerId: string) => {
setConfigurePlayerId(playerId)
}
// Transform players to use profile emojis and names for all players
const getPlayerEmoji = (playerId: number) => {
switch (playerId) {
case 1: return profile.player1Emoji
case 2: return profile.player2Emoji
case 3: return profile.player3Emoji
case 4: return profile.player4Emoji
default: return players.find(p => p.id === playerId)?.emoji || '😀'
}
}
// Get active and inactive players as arrays
const activePlayerList = Array.from(activePlayers)
.map(id => players.get(id))
.filter(p => p !== undefined)
.map(p => ({ id: p.id, name: p.name, emoji: p.emoji }))
const getPlayerName = (playerId: number) => {
switch (playerId) {
case 1: return profile.player1Name
case 2: return profile.player2Name
case 3: return profile.player3Name
case 4: return profile.player4Name
default: return players.find(p => p.id === playerId)?.name || `Player ${playerId}`
}
}
const activePlayers = players
.filter(p => p.isActive)
.map(player => ({
...player,
emoji: getPlayerEmoji(player.id),
name: getPlayerName(player.id)
}))
const inactivePlayers = players
.filter(p => !p.isActive)
.map(player => ({
...player,
emoji: getPlayerEmoji(player.id),
name: getPlayerName(player.id)
}))
const inactivePlayerList = Array.from(players.values())
.filter(p => !activePlayers.has(p.id))
.map(p => ({ id: p.id, name: p.name, emoji: p.emoji }))
// Compute game mode from active player count
const gameMode = activePlayerCount === 0 ? 'none' :
@ -93,8 +61,8 @@ export function PageWithNav({ navTitle, navEmoji, emphasizeGameContext = false,
navTitle={navTitle}
navEmoji={navEmoji}
gameMode={gameMode}
activePlayers={activePlayers}
inactivePlayers={inactivePlayers}
activePlayers={activePlayerList}
inactivePlayers={inactivePlayerList}
shouldEmphasize={shouldEmphasize}
showFullscreenSelection={showFullscreenSelection}
onAddPlayer={handleAddPlayer}

View File

@ -1,7 +1,7 @@
import React from 'react'
interface Player {
id: number
id: string
name: string
emoji: string
}
@ -9,12 +9,12 @@ interface Player {
interface ActivePlayersListProps {
activePlayers: Player[]
shouldEmphasize: boolean
onRemovePlayer: (playerId: number) => void
onConfigurePlayer: (playerId: number) => void
onRemovePlayer: (playerId: string) => void
onConfigurePlayer: (playerId: string) => void
}
export function ActivePlayersList({ activePlayers, shouldEmphasize, onRemovePlayer, onConfigurePlayer }: ActivePlayersListProps) {
const [hoveredPlayerId, setHoveredPlayerId] = React.useState<number | null>(null)
const [hoveredPlayerId, setHoveredPlayerId] = React.useState<string | null>(null)
return (
<>

View File

@ -1,7 +1,7 @@
import React from 'react'
interface Player {
id: number
id: string
name: string
emoji: string
}
@ -9,7 +9,7 @@ interface Player {
interface AddPlayerButtonProps {
inactivePlayers: Player[]
shouldEmphasize: boolean
onAddPlayer: (playerId: number) => void
onAddPlayer: (playerId: string) => void
}
export function AddPlayerButton({ inactivePlayers, shouldEmphasize, onAddPlayer }: AddPlayerButtonProps) {
@ -30,7 +30,7 @@ export function AddPlayerButton({ inactivePlayers, shouldEmphasize, onAddPlayer
}
}, [showPopover])
const handleAddPlayerClick = (playerId: number) => {
const handleAddPlayerClick = (playerId: string) => {
onAddPlayer(playerId)
setShowPopover(false)
}

View File

@ -1,15 +1,15 @@
import React from 'react'
interface Player {
id: number
id: string
name: string
emoji: string
}
interface FullscreenPlayerSelectionProps {
inactivePlayers: Player[]
onSelectPlayer: (playerId: number) => void
onConfigurePlayer: (playerId: number) => void
onSelectPlayer: (playerId: string) => void
onConfigurePlayer: (playerId: string) => void
isVisible: boolean
}

View File

@ -7,7 +7,7 @@ import { FullscreenPlayerSelection } from './FullscreenPlayerSelection'
type GameMode = 'none' | 'single' | 'battle' | 'tournament'
interface Player {
id: number
id: string
name: string
emoji: string
}
@ -20,9 +20,9 @@ interface GameContextNavProps {
inactivePlayers: Player[]
shouldEmphasize: boolean
showFullscreenSelection: boolean
onAddPlayer: (playerId: number) => void
onRemovePlayer: (playerId: number) => void
onConfigurePlayer: (playerId: number) => void
onAddPlayer: (playerId: string) => void
onRemovePlayer: (playerId: string) => void
onConfigurePlayer: (playerId: string) => void
}
export function GameContextNav({

View File

@ -1,54 +1,49 @@
import React, { useState } from 'react'
import { useUserProfile } from '../../contexts/UserProfileContext'
import { useGameMode } from '../../contexts/GameModeContext'
import { EmojiPicker } from '../../app/games/matching/components/EmojiPicker'
interface PlayerConfigDialogProps {
playerId: 1 | 2 | 3 | 4
playerId: string
onClose: () => void
}
export function PlayerConfigDialog({ playerId, onClose }: PlayerConfigDialogProps) {
const { profile, updatePlayerEmoji, updatePlayerName } = useUserProfile()
const { getPlayer, updatePlayer } = useGameMode()
const [showEmojiPicker, setShowEmojiPicker] = useState(false)
const getCurrentName = () => {
switch (playerId) {
case 1: return profile.player1Name
case 2: return profile.player2Name
case 3: return profile.player3Name
case 4: return profile.player4Name
}
const player = getPlayer(playerId)
if (!player) {
return null
}
const getCurrentEmoji = () => {
switch (playerId) {
case 1: return profile.player1Emoji
case 2: return profile.player2Emoji
case 3: return profile.player3Emoji
case 4: return profile.player4Emoji
}
}
const [tempName, setTempName] = useState(getCurrentName())
const currentEmoji = getCurrentEmoji()
const [tempName, setTempName] = useState(player.name)
const handleSave = () => {
updatePlayerName(playerId, tempName)
updatePlayer(playerId, { name: tempName })
onClose()
}
const handleEmojiSelect = (emoji: string) => {
updatePlayerEmoji(playerId, emoji)
updatePlayer(playerId, { emoji })
setShowEmojiPicker(false)
}
// Get player number for UI theming (first 4 players get special colors)
const allPlayers = Array.from(useGameMode().players.values()).sort((a, b) => a.createdAt - b.createdAt)
const playerIndex = allPlayers.findIndex(p => p.id === playerId)
const displayNumber = playerIndex + 1
// Color based on player's actual color
const gradientColor = player.color
if (showEmojiPicker) {
return (
<EmojiPicker
currentEmoji={currentEmoji}
currentEmoji={player.emoji}
onEmojiSelect={handleEmojiSelect}
onClose={() => setShowEmojiPicker(false)}
playerNumber={playerId}
playerNumber={displayNumber}
/>
)
}
@ -87,18 +82,12 @@ export function PlayerConfigDialog({ playerId, onClose }: PlayerConfigDialogProp
<h2 style={{
fontSize: '24px',
fontWeight: 'bold',
background: playerId === 1
? 'linear-gradient(135deg, #60a5fa, #3b82f6)'
: playerId === 2
? 'linear-gradient(135deg, #f472b6, #ec4899)'
: playerId === 3
? 'linear-gradient(135deg, #a78bfa, #8b5cf6)'
: 'linear-gradient(135deg, #fbbf24, #f59e0b)',
background: `linear-gradient(135deg, ${gradientColor}, ${gradientColor}dd)`,
backgroundClip: 'text',
color: 'transparent',
margin: 0
}}>
Configure Player {playerId}
Configure Player
</h2>
<button
onClick={onClose}
@ -144,8 +133,7 @@ export function PlayerConfigDialog({ playerId, onClose }: PlayerConfigDialogProp
gap: '12px'
}}
onMouseEnter={(e) => {
const borderColor = playerId === 1 ? '#60a5fa' : playerId === 2 ? '#f472b6' : playerId === 3 ? '#a78bfa' : '#fbbf24'
e.currentTarget.style.borderColor = borderColor
e.currentTarget.style.borderColor = gradientColor
e.currentTarget.style.transform = 'translateY(-2px)'
e.currentTarget.style.boxShadow = '0 4px 12px rgba(0,0,0,0.1)'
}}
@ -159,7 +147,7 @@ export function PlayerConfigDialog({ playerId, onClose }: PlayerConfigDialogProp
fontSize: '48px',
lineHeight: 1
}}>
{currentEmoji}
{player.emoji}
</div>
<div style={{
flex: 1,
@ -204,7 +192,7 @@ export function PlayerConfigDialog({ playerId, onClose }: PlayerConfigDialogProp
type="text"
value={tempName}
onChange={(e) => setTempName(e.target.value)}
placeholder={`Player ${playerId}`}
placeholder="Player Name"
maxLength={20}
style={{
width: '100%',
@ -217,16 +205,8 @@ export function PlayerConfigDialog({ playerId, onClose }: PlayerConfigDialogProp
fontWeight: '500'
}}
onFocus={(e) => {
const focusColor = playerId === 1 ? '#60a5fa' : playerId === 2 ? '#f472b6' : playerId === 3 ? '#a78bfa' : '#fbbf24'
const shadowColor = playerId === 1
? 'rgba(96, 165, 250, 0.1)'
: playerId === 2
? 'rgba(244, 114, 182, 0.1)'
: playerId === 3
? 'rgba(167, 139, 250, 0.1)'
: 'rgba(251, 191, 36, 0.1)'
e.currentTarget.style.borderColor = focusColor
e.currentTarget.style.boxShadow = `0 0 0 3px ${shadowColor}`
e.currentTarget.style.borderColor = gradientColor
e.currentTarget.style.boxShadow = `0 0 0 3px ${gradientColor}20`
}}
onBlur={(e) => {
e.currentTarget.style.borderColor = '#e5e7eb'
@ -278,13 +258,7 @@ export function PlayerConfigDialog({ playerId, onClose }: PlayerConfigDialogProp
style={{
flex: 1,
padding: '12px',
background: playerId === 1
? 'linear-gradient(135deg, #60a5fa, #3b82f6)'
: playerId === 2
? 'linear-gradient(135deg, #f472b6, #ec4899)'
: playerId === 3
? 'linear-gradient(135deg, #a78bfa, #8b5cf6)'
: 'linear-gradient(135deg, #fbbf24, #f59e0b)',
background: `linear-gradient(135deg, ${gradientColor}, ${gradientColor}dd)`,
border: 'none',
borderRadius: '12px',
fontSize: '14px',