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
onEmojiSelect: (emoji: string) => void
onClose: () => void
playerNumber: 1 | 2
playerNumber: 1 | 2 | 3 | 4
}
// Create a map of emoji to their searchable data
@@ -195,7 +195,11 @@ export function EmojiPicker({ currentEmoji, onEmojiSelect, onClose, playerNumber
padding: '8px 12px',
background: playerNumber === 1
? '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',
color: 'white',
display: 'flex',
@@ -326,18 +330,35 @@ export function EmojiPicker({ currentEmoji, onEmojiSelect, onClose, playerNumber
})}>
{displayEmojis.map(emoji => {
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 (
<button
key={emoji}
className={css({
aspectRatio: '1',
background: isSelected
? (playerNumber === 1 ? 'blue.100' : 'pink.100')
: 'transparent',
background: getSelectedBg(),
border: '2px solid',
borderColor: isSelected
? (playerNumber === 1 ? 'blue.400' : 'pink.400')
: 'transparent',
borderColor: getSelectedBorder(),
borderRadius: '6px',
fontSize: '20px',
cursor: 'pointer',
@@ -346,9 +367,7 @@ export function EmojiPicker({ currentEmoji, onEmojiSelect, onClose, playerNumber
alignItems: 'center',
justifyContent: 'center',
_hover: {
background: isSelected
? (playerNumber === 1 ? 'blue.200' : 'pink.200')
: 'gray.100',
background: getHoverBg(),
transform: 'scale(1.15)',
zIndex: 1,
fontSize: '24px'

View File

@@ -18,7 +18,7 @@ export function PageWithNav({ navTitle, navEmoji, emphasizeGameContext = false,
const { players, activePlayerCount, updatePlayer } = useGameMode()
const { profile } = useUserProfile()
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
React.useEffect(() => {
@@ -35,27 +35,47 @@ export function PageWithNav({ navTitle, navEmoji, emphasizeGameContext = false,
}
const handleConfigurePlayer = (playerId: number) => {
// Only support configuring players 1 and 2
if (playerId === 1 || playerId === 2) {
setConfigurePlayerId(playerId)
// Support configuring all players (1-4)
if (playerId >= 1 && playerId <= 4) {
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
.filter(p => p.isActive)
.map(player => ({
...player,
emoji: player.id === 1 ? profile.player1Emoji : player.id === 2 ? profile.player2Emoji : player.emoji,
name: player.id === 1 ? profile.player1Name : player.id === 2 ? profile.player2Name : player.name
emoji: getPlayerEmoji(player.id),
name: getPlayerName(player.id)
}))
const inactivePlayers = players
.filter(p => !p.isActive)
.map(player => ({
...player,
emoji: player.id === 1 ? profile.player1Emoji : player.id === 2 ? profile.player2Emoji : player.emoji,
name: player.id === 1 ? profile.player1Name : player.id === 2 ? profile.player2Name : player.name
emoji: getPlayerEmoji(player.id),
name: getPlayerName(player.id)
}))
// Compute game mode from active player count

View File

@@ -27,19 +27,18 @@ export function ActivePlayersList({ activePlayers, shouldEmphasize, onRemovePlay
lineHeight: 1,
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',
cursor: shouldEmphasize && (player.id === 1 || player.id === 2) ? 'pointer' : 'default'
cursor: shouldEmphasize ? 'pointer' : 'default'
}}
title={player.name}
onClick={() => shouldEmphasize && (player.id === 1 || player.id === 2) && onConfigurePlayer(player.id)}
onClick={() => shouldEmphasize && onConfigurePlayer(player.id)}
onMouseEnter={() => shouldEmphasize && setHoveredPlayerId(player.id)}
onMouseLeave={() => shouldEmphasize && setHoveredPlayerId(null)}
>
{player.emoji}
{shouldEmphasize && hoveredPlayerId === player.id && (
<>
{/* Configure button - bottom left (only for players 1 & 2) */}
{(player.id === 1 || player.id === 2) && (
<button
{/* Configure button - bottom left */}
<button
onClick={(e) => {
e.stopPropagation()
onConfigurePlayer(player.id)
@@ -76,7 +75,6 @@ export function ActivePlayersList({ activePlayers, shouldEmphasize, onRemovePlay
>
</button>
)}
{/* Remove button - top right */}
<button

View File

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

View File

@@ -3,18 +3,34 @@ import { useUserProfile } from '../../contexts/UserProfileContext'
import { EmojiPicker } from '../../app/games/matching/components/EmojiPicker'
interface PlayerConfigDialogProps {
playerId: 1 | 2
playerId: 1 | 2 | 3 | 4
onClose: () => void
}
export function PlayerConfigDialog({ playerId, onClose }: PlayerConfigDialogProps) {
const { profile, updatePlayerEmoji, updatePlayerName } = useUserProfile()
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 = () => {
updatePlayerName(playerId, tempName)
@@ -73,7 +89,11 @@ export function PlayerConfigDialog({ playerId, onClose }: PlayerConfigDialogProp
fontWeight: 'bold',
background: playerId === 1
? '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',
color: 'transparent',
margin: 0
@@ -124,7 +144,8 @@ export function PlayerConfigDialog({ playerId, onClose }: PlayerConfigDialogProp
gap: '12px'
}}
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.boxShadow = '0 4px 12px rgba(0,0,0,0.1)'
}}
@@ -196,10 +217,16 @@ export function PlayerConfigDialog({ playerId, onClose }: PlayerConfigDialogProp
fontWeight: '500'
}}
onFocus={(e) => {
e.currentTarget.style.borderColor = playerId === 1 ? '#60a5fa' : '#f472b6'
e.currentTarget.style.boxShadow = playerId === 1
? '0 0 0 3px rgba(96, 165, 250, 0.1)'
: '0 0 0 3px rgba(244, 114, 182, 0.1)'
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}`
}}
onBlur={(e) => {
e.currentTarget.style.borderColor = '#e5e7eb'
@@ -253,7 +280,11 @@ export function PlayerConfigDialog({ playerId, onClose }: PlayerConfigDialogProp
padding: '12px',
background: playerId === 1
? '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',
borderRadius: '12px',
fontSize: '14px',

View File

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