feat: extend player customization to support all 4 players

Extend profile system and configuration UI to support all players:
- Add player3Emoji, player4Emoji, player3Name, player4Name to UserProfile
- Update PlayerConfigDialog to support players 1-4 with unique gradients
  - Player 1: Blue, Player 2: Pink, Player 3: Purple, Player 4: Yellow
- Update EmojiPicker color schemes for all 4 players
- Revert gear icon restrictions - show for all players
- Update PageWithNav to use profile data for all players

All players now fully customizable with persistent storage.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Thomas Hallock
2025-09-29 18:04:09 -05:00
parent d0a3bc7dc1
commit 72f8dee183
6 changed files with 118 additions and 44 deletions

View File

@@ -24,7 +24,7 @@ interface EmojiPickerProps {
currentEmoji: string currentEmoji: string
onEmojiSelect: (emoji: string) => void onEmojiSelect: (emoji: string) => void
onClose: () => void onClose: () => void
playerNumber: 1 | 2 playerNumber: 1 | 2 | 3 | 4
} }
// Create a map of emoji to their searchable data // Create a map of emoji to their searchable data
@@ -195,7 +195,11 @@ export function EmojiPicker({ currentEmoji, onEmojiSelect, onClose, playerNumber
padding: '8px 12px', padding: '8px 12px',
background: playerNumber === 1 background: playerNumber === 1
? 'linear-gradient(135deg, #74b9ff, #0984e3)' ? 'linear-gradient(135deg, #74b9ff, #0984e3)'
: 'linear-gradient(135deg, #fd79a8, #e84393)', : playerNumber === 2
? 'linear-gradient(135deg, #fd79a8, #e84393)'
: playerNumber === 3
? 'linear-gradient(135deg, #a78bfa, #8b5cf6)'
: 'linear-gradient(135deg, #fbbf24, #f59e0b)',
borderRadius: '12px', borderRadius: '12px',
color: 'white', color: 'white',
display: 'flex', display: 'flex',
@@ -326,18 +330,35 @@ export function EmojiPicker({ currentEmoji, onEmojiSelect, onClose, playerNumber
})}> })}>
{displayEmojis.map(emoji => { {displayEmojis.map(emoji => {
const isSelected = emoji === currentEmoji const isSelected = emoji === currentEmoji
const getSelectedBg = () => {
if (!isSelected) return 'transparent'
if (playerNumber === 1) return 'blue.100'
if (playerNumber === 2) return 'pink.100'
if (playerNumber === 3) return 'purple.100'
return 'yellow.100'
}
const getSelectedBorder = () => {
if (!isSelected) return 'transparent'
if (playerNumber === 1) return 'blue.400'
if (playerNumber === 2) return 'pink.400'
if (playerNumber === 3) return 'purple.400'
return 'yellow.400'
}
const getHoverBg = () => {
if (!isSelected) return 'gray.100'
if (playerNumber === 1) return 'blue.200'
if (playerNumber === 2) return 'pink.200'
if (playerNumber === 3) return 'purple.200'
return 'yellow.200'
}
return ( return (
<button <button
key={emoji} key={emoji}
className={css({ className={css({
aspectRatio: '1', aspectRatio: '1',
background: isSelected background: getSelectedBg(),
? (playerNumber === 1 ? 'blue.100' : 'pink.100')
: 'transparent',
border: '2px solid', border: '2px solid',
borderColor: isSelected borderColor: getSelectedBorder(),
? (playerNumber === 1 ? 'blue.400' : 'pink.400')
: 'transparent',
borderRadius: '6px', borderRadius: '6px',
fontSize: '20px', fontSize: '20px',
cursor: 'pointer', cursor: 'pointer',
@@ -346,9 +367,7 @@ export function EmojiPicker({ currentEmoji, onEmojiSelect, onClose, playerNumber
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
_hover: { _hover: {
background: isSelected background: getHoverBg(),
? (playerNumber === 1 ? 'blue.200' : 'pink.200')
: 'gray.100',
transform: 'scale(1.15)', transform: 'scale(1.15)',
zIndex: 1, zIndex: 1,
fontSize: '24px' fontSize: '24px'

View File

@@ -18,7 +18,7 @@ export function PageWithNav({ navTitle, navEmoji, emphasizeGameContext = false,
const { players, activePlayerCount, updatePlayer } = useGameMode() const { players, activePlayerCount, updatePlayer } = useGameMode()
const { profile } = useUserProfile() const { profile } = useUserProfile()
const [mounted, setMounted] = React.useState(false) const [mounted, setMounted] = React.useState(false)
const [configurePlayerId, setConfigurePlayerId] = React.useState<1 | 2 | null>(null) const [configurePlayerId, setConfigurePlayerId] = React.useState<1 | 2 | 3 | 4 | null>(null)
// Delay mounting animation slightly for smooth transition // Delay mounting animation slightly for smooth transition
React.useEffect(() => { React.useEffect(() => {
@@ -35,27 +35,47 @@ export function PageWithNav({ navTitle, navEmoji, emphasizeGameContext = false,
} }
const handleConfigurePlayer = (playerId: number) => { const handleConfigurePlayer = (playerId: number) => {
// Only support configuring players 1 and 2 // Support configuring all players (1-4)
if (playerId === 1 || playerId === 2) { if (playerId >= 1 && playerId <= 4) {
setConfigurePlayerId(playerId) setConfigurePlayerId(playerId as 1 | 2 | 3 | 4)
}
}
// 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 || '😀'
}
}
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}`
} }
} }
// Transform players to use profile emojis for players 1 and 2
const activePlayers = players const activePlayers = players
.filter(p => p.isActive) .filter(p => p.isActive)
.map(player => ({ .map(player => ({
...player, ...player,
emoji: player.id === 1 ? profile.player1Emoji : player.id === 2 ? profile.player2Emoji : player.emoji, emoji: getPlayerEmoji(player.id),
name: player.id === 1 ? profile.player1Name : player.id === 2 ? profile.player2Name : player.name name: getPlayerName(player.id)
})) }))
const inactivePlayers = players const inactivePlayers = players
.filter(p => !p.isActive) .filter(p => !p.isActive)
.map(player => ({ .map(player => ({
...player, ...player,
emoji: player.id === 1 ? profile.player1Emoji : player.id === 2 ? profile.player2Emoji : player.emoji, emoji: getPlayerEmoji(player.id),
name: player.id === 1 ? profile.player1Name : player.id === 2 ? profile.player2Name : player.name name: getPlayerName(player.id)
})) }))
// Compute game mode from active player count // Compute game mode from active player count

View File

@@ -27,19 +27,18 @@ export function ActivePlayersList({ activePlayers, shouldEmphasize, onRemovePlay
lineHeight: 1, lineHeight: 1,
transition: 'font-size 0.4s cubic-bezier(0.4, 0, 0.2, 1), filter 0.4s ease', transition: 'font-size 0.4s cubic-bezier(0.4, 0, 0.2, 1), filter 0.4s ease',
filter: shouldEmphasize ? 'drop-shadow(0 4px 8px rgba(0,0,0,0.25))' : 'none', filter: shouldEmphasize ? 'drop-shadow(0 4px 8px rgba(0,0,0,0.25))' : 'none',
cursor: shouldEmphasize && (player.id === 1 || player.id === 2) ? 'pointer' : 'default' cursor: shouldEmphasize ? 'pointer' : 'default'
}} }}
title={player.name} title={player.name}
onClick={() => shouldEmphasize && (player.id === 1 || player.id === 2) && onConfigurePlayer(player.id)} onClick={() => shouldEmphasize && onConfigurePlayer(player.id)}
onMouseEnter={() => shouldEmphasize && setHoveredPlayerId(player.id)} onMouseEnter={() => shouldEmphasize && setHoveredPlayerId(player.id)}
onMouseLeave={() => shouldEmphasize && setHoveredPlayerId(null)} onMouseLeave={() => shouldEmphasize && setHoveredPlayerId(null)}
> >
{player.emoji} {player.emoji}
{shouldEmphasize && hoveredPlayerId === player.id && ( {shouldEmphasize && hoveredPlayerId === player.id && (
<> <>
{/* Configure button - bottom left (only for players 1 & 2) */} {/* Configure button - bottom left */}
{(player.id === 1 || player.id === 2) && ( <button
<button
onClick={(e) => { onClick={(e) => {
e.stopPropagation() e.stopPropagation()
onConfigurePlayer(player.id) onConfigurePlayer(player.id)
@@ -76,7 +75,6 @@ export function ActivePlayersList({ activePlayers, shouldEmphasize, onRemovePlay
> >
</button> </button>
)}
{/* Remove button - top right */} {/* Remove button - top right */}
<button <button

View File

@@ -120,9 +120,8 @@ export function FullscreenPlayerSelection({ inactivePlayers, onSelectPlayer, onC
</div> </div>
</button> </button>
{/* Subtle gear icon for configuration (only for players 1 & 2) */} {/* Subtle gear icon for configuration */}
{(player.id === 1 || player.id === 2) && ( <button
<button
onClick={(e) => { onClick={(e) => {
e.stopPropagation() e.stopPropagation()
onConfigurePlayer(player.id) onConfigurePlayer(player.id)
@@ -165,7 +164,6 @@ export function FullscreenPlayerSelection({ inactivePlayers, onSelectPlayer, onC
> >
</button> </button>
)}
</div> </div>
))} ))}
</div> </div>

View File

@@ -3,18 +3,34 @@ import { useUserProfile } from '../../contexts/UserProfileContext'
import { EmojiPicker } from '../../app/games/matching/components/EmojiPicker' import { EmojiPicker } from '../../app/games/matching/components/EmojiPicker'
interface PlayerConfigDialogProps { interface PlayerConfigDialogProps {
playerId: 1 | 2 playerId: 1 | 2 | 3 | 4
onClose: () => void onClose: () => void
} }
export function PlayerConfigDialog({ playerId, onClose }: PlayerConfigDialogProps) { export function PlayerConfigDialog({ playerId, onClose }: PlayerConfigDialogProps) {
const { profile, updatePlayerEmoji, updatePlayerName } = useUserProfile() const { profile, updatePlayerEmoji, updatePlayerName } = useUserProfile()
const [showEmojiPicker, setShowEmojiPicker] = useState(false) const [showEmojiPicker, setShowEmojiPicker] = useState(false)
const [tempName, setTempName] = useState(
playerId === 1 ? profile.player1Name : profile.player2Name
)
const currentEmoji = playerId === 1 ? profile.player1Emoji : profile.player2Emoji 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 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 handleSave = () => { const handleSave = () => {
updatePlayerName(playerId, tempName) updatePlayerName(playerId, tempName)
@@ -73,7 +89,11 @@ export function PlayerConfigDialog({ playerId, onClose }: PlayerConfigDialogProp
fontWeight: 'bold', fontWeight: 'bold',
background: playerId === 1 background: playerId === 1
? 'linear-gradient(135deg, #60a5fa, #3b82f6)' ? 'linear-gradient(135deg, #60a5fa, #3b82f6)'
: 'linear-gradient(135deg, #f472b6, #ec4899)', : 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
@@ -124,7 +144,8 @@ export function PlayerConfigDialog({ playerId, onClose }: PlayerConfigDialogProp
gap: '12px' gap: '12px'
}} }}
onMouseEnter={(e) => { onMouseEnter={(e) => {
e.currentTarget.style.borderColor = playerId === 1 ? '#60a5fa' : '#f472b6' const borderColor = playerId === 1 ? '#60a5fa' : playerId === 2 ? '#f472b6' : playerId === 3 ? '#a78bfa' : '#fbbf24'
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)'
}} }}
@@ -196,10 +217,16 @@ export function PlayerConfigDialog({ playerId, onClose }: PlayerConfigDialogProp
fontWeight: '500' fontWeight: '500'
}} }}
onFocus={(e) => { onFocus={(e) => {
e.currentTarget.style.borderColor = playerId === 1 ? '#60a5fa' : '#f472b6' const focusColor = playerId === 1 ? '#60a5fa' : playerId === 2 ? '#f472b6' : playerId === 3 ? '#a78bfa' : '#fbbf24'
e.currentTarget.style.boxShadow = playerId === 1 const shadowColor = playerId === 1
? '0 0 0 3px rgba(96, 165, 250, 0.1)' ? 'rgba(96, 165, 250, 0.1)'
: '0 0 0 3px rgba(244, 114, 182, 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'
@@ -253,7 +280,11 @@ export function PlayerConfigDialog({ playerId, onClose }: PlayerConfigDialogProp
padding: '12px', padding: '12px',
background: playerId === 1 background: playerId === 1
? 'linear-gradient(135deg, #60a5fa, #3b82f6)' ? 'linear-gradient(135deg, #60a5fa, #3b82f6)'
: 'linear-gradient(135deg, #f472b6, #ec4899)', : 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',

View File

@@ -30,8 +30,12 @@ export const PLAYER_EMOJIS = [
export interface UserProfile { export interface UserProfile {
player1Emoji: string player1Emoji: string
player2Emoji: string player2Emoji: string
player3Emoji: string
player4Emoji: string
player1Name: string player1Name: string
player2Name: string player2Name: string
player3Name: string
player4Name: string
gamesPlayed: number gamesPlayed: number
totalWins: number totalWins: number
favoriteGameType: 'abacus-numeral' | 'complement-pairs' | null favoriteGameType: 'abacus-numeral' | 'complement-pairs' | null
@@ -41,8 +45,8 @@ export interface UserProfile {
export interface UserProfileContextType { export interface UserProfileContextType {
profile: UserProfile profile: UserProfile
updatePlayerEmoji: (player: 1 | 2, emoji: string) => void updatePlayerEmoji: (player: 1 | 2 | 3 | 4, emoji: string) => void
updatePlayerName: (player: 1 | 2, name: string) => void updatePlayerName: (player: 1 | 2 | 3 | 4, name: string) => void
updateGameStats: (stats: Partial<Pick<UserProfile, 'gamesPlayed' | 'totalWins' | 'favoriteGameType' | 'bestTime' | 'highestAccuracy'>>) => void updateGameStats: (stats: Partial<Pick<UserProfile, 'gamesPlayed' | 'totalWins' | 'favoriteGameType' | 'bestTime' | 'highestAccuracy'>>) => void
resetProfile: () => void resetProfile: () => void
} }
@@ -50,8 +54,12 @@ export interface UserProfileContextType {
const defaultProfile: UserProfile = { const defaultProfile: UserProfile = {
player1Emoji: '😀', player1Emoji: '😀',
player2Emoji: '😎', player2Emoji: '😎',
player3Emoji: '🤓',
player4Emoji: '🥳',
player1Name: 'Player 1', player1Name: 'Player 1',
player2Name: 'Player 2', player2Name: 'Player 2',
player3Name: 'Player 3',
player4Name: 'Player 4',
gamesPlayed: 0, gamesPlayed: 0,
totalWins: 0, totalWins: 0,
favoriteGameType: null, favoriteGameType: null,