feat: add interactive world map continent selector

Replace continent button grid with interactive world map selector:

Interactive Map Features:
- Visual world map showing all continents with color-coded regions
- Click any continent on the map to select it
- Hover effects show which continent you're pointing at
- Selected continent highlighted in solid blue
- Unselected continents shown in subtle gray

Visual Feedback:
- Selected: Solid blue fill with blue stroke
- Hovered: Semi-transparent blue overlay
- Unselected: Light gray with subtle borders
- Smooth transitions between states

Legend Buttons:
- Small buttons below map for quick access
- All 7 continents + "All" option
- Synchronized with map selection
- Hover on button also highlights continent on map

UX Improvements:
- More intuitive than button grid
- Visual context of where continents are located
- Easier to understand geographic relationships
- Better use of screen space
- Instructions text: "Click a continent to focus on it, or select 'All' for the whole world"

Technical Implementation:
- Create ContinentSelector component with interactive SVG
- Group world map regions by continent
- Bidirectional hover state (map ↔ buttons)
- Reuse existing continent grouping logic
- Maintain all existing functionality (persistence, filtering, etc.)

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Thomas Hallock 2025-11-18 15:55:32 -06:00
parent 7bb03b8409
commit 245005c8ec
2 changed files with 237 additions and 73 deletions

View File

@ -0,0 +1,232 @@
'use client'
import { useState } from 'react'
import { css } from '@styled/css'
import { useTheme } from '@/contexts/ThemeContext'
import { WORLD_MAP } from '../maps'
import { getContinentForCountry, CONTINENTS, type ContinentId } from '../continents'
interface ContinentSelectorProps {
selectedContinent: ContinentId | 'all'
onSelectContinent: (continent: ContinentId | 'all') => void
}
export function ContinentSelector({
selectedContinent,
onSelectContinent,
}: ContinentSelectorProps) {
const { resolvedTheme } = useTheme()
const isDark = resolvedTheme === 'dark'
const [hoveredContinent, setHoveredContinent] = useState<ContinentId | 'all' | null>(null)
// Group regions by continent
const regionsByContinent = new Map<ContinentId | 'all', typeof WORLD_MAP.regions>()
regionsByContinent.set('all', []) // Initialize all continents
CONTINENTS.forEach((continent) => {
regionsByContinent.set(continent.id, [])
})
WORLD_MAP.regions.forEach((region) => {
const continent = getContinentForCountry(region.id)
if (continent) {
regionsByContinent.get(continent)?.push(region)
}
})
// Get color for continent based on state
const getContinentColor = (continentId: ContinentId | 'all'): string => {
const isSelected = selectedContinent === continentId
const isHovered = hoveredContinent === continentId
if (isSelected) {
return isDark ? '#3b82f6' : '#2563eb' // Solid blue for selected
}
if (isHovered) {
return isDark ? '#60a5fa66' : '#3b82f655' // Semi-transparent blue for hover
}
return isDark ? '#4b556333' : '#d1d5db44' // Very light for unselected
}
const getContinentStroke = (continentId: ContinentId | 'all'): string => {
const isSelected = selectedContinent === continentId
const isHovered = hoveredContinent === continentId
if (isSelected) {
return isDark ? '#60a5fa' : '#1d4ed8'
}
if (isHovered) {
return isDark ? '#93c5fd' : '#3b82f6'
}
return isDark ? '#374151' : '#9ca3af'
}
return (
<div data-component="continent-selector">
<div
className={css({
display: 'flex',
flexDirection: 'column',
gap: '2',
marginBottom: '2',
})}
>
<div
className={css({
fontSize: 'sm',
color: isDark ? 'gray.400' : 'gray.600',
textAlign: 'center',
})}
>
Click a continent to focus on it, or select &quot;All&quot; for the whole world
</div>
</div>
{/* Interactive Map */}
<div
className={css({
width: '100%',
padding: '4',
bg: isDark ? 'gray.900' : 'gray.50',
rounded: 'xl',
border: '2px solid',
borderColor: isDark ? 'gray.700' : 'gray.200',
})}
>
<svg
viewBox={WORLD_MAP.viewBox}
className={css({
width: '100%',
height: 'auto',
cursor: 'pointer',
})}
>
{/* Background */}
<rect x="0" y="0" width="100%" height="100%" fill={isDark ? '#111827' : '#f9fafb'} />
{/* Render each continent as a group */}
{CONTINENTS.map((continent) => {
const regions = regionsByContinent.get(continent.id) || []
if (regions.length === 0) return null
return (
<g
key={continent.id}
data-continent={continent.id}
onMouseEnter={() => setHoveredContinent(continent.id)}
onMouseLeave={() => setHoveredContinent(null)}
onClick={() => onSelectContinent(continent.id)}
style={{
cursor: 'pointer',
transition: 'all 0.2s ease',
}}
>
{/* All regions in this continent */}
{regions.map((region) => (
<path
key={region.id}
d={region.path}
fill={getContinentColor(continent.id)}
stroke={getContinentStroke(continent.id)}
strokeWidth={selectedContinent === continent.id ? 1 : 0.5}
style={{
pointerEvents: 'none',
transition: 'all 0.2s ease',
}}
/>
))}
</g>
)
})}
</svg>
</div>
{/* Legend/Buttons below map */}
<div
className={css({
display: 'grid',
gridTemplateColumns: 'repeat(4, 1fr)',
gap: '2',
marginTop: '3',
})}
>
{/* All option */}
<button
data-action="select-all-continents"
onClick={() => onSelectContinent('all')}
onMouseEnter={() => setHoveredContinent('all')}
onMouseLeave={() => setHoveredContinent(null)}
className={css({
padding: '2',
rounded: 'lg',
border: '2px solid',
borderColor: selectedContinent === 'all' ? 'blue.500' : 'transparent',
bg:
selectedContinent === 'all'
? isDark
? 'blue.900'
: 'blue.50'
: hoveredContinent === 'all'
? isDark
? 'gray.700'
: 'gray.200'
: isDark
? 'gray.800'
: 'gray.100',
color: isDark ? 'gray.100' : 'gray.900',
cursor: 'pointer',
transition: 'all 0.2s',
fontSize: 'xs',
fontWeight: selectedContinent === 'all' ? 'bold' : 'normal',
_hover: {
borderColor: 'blue.400',
},
})}
>
<div className={css({ fontSize: 'lg' })}>🌍</div>
<div>All</div>
</button>
{/* Continent buttons */}
{CONTINENTS.map((continent) => (
<button
key={continent.id}
data-action={`select-${continent.id}-continent`}
onClick={() => onSelectContinent(continent.id)}
onMouseEnter={() => setHoveredContinent(continent.id)}
onMouseLeave={() => setHoveredContinent(null)}
className={css({
padding: '2',
rounded: 'lg',
border: '2px solid',
borderColor: selectedContinent === continent.id ? 'blue.500' : 'transparent',
bg:
selectedContinent === continent.id
? isDark
? 'blue.900'
: 'blue.50'
: hoveredContinent === continent.id
? isDark
? 'gray.700'
: 'gray.200'
: isDark
? 'gray.800'
: 'gray.100',
color: isDark ? 'gray.100' : 'gray.900',
cursor: 'pointer',
transition: 'all 0.2s',
fontSize: 'xs',
fontWeight: selectedContinent === continent.id ? 'bold' : 'normal',
_hover: {
borderColor: 'blue.400',
},
})}
>
<div className={css({ fontSize: 'lg' })}>{continent.emoji}</div>
<div>{continent.name}</div>
</button>
))}
</div>
</div>
)
}

View File

@ -3,7 +3,7 @@
import { css } from '@styled/css' import { css } from '@styled/css'
import { useTheme } from '@/contexts/ThemeContext' import { useTheme } from '@/contexts/ThemeContext'
import { useKnowYourWorld } from '../Provider' import { useKnowYourWorld } from '../Provider'
import { CONTINENTS } from '../continents' import { ContinentSelector } from './ContinentSelector'
export function SetupPhase() { export function SetupPhase() {
const { resolvedTheme } = useTheme() const { resolvedTheme } = useTheme()
@ -123,78 +123,10 @@ export function SetupPhase() {
> >
Focus on Continent 🌐 Focus on Continent 🌐
</h2> </h2>
<div <ContinentSelector
className={css({ selectedContinent={state.selectedContinent}
display: 'grid', onSelectContinent={setContinent}
gridTemplateColumns: 'repeat(4, 1fr)', />
gap: '3',
})}
>
{/* All continents option */}
<button
data-action="select-all-continents"
onClick={() => setContinent('all')}
className={css({
padding: '3',
rounded: 'lg',
border: '2px solid',
borderColor: state.selectedContinent === 'all' ? 'blue.500' : 'transparent',
bg:
state.selectedContinent === 'all'
? isDark
? 'blue.900'
: 'blue.50'
: isDark
? 'gray.800'
: 'gray.100',
color: isDark ? 'gray.100' : 'gray.900',
cursor: 'pointer',
transition: 'all 0.2s',
_hover: {
borderColor: 'blue.400',
},
})}
>
<div className={css({ fontSize: '2xl', marginBottom: '1' })}>🌍</div>
<div className={css({ fontSize: 'sm', fontWeight: 'bold' })}>All</div>
<div className={css({ fontSize: '2xs', color: isDark ? 'gray.400' : 'gray.600' })}>
Whole world
</div>
</button>
{/* Individual continents */}
{CONTINENTS.map((continent) => (
<button
key={continent.id}
data-action={`select-${continent.id}-continent`}
onClick={() => setContinent(continent.id)}
className={css({
padding: '3',
rounded: 'lg',
border: '2px solid',
borderColor:
state.selectedContinent === continent.id ? 'blue.500' : 'transparent',
bg:
state.selectedContinent === continent.id
? isDark
? 'blue.900'
: 'blue.50'
: isDark
? 'gray.800'
: 'gray.100',
color: isDark ? 'gray.100' : 'gray.900',
cursor: 'pointer',
transition: 'all 0.2s',
_hover: {
borderColor: 'blue.400',
},
})}
>
<div className={css({ fontSize: '2xl', marginBottom: '1' })}>{continent.emoji}</div>
<div className={css({ fontSize: 'sm', fontWeight: 'bold' })}>{continent.name}</div>
</button>
))}
</div>
</div> </div>
)} )}