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:
@@ -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'
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user