feat: add reusable GameSelector and GameCard components

- Create GameSelector component with variant support (compact/detailed)
- Create GameCard component with game availability logic
- Support filtering games based on active player count
- Add empty state handling for when no games are available
- Components are composable and can be used anywhere in the app

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Thomas Hallock
2025-09-27 15:44:49 -05:00
parent dbf61c4b2d
commit c5a654aef1
2 changed files with 183 additions and 0 deletions

View File

@@ -0,0 +1,98 @@
'use client'
import { css } from '../../styled-system/css'
import { useGameMode } from '../contexts/GameModeContext'
import { GAMES_CONFIG, GameType } from './GameSelector'
interface GameCardProps {
gameType: GameType
config: typeof GAMES_CONFIG[GameType]
variant?: 'compact' | 'detailed'
className?: string
}
export function GameCard({
gameType,
config,
variant = 'detailed',
className
}: GameCardProps) {
const { activePlayerCount } = useGameMode()
// Check if a game is available based on active player count
const isGameAvailable = () => {
return activePlayerCount <= config.maxPlayers && activePlayerCount > 0
}
const handleGameClick = () => {
if (isGameAvailable()) {
window.location.href = config.url
}
}
const available = isGameAvailable()
return (
<div
onClick={handleGameClick}
className={css({
background: 'white',
rounded: variant === 'compact' ? 'xl' : '2xl',
p: variant === 'compact' ? '3' : '4',
border: '2px solid',
borderColor: available ? 'blue.200' : 'gray.200',
boxShadow: variant === 'compact'
? '0 2px 8px rgba(0, 0, 0, 0.1)'
: '0 4px 12px rgba(0, 0, 0, 0.1)',
opacity: available ? 1 : 0.5,
cursor: available ? 'pointer' : 'not-allowed',
transition: 'all 0.3s ease',
_hover: available ? {
transform: variant === 'compact' ? 'translateY(-1px)' : 'translateY(-2px)',
boxShadow: variant === 'compact'
? '0 4px 12px rgba(59, 130, 246, 0.15)'
: '0 8px 20px rgba(59, 130, 246, 0.15)',
borderColor: 'blue.300'
} : {}
}, className)}
>
<div className={css({
textAlign: 'center'
})}>
<div className={css({
fontSize: variant === 'compact' ? 'xl' : '2xl',
mb: variant === 'compact' ? '1' : '2'
})}>
{config.icon}
</div>
<h4 className={css({
fontSize: variant === 'compact' ? 'base' : 'lg',
fontWeight: 'bold',
color: 'gray.800',
mb: '1'
})}>
{config.name}
</h4>
{variant === 'detailed' && (
<p className={css({
fontSize: 'sm',
color: 'gray.600',
mb: '2'
})}>
{config.description}
</p>
)}
<div className={css({
fontSize: 'xs',
color: available ? 'green.600' : 'red.600',
fontWeight: 'semibold'
})}>
{activePlayerCount <= config.maxPlayers
? `${activePlayerCount}/${config.maxPlayers} players`
: `✗ Too many players (max ${config.maxPlayers})`
}
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,85 @@
'use client'
import { css } from '../../styled-system/css'
import { useGameMode } from '../contexts/GameModeContext'
import { GameCard } from './GameCard'
// Game configuration defining player limits
export const GAMES_CONFIG = {
'memory-lightning': {
name: 'Speed Memory Quiz',
maxPlayers: 1,
description: 'Solo memory challenge',
url: '/games/memory-quiz',
icon: '⚡'
},
'battle-arena': {
name: 'Matching Pairs Battle',
maxPlayers: 4,
description: 'Multiplayer memory battle',
url: '/games/matching',
icon: '⚔️'
}
} as const
export type GameType = keyof typeof GAMES_CONFIG
interface GameSelectorProps {
variant?: 'compact' | 'detailed'
showHeader?: boolean
emptyStateMessage?: string
className?: string
}
export function GameSelector({
variant = 'detailed',
showHeader = true,
emptyStateMessage = 'Select champions to see available games',
className
}: GameSelectorProps) {
const { activePlayerCount } = useGameMode()
return (
<div className={className}>
{showHeader && (
<h3 className={css({
fontSize: variant === 'compact' ? 'lg' : 'xl',
fontWeight: 'bold',
color: 'gray.800',
mb: '4',
textAlign: 'center'
})}>
🎮 Available Games
</h3>
)}
{activePlayerCount === 0 ? (
<div className={css({
textAlign: 'center',
py: variant === 'compact' ? '4' : '8',
color: 'gray.500'
})}>
<div className={css({ fontSize: variant === 'compact' ? '2xl' : '3xl', mb: '2' })}>🎯</div>
<p className={css({ fontSize: variant === 'compact' ? 'sm' : 'base' })}>
{emptyStateMessage}
</p>
</div>
) : (
<div className={css({
display: 'grid',
gridTemplateColumns: { base: '1fr', md: variant === 'compact' ? 'repeat(2, 1fr)' : 'repeat(2, 1fr)' },
gap: variant === 'compact' ? '3' : '4'
})}>
{Object.entries(GAMES_CONFIG).map(([gameType, config]) => (
<GameCard
key={gameType}
gameType={gameType as GameType}
config={config}
variant={variant}
/>
))}
</div>
)}
</div>
)
}