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:
parent
7bb03b8409
commit
245005c8ec
|
|
@ -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 "All" 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>
|
||||
)
|
||||
}
|
||||
|
|
@ -3,7 +3,7 @@
|
|||
import { css } from '@styled/css'
|
||||
import { useTheme } from '@/contexts/ThemeContext'
|
||||
import { useKnowYourWorld } from '../Provider'
|
||||
import { CONTINENTS } from '../continents'
|
||||
import { ContinentSelector } from './ContinentSelector'
|
||||
|
||||
export function SetupPhase() {
|
||||
const { resolvedTheme } = useTheme()
|
||||
|
|
@ -123,78 +123,10 @@ export function SetupPhase() {
|
|||
>
|
||||
Focus on Continent 🌐
|
||||
</h2>
|
||||
<div
|
||||
className={css({
|
||||
display: 'grid',
|
||||
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>
|
||||
<ContinentSelector
|
||||
selectedContinent={state.selectedContinent}
|
||||
onSelectContinent={setContinent}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue