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:
98
apps/web/src/components/GameCard.tsx
Normal file
98
apps/web/src/components/GameCard.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
85
apps/web/src/components/GameSelector.tsx
Normal file
85
apps/web/src/components/GameSelector.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user