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