diff --git a/src/pages/travel/MiniMap.css b/src/pages/travel/MiniMap.css new file mode 100644 index 0000000..8e2f524 --- /dev/null +++ b/src/pages/travel/MiniMap.css @@ -0,0 +1,106 @@ +/* ── MiniMap ── */ + +.minimap-wrapper { + width: 100%; +} + +/* toolbar */ +.minimap-toolbar { + display: flex; + align-items: center; + gap: 10px; + margin-bottom: 8px; +} + +.minimap-toggle-btn, +.minimap-clear-btn { + background: var(--tv-surface); + color: var(--tv-muted); + border: 1px solid var(--tv-line-bright); + border-radius: var(--tv-r-sm); + padding: 5px 14px; + font: 11px var(--tv-mono); + letter-spacing: 0.04em; + cursor: pointer; + transition: color 0.2s, border-color 0.2s; +} + +.minimap-toggle-btn:hover, +.minimap-clear-btn:hover { + color: var(--tv-text); + border-color: var(--tv-accent); +} + +/* container */ +.minimap-container { + position: relative; + height: var(--minimap-h, 200px); + border-radius: var(--tv-r-lg); + border: 1px solid var(--tv-line-bright); + box-shadow: 0 2px 16px rgba(0, 0, 0, 0.35); + overflow: hidden; + transition: height 0.35s ease, opacity 0.35s ease; + opacity: 1; +} + +.minimap-collapsed { + height: 0 !important; + opacity: 0; + pointer-events: none; + border: none; + box-shadow: none; +} + +/* leaflet overrides */ +.minimap-leaflet { + background: var(--tv-bg); +} + +.minimap-leaflet .leaflet-tile-pane { + filter: brightness(0.7) saturate(0.4); +} + +/* hint overlay */ +.minimap-hint { + position: absolute; + bottom: 10px; + left: 50%; + transform: translateX(-50%); + z-index: 800; + font: 10px var(--tv-mono); + letter-spacing: 0.18em; + color: var(--tv-dim); + background: rgba(15, 12, 9, 0.65); + padding: 4px 14px; + border-radius: var(--tv-r-sm); + pointer-events: none; +} + +/* tooltip */ +.minimap-tooltip { + background: var(--tv-surface) !important; + color: var(--tv-text) !important; + border: 1px solid var(--tv-line-bright) !important; + border-radius: var(--tv-r-sm) !important; + font: 11px var(--tv-mono) !important; + padding: 3px 10px !important; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.4) !important; +} + +.minimap-tooltip::before { + border-top-color: var(--tv-surface) !important; +} + +/* mobile */ +@media (max-width: 768px) { + .minimap-container { + height: 150px; + } +} + +/* reduced motion */ +@media (prefers-reduced-motion: reduce) { + .minimap-container { + transition: none; + } +} diff --git a/src/pages/travel/MiniMap.jsx b/src/pages/travel/MiniMap.jsx new file mode 100644 index 0000000..8bcbb93 --- /dev/null +++ b/src/pages/travel/MiniMap.jsx @@ -0,0 +1,166 @@ +import React, { useState, useCallback } from 'react'; +import { MapContainer, TileLayer, GeoJSON, useMap } from 'react-leaflet'; +import 'leaflet/dist/leaflet.css'; +import { useIsMobile } from '../../hooks/useIsMobile'; +import './MiniMap.css'; + +/* ───────────────────────────────────────────── + Region accent palette +───────────────────────────────────────────── */ +export const REGION_PALETTE = { + japan: '#e05c4b', + korea: '#d64f6e', + china: '#c84b3a', + europe: '#5b8fc4', + france: '#6f8fc4', + italy: '#78a46e', + spain: '#c4844a', + sea: '#4aad8b', + thailand: '#4aad8b', + vietnam: '#5faa78', + bali: '#7aac5a', + indonesia: '#8aaa4a', + america: '#b4885c', + usa: '#b4885c', + canada: '#6a9890', + africa: '#c47c3c', + middle: '#c4a24a', + dubai: '#c4a24a', + default: '#c8905e', +}; + +export function getRegionAccent(regionId = '') { + const id = regionId.toLowerCase(); + for (const [key, color] of Object.entries(REGION_PALETTE)) { + if (key !== 'default' && id.includes(key)) return color; + } + return REGION_PALETTE.default; +} + +/* ───────────────────────────────────────────── + MapLayer — internal component +───────────────────────────────────────────── */ +function MapLayer({ geojson, selectedRegionId, onSelectRegion }) { + const map = useMap(); + + const style = useCallback( + (feature) => { + const rid = feature.properties?.id || feature.properties?.name || ''; + const isSelected = + selectedRegionId && rid.toLowerCase() === selectedRegionId.toLowerCase(); + const accent = getRegionAccent(rid); + return { + fillColor: isSelected ? accent : 'rgba(232,221,208,0.12)', + fillOpacity: isSelected ? 0.45 : 0.18, + color: isSelected ? accent : 'rgba(232,221,208,0.25)', + weight: isSelected ? 2.5 : 1, + }; + }, + [selectedRegionId], + ); + + const onEachFeature = useCallback( + (feature, layer) => { + const name = + feature.properties?.name_ko || + feature.properties?.name || + feature.properties?.id || + ''; + if (name) { + layer.bindTooltip(name, { + className: 'minimap-tooltip', + sticky: true, + }); + } + layer.on('click', () => { + const rid = feature.properties?.id || feature.properties?.name || ''; + onSelectRegion(rid); + const bounds = layer.getBounds(); + if (bounds.isValid()) { + map.fitBounds(bounds, { padding: [30, 30], maxZoom: 6 }); + } + }); + }, + [map, onSelectRegion], + ); + + if (!geojson) return null; + + return ( + + ); +} + +/* ───────────────────────────────────────────── + MiniMap +───────────────────────────────────────────── */ +export default function MiniMap({ + geojson, + selectedRegionId, + onSelectRegion, + onClearRegion, +}) { + const [expanded, setExpanded] = useState(true); + const isMobile = useIsMobile(); + + const toggleExpanded = () => setExpanded((v) => !v); + + return ( +
+ {/* toolbar */} +
+ + + {selectedRegionId && ( + + )} +
+ + {/* map container */} +
+ + + + + + {!selectedRegionId && expanded && ( +
CLICK A REGION
+ )} +
+
+ ); +}