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 { 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>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue