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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,54 +1,49 @@
import React, { useState } from 'react' import React, { useState } from 'react'
import { useUserProfile } from '../../contexts/UserProfileContext' import { useGameMode } from '../../contexts/GameModeContext'
import { EmojiPicker } from '../../app/games/matching/components/EmojiPicker' import { EmojiPicker } from '../../app/games/matching/components/EmojiPicker'
interface PlayerConfigDialogProps { interface PlayerConfigDialogProps {
playerId: 1 | 2 | 3 | 4 playerId: string
onClose: () => void onClose: () => void
} }
export function PlayerConfigDialog({ playerId, onClose }: PlayerConfigDialogProps) { export function PlayerConfigDialog({ playerId, onClose }: PlayerConfigDialogProps) {
const { profile, updatePlayerEmoji, updatePlayerName } = useUserProfile() const { getPlayer, updatePlayer } = useGameMode()
const [showEmojiPicker, setShowEmojiPicker] = useState(false) const [showEmojiPicker, setShowEmojiPicker] = useState(false)
const getCurrentName = () => { const player = getPlayer(playerId)
switch (playerId) {
case 1: return profile.player1Name if (!player) {
case 2: return profile.player2Name return null
case 3: return profile.player3Name
case 4: return profile.player4Name
}
} }
const getCurrentEmoji = () => { const [tempName, setTempName] = useState(player.name)
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 handleSave = () => { const handleSave = () => {
updatePlayerName(playerId, tempName) updatePlayer(playerId, { name: tempName })
onClose() onClose()
} }
const handleEmojiSelect = (emoji: string) => { const handleEmojiSelect = (emoji: string) => {
updatePlayerEmoji(playerId, emoji) updatePlayer(playerId, { emoji })
setShowEmojiPicker(false) 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) { if (showEmojiPicker) {
return ( return (
<EmojiPicker <EmojiPicker
currentEmoji={currentEmoji} currentEmoji={player.emoji}
onEmojiSelect={handleEmojiSelect} onEmojiSelect={handleEmojiSelect}
onClose={() => setShowEmojiPicker(false)} onClose={() => setShowEmojiPicker(false)}
playerNumber={playerId} playerNumber={displayNumber}
/> />
) )
} }
@ -87,18 +82,12 @@ export function PlayerConfigDialog({ playerId, onClose }: PlayerConfigDialogProp
<h2 style={{ <h2 style={{
fontSize: '24px', fontSize: '24px',
fontWeight: 'bold', fontWeight: 'bold',
background: playerId === 1 background: `linear-gradient(135deg, ${gradientColor}, ${gradientColor}dd)`,
? 'linear-gradient(135deg, #60a5fa, #3b82f6)'
: playerId === 2
? 'linear-gradient(135deg, #f472b6, #ec4899)'
: playerId === 3
? 'linear-gradient(135deg, #a78bfa, #8b5cf6)'
: 'linear-gradient(135deg, #fbbf24, #f59e0b)',
backgroundClip: 'text', backgroundClip: 'text',
color: 'transparent', color: 'transparent',
margin: 0 margin: 0
}}> }}>
Configure Player {playerId} Configure Player
</h2> </h2>
<button <button
onClick={onClose} onClick={onClose}
@ -144,8 +133,7 @@ export function PlayerConfigDialog({ playerId, onClose }: PlayerConfigDialogProp
gap: '12px' gap: '12px'
}} }}
onMouseEnter={(e) => { onMouseEnter={(e) => {
const borderColor = playerId === 1 ? '#60a5fa' : playerId === 2 ? '#f472b6' : playerId === 3 ? '#a78bfa' : '#fbbf24' e.currentTarget.style.borderColor = gradientColor
e.currentTarget.style.borderColor = borderColor
e.currentTarget.style.transform = 'translateY(-2px)' e.currentTarget.style.transform = 'translateY(-2px)'
e.currentTarget.style.boxShadow = '0 4px 12px rgba(0,0,0,0.1)' 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', fontSize: '48px',
lineHeight: 1 lineHeight: 1
}}> }}>
{currentEmoji} {player.emoji}
</div> </div>
<div style={{ <div style={{
flex: 1, flex: 1,
@ -204,7 +192,7 @@ export function PlayerConfigDialog({ playerId, onClose }: PlayerConfigDialogProp
type="text" type="text"
value={tempName} value={tempName}
onChange={(e) => setTempName(e.target.value)} onChange={(e) => setTempName(e.target.value)}
placeholder={`Player ${playerId}`} placeholder="Player Name"
maxLength={20} maxLength={20}
style={{ style={{
width: '100%', width: '100%',
@ -217,16 +205,8 @@ export function PlayerConfigDialog({ playerId, onClose }: PlayerConfigDialogProp
fontWeight: '500' fontWeight: '500'
}} }}
onFocus={(e) => { onFocus={(e) => {
const focusColor = playerId === 1 ? '#60a5fa' : playerId === 2 ? '#f472b6' : playerId === 3 ? '#a78bfa' : '#fbbf24' e.currentTarget.style.borderColor = gradientColor
const shadowColor = playerId === 1 e.currentTarget.style.boxShadow = `0 0 0 3px ${gradientColor}20`
? '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}`
}} }}
onBlur={(e) => { onBlur={(e) => {
e.currentTarget.style.borderColor = '#e5e7eb' e.currentTarget.style.borderColor = '#e5e7eb'
@ -278,13 +258,7 @@ export function PlayerConfigDialog({ playerId, onClose }: PlayerConfigDialogProp
style={{ style={{
flex: 1, flex: 1,
padding: '12px', padding: '12px',
background: playerId === 1 background: `linear-gradient(135deg, ${gradientColor}, ${gradientColor}dd)`,
? 'linear-gradient(135deg, #60a5fa, #3b82f6)'
: playerId === 2
? 'linear-gradient(135deg, #f472b6, #ec4899)'
: playerId === 3
? 'linear-gradient(135deg, #a78bfa, #8b5cf6)'
: 'linear-gradient(135deg, #fbbf24, #f59e0b)',
border: 'none', border: 'none',
borderRadius: '12px', borderRadius: '12px',
fontSize: '14px', fontSize: '14px',
@ -309,4 +283,4 @@ export function PlayerConfigDialog({ playerId, onClose }: PlayerConfigDialogProp
</div> </div>
</div> </div>
) )
} }