soroban-abacus-flashcards/apps/web/src/arcade-games/know-your-world/components/SetupPhase.tsx

557 lines
18 KiB
TypeScript

'use client'
import { useCallback, useMemo } from 'react'
import * as Select from '@radix-ui/react-select'
import { css } from '@styled/css'
import { useTheme } from '@/contexts/ThemeContext'
import { useKnowYourWorld } from '../Provider'
import { DrillDownMapSelector } from './DrillDownMapSelector'
import { ALL_REGION_SIZES, ASSISTANCE_LEVELS, getFilteredMapDataBySizesSync } from '../maps'
import type { AssistanceLevelConfig } from '../maps'
import type { ContinentId } from '../continents'
// Generate feature badges for an assistance level
function getFeatureBadges(level: AssistanceLevelConfig): Array<{ label: string; icon: string }> {
const badges: Array<{ label: string; icon: string }> = []
if (level.hotColdEnabled) {
badges.push({ label: 'Hot/cold', icon: '🔥' })
}
if (level.hintsMode === 'onRequest') {
if (level.autoHintDefault) {
badges.push({ label: 'Auto-hints', icon: '💡' })
} else {
badges.push({ label: 'Hints', icon: '💡' })
}
} else if (level.hintsMode === 'limited' && level.hintLimit) {
badges.push({ label: `${level.hintLimit} hints`, icon: '💡' })
}
return badges
}
// Game mode options with rich descriptions
const GAME_MODE_OPTIONS = [
{
value: 'cooperative' as const,
emoji: '🤝',
label: 'Cooperative',
description: 'Work together to find all regions',
},
{
value: 'race' as const,
emoji: '🏁',
label: 'Race',
description: 'First to click the correct region wins',
},
{
value: 'turn-based' as const,
emoji: '↔️',
label: 'Turn-Based',
description: 'Take turns finding regions',
},
]
// Study time options with rich descriptions
const STUDY_TIME_OPTIONS = [
{
value: 0 as const,
emoji: '⏭️',
label: 'Skip',
description: 'Jump straight into the game',
},
{
value: 30 as const,
emoji: '⏱️',
label: '30 seconds',
description: 'Quick review before playing',
},
{
value: 60 as const,
emoji: '⏲️',
label: '1 minute',
description: 'Moderate study time',
},
{
value: 120 as const,
emoji: '⏰',
label: '2 minutes',
description: 'Extended study period',
},
]
export function SetupPhase() {
const { resolvedTheme } = useTheme()
const isDark = resolvedTheme === 'dark'
const {
state,
startGame,
setMap,
setMode,
setRegionSizes,
setAssistanceLevel,
setStudyDuration,
setContinent,
} = useKnowYourWorld()
// Calculate region counts per size category
const regionCountsBySize = useMemo(() => {
const counts: Record<string, number> = {}
for (const size of ALL_REGION_SIZES) {
try {
const filteredData = getFilteredMapDataBySizesSync(
state.selectedMap,
state.selectedContinent,
[size]
)
counts[size] = filteredData.regions.length
} catch {
counts[size] = 0
}
}
return counts
}, [state.selectedMap, state.selectedContinent])
// Handle selection change from drill-down selector
const handleSelectionChange = useCallback(
(mapId: 'world' | 'usa', continentId: ContinentId | 'all') => {
setMap(mapId)
setContinent(continentId)
},
[setMap, setContinent]
)
// Get selected options for display
const selectedMode = GAME_MODE_OPTIONS.find((opt) => opt.value === state.gameMode)
const selectedStudyTime = STUDY_TIME_OPTIONS.find((opt) => opt.value === state.studyDuration)
const selectedAssistance = ASSISTANCE_LEVELS.find((level) => level.id === state.assistanceLevel)
// Styles for Radix Select components
const triggerStyles = css({
display: 'flex',
alignItems: 'center',
gap: '3',
padding: '3',
bg: isDark ? 'gray.800' : 'white',
border: '2px solid',
borderColor: isDark ? 'gray.600' : 'gray.300',
rounded: 'xl',
cursor: 'pointer',
width: '100%', // Fill grid cell
transition: 'all 0.15s',
_hover: {
borderColor: isDark ? 'blue.500' : 'blue.400',
bg: isDark ? 'gray.750' : 'gray.50',
},
_focus: {
outline: 'none',
borderColor: 'blue.500',
boxShadow: '0 0 0 3px rgba(59, 130, 246, 0.2)',
},
})
const contentStyles = css({
bg: isDark ? 'gray.800' : 'white',
border: '2px solid',
borderColor: isDark ? 'gray.600' : 'gray.200',
rounded: 'xl',
shadow: 'xl',
overflow: 'hidden',
zIndex: 1000,
minWidth: '220px',
})
const itemStyles = css({
display: 'flex',
alignItems: 'center',
gap: '3',
padding: '3',
cursor: 'pointer',
outline: 'none',
transition: 'all 0.1s',
_hover: {
bg: isDark ? 'gray.700' : 'blue.50',
},
'&[data-state="checked"]': {
bg: isDark ? 'blue.900/50' : 'blue.100',
},
})
const labelStyles = css({
fontSize: 'xs',
fontWeight: '600',
color: isDark ? 'gray.400' : 'gray.500',
marginBottom: '2',
textTransform: 'uppercase',
letterSpacing: 'wide',
})
return (
<div
data-component="setup-phase"
className={css({
display: 'flex',
flexDirection: 'column',
gap: '4',
maxWidth: '800px',
margin: '0 auto',
paddingTop: '16',
paddingX: '4',
paddingBottom: '6',
})}
>
{/* Header */}
<div
data-element="header"
className={css({
textAlign: 'center',
marginBottom: '2',
})}
>
<h1
className={css({
fontSize: '2xl',
fontWeight: 'bold',
color: isDark ? 'gray.100' : 'gray.900',
})}
>
Know Your World 🌍
</h1>
<p
className={css({
fontSize: 'sm',
color: isDark ? 'gray.400' : 'gray.600',
marginTop: '1',
})}
>
Click continents to zoom in, or start playing from any level
</p>
</div>
{/* Drill-Down Map Selector */}
<div data-section="map-selection">
<DrillDownMapSelector
selectedMap={state.selectedMap}
selectedContinent={state.selectedContinent}
onSelectionChange={handleSelectionChange}
onStartGame={startGame}
includeSizes={state.includeSizes}
onRegionSizesChange={setRegionSizes}
regionCountsBySize={regionCountsBySize}
/>
</div>
{/* Settings Row with Radix Selects */}
<div
data-section="settings"
className={css({
display: 'grid',
gridTemplateColumns: '1fr', // Stack on mobile
gap: '4',
padding: '5',
bg: isDark ? 'gray.800/50' : 'gray.50',
rounded: '2xl',
border: '1px solid',
borderColor: isDark ? 'gray.700' : 'gray.200',
md: {
gridTemplateColumns: 'repeat(3, 1fr)', // 3 columns on desktop
},
})}
>
{/* Game Mode */}
<div data-setting="game-mode" className={css({ display: 'flex', flexDirection: 'column' })}>
<label className={labelStyles}>Mode</label>
<Select.Root
value={state.gameMode}
onValueChange={(value) => setMode(value as 'cooperative' | 'race' | 'turn-based')}
>
<Select.Trigger className={triggerStyles}>
<span className={css({ fontSize: '2xl' })}>{selectedMode?.emoji}</span>
<div className={css({ flex: 1, textAlign: 'left' })}>
<div
className={css({
fontWeight: '600',
color: isDark ? 'gray.100' : 'gray.900',
fontSize: 'sm',
})}
>
{selectedMode?.label}
</div>
<div
className={css({
fontSize: 'xs',
color: isDark ? 'gray.400' : 'gray.500',
lineHeight: 'tight',
})}
>
{selectedMode?.description}
</div>
</div>
<Select.Icon className={css({ color: isDark ? 'gray.400' : 'gray.500' })}>
</Select.Icon>
</Select.Trigger>
<Select.Portal>
<Select.Content className={contentStyles} position="popper" sideOffset={5}>
<Select.Viewport>
{GAME_MODE_OPTIONS.map((option) => (
<Select.Item key={option.value} value={option.value} className={itemStyles}>
<span className={css({ fontSize: '2xl' })}>{option.emoji}</span>
<div className={css({ flex: 1 })}>
<Select.ItemText>
<span
className={css({
fontWeight: '600',
color: isDark ? 'gray.100' : 'gray.900',
fontSize: 'sm',
})}
>
{option.label}
</span>
</Select.ItemText>
<div
className={css({
fontSize: 'xs',
color: isDark ? 'gray.400' : 'gray.500',
lineHeight: 'tight',
})}
>
{option.description}
</div>
</div>
</Select.Item>
))}
</Select.Viewport>
</Select.Content>
</Select.Portal>
</Select.Root>
</div>
{/* Assistance Level */}
<div
data-setting="assistance-level"
className={css({ display: 'flex', flexDirection: 'column' })}
>
<label className={labelStyles}>Assistance</label>
<Select.Root
value={state.assistanceLevel}
onValueChange={(value) =>
setAssistanceLevel(value as 'guided' | 'helpful' | 'standard' | 'none')
}
>
<Select.Trigger className={triggerStyles}>
<span className={css({ fontSize: '2xl' })}>{selectedAssistance?.emoji || '💡'}</span>
<div className={css({ flex: 1, textAlign: 'left' })}>
<div
className={css({
fontWeight: '600',
color: isDark ? 'gray.100' : 'gray.900',
fontSize: 'sm',
})}
>
{selectedAssistance?.label}
</div>
<div
className={css({
fontSize: 'xs',
color: isDark ? 'gray.400' : 'gray.500',
lineHeight: 'tight',
})}
>
{selectedAssistance?.description}
</div>
{/* Feature badges */}
{selectedAssistance && (
<div
className={css({
display: 'flex',
gap: '1',
marginTop: '1',
flexWrap: 'wrap',
})}
>
{getFeatureBadges(selectedAssistance).map((badge) => (
<span
key={badge.label}
className={css({
fontSize: '2xs',
padding: '0.5 1',
bg: isDark ? 'gray.700' : 'gray.200',
color: isDark ? 'gray.300' : 'gray.600',
rounded: 'sm',
})}
>
{badge.icon} {badge.label}
</span>
))}
</div>
)}
</div>
<Select.Icon className={css({ color: isDark ? 'gray.400' : 'gray.500' })}>
</Select.Icon>
</Select.Trigger>
<Select.Portal>
<Select.Content className={contentStyles} position="popper" sideOffset={5}>
<Select.Viewport>
{ASSISTANCE_LEVELS.map((level) => {
const badges = getFeatureBadges(level)
return (
<Select.Item key={level.id} value={level.id} className={itemStyles}>
<span className={css({ fontSize: '2xl' })}>{level.emoji}</span>
<div className={css({ flex: 1 })}>
<Select.ItemText>
<span
className={css({
fontWeight: '600',
color: isDark ? 'gray.100' : 'gray.900',
fontSize: 'sm',
})}
>
{level.label}
</span>
</Select.ItemText>
<div
className={css({
fontSize: 'xs',
color: isDark ? 'gray.400' : 'gray.500',
lineHeight: 'tight',
})}
>
{level.description}
</div>
{/* Feature badges */}
<div
className={css({
display: 'flex',
gap: '1',
marginTop: '1',
flexWrap: 'wrap',
})}
>
{badges.map((badge) => (
<span
key={badge.label}
className={css({
fontSize: '2xs',
padding: '0.5 1',
bg: isDark ? 'gray.700' : 'gray.200',
color: isDark ? 'gray.300' : 'gray.600',
rounded: 'sm',
})}
>
{badge.icon} {badge.label}
</span>
))}
</div>
</div>
</Select.Item>
)
})}
</Select.Viewport>
</Select.Content>
</Select.Portal>
</Select.Root>
</div>
{/* Study Duration */}
<div
data-setting="study-duration"
className={css({ display: 'flex', flexDirection: 'column' })}
>
<label className={labelStyles}>Study Time</label>
<Select.Root
value={String(state.studyDuration)}
onValueChange={(value) => setStudyDuration(Number(value) as 0 | 30 | 60 | 120)}
>
<Select.Trigger className={triggerStyles}>
<span className={css({ fontSize: '2xl' })}>{selectedStudyTime?.emoji}</span>
<div className={css({ flex: 1, textAlign: 'left' })}>
<div
className={css({
fontWeight: '600',
color: isDark ? 'gray.100' : 'gray.900',
fontSize: 'sm',
})}
>
{selectedStudyTime?.label}
</div>
<div
className={css({
fontSize: 'xs',
color: isDark ? 'gray.400' : 'gray.500',
lineHeight: 'tight',
})}
>
{selectedStudyTime?.description}
</div>
</div>
<Select.Icon className={css({ color: isDark ? 'gray.400' : 'gray.500' })}>
</Select.Icon>
</Select.Trigger>
<Select.Portal>
<Select.Content className={contentStyles} position="popper" sideOffset={5}>
<Select.Viewport>
{STUDY_TIME_OPTIONS.map((option) => (
<Select.Item
key={option.value}
value={String(option.value)}
className={itemStyles}
>
<span className={css({ fontSize: '2xl' })}>{option.emoji}</span>
<div className={css({ flex: 1 })}>
<Select.ItemText>
<span
className={css({
fontWeight: '600',
color: isDark ? 'gray.100' : 'gray.900',
fontSize: 'sm',
})}
>
{option.label}
</span>
</Select.ItemText>
<div
className={css({
fontSize: 'xs',
color: isDark ? 'gray.400' : 'gray.500',
lineHeight: 'tight',
})}
>
{option.description}
</div>
</div>
</Select.Item>
))}
</Select.Viewport>
</Select.Content>
</Select.Portal>
</Select.Root>
</div>
</div>
{/* Tips Section */}
<div
data-element="tips"
className={css({
padding: '3',
bg: isDark ? 'gray.800/30' : 'gray.100/30',
rounded: 'lg',
fontSize: 'sm',
color: isDark ? 'gray.400' : 'gray.600',
textAlign: 'center',
})}
>
<strong>Tip:</strong> Press G to give up on a region.{' '}
{(state.assistanceLevel === 'guided' || state.assistanceLevel === 'helpful') &&
'Skipped regions return after 2-3 turns.'}
{state.assistanceLevel === 'standard' && 'Giving up counts against your score.'}
{state.assistanceLevel === 'none' && 'No assistance available in this mode.'}
{selectedAssistance?.hintsMode === 'onRequest' && ' Press H for hints!'}
</div>
</div>
)
}