import React, { useEffect, useState, useRef } from 'react' import { useNavigate } from 'react-router-dom' import { getIntlLanguage, getLocaleForLanguage, useTranslation } from '../i18n' import { useSettingsStore } from '../store/settingsStore' import Navbar from '../components/Layout/Navbar' import apiClient from '../api/client' import CustomSelect from '../components/shared/CustomSelect' import { Globe, MapPin, Briefcase, Calendar, Flag, ChevronRight, PanelLeftOpen, PanelLeftClose, X, Star, Plus, Trash2 } from 'lucide-react' import L from 'leaflet' import type { AtlasPlace, GeoJsonFeatureCollection, TranslationFn } from '../types' // Convert country code to flag emoji interface AtlasCountry { code: string tripCount: number placeCount: number firstVisit?: string | null lastVisit?: string | null } interface AtlasStats { totalTrips: number totalPlaces: number totalCountries: number totalDays: number totalCities?: number } interface AtlasData { countries: AtlasCountry[] stats: AtlasStats mostVisited?: AtlasCountry | null continents?: Record lastTrip?: { id: number; title: string; countryCode?: string } | null nextTrip?: { id: number; title: string; countryCode?: string } | null streak?: number firstYear?: number tripsThisYear?: number } interface CountryDetail { places: AtlasPlace[] trips: { id: number; title: string }[] manually_marked?: boolean } function MobileStats({ data, stats, countries, resolveName, t, dark }: { data: AtlasData | null; stats: AtlasStats; countries: AtlasCountry[]; resolveName: (code: string) => string; t: TranslationFn; dark: boolean }): React.ReactElement { const tp = dark ? '#f1f5f9' : '#0f172a' const tf = dark ? '#475569' : '#94a3b8' const { continents, lastTrip, nextTrip, streak, firstYear, tripsThisYear } = data || {} const CL = { 'Europe': t('atlas.europe'), 'Asia': t('atlas.asia'), 'North America': t('atlas.northAmerica'), 'South America': t('atlas.southAmerica'), 'Africa': t('atlas.africa'), 'Oceania': t('atlas.oceania') } const thisYear = new Date().getFullYear() return (
{/* Stats grid */}
{[[stats.totalCountries, t('atlas.countries')], [stats.totalTrips, t('atlas.trips')], [stats.totalPlaces, t('atlas.places')], [stats.totalCities || 0, t('atlas.cities')], [stats.totalDays, t('atlas.days')]].map(([v, l], i) => (

{v}

{l}

))}
{/* Continents */}
{['Europe', 'Asia', 'North America', 'South America', 'Africa', 'Oceania'].map(cont => { const count = continents?.[cont] || 0 return (

0 ? tp : (dark ? 'rgba(255,255,255,0.15)' : 'rgba(0,0,0,0.12)') }}>{count}

0 ? tf : (dark ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.08)') }}>{CL[cont]}

) })}
{/* Highlights */}
{streak > 0 && (

{streak}

{streak === 1 ? t('atlas.yearInRow') : t('atlas.yearsInRow')}

)} {tripsThisYear > 0 && (

{tripsThisYear}

{tripsThisYear === 1 ? t('atlas.tripIn') : t('atlas.tripsIn')} {thisYear}

)}
) } function countryCodeToFlag(code: string): string { if (!code || code.length !== 2) return '' return String.fromCodePoint(...[...code.toUpperCase()].map(c => 0x1F1E6 + c.charCodeAt(0) - 65)) } function useCountryNames(language: string): (code: string) => string { const [resolver, setResolver] = useState<(code: string) => string>(() => (code: string) => code) useEffect(() => { try { const dn = new Intl.DisplayNames([getIntlLanguage(language)], { type: 'region' }) setResolver(() => (code: string) => { try { return dn.of(code) || code } catch { return code } }) } catch { /* */ } }, [language]) return resolver } // Map visited country codes to ISO-3166 alpha3 (GeoJSON uses alpha3) // Built dynamically from GeoJSON + hardcoded fallbacks const A2_TO_A3_BASE: Record = {"AF":"AFG","AL":"ALB","DZ":"DZA","AD":"AND","AO":"AGO","AG":"ATG","AR":"ARG","AM":"ARM","AU":"AUS","AT":"AUT","AZ":"AZE","BS":"BHS","BH":"BHR","BD":"BGD","BB":"BRB","BY":"BLR","BE":"BEL","BZ":"BLZ","BJ":"BEN","BT":"BTN","BO":"BOL","BA":"BIH","BW":"BWA","BR":"BRA","BN":"BRN","BG":"BGR","BF":"BFA","BI":"BDI","CV":"CPV","KH":"KHM","CM":"CMR","CA":"CAN","CF":"CAF","TD":"TCD","CL":"CHL","CN":"CHN","CO":"COL","KM":"COM","CG":"COG","CD":"COD","CR":"CRI","CI":"CIV","HR":"HRV","CU":"CUB","CY":"CYP","CZ":"CZE","DK":"DNK","DJ":"DJI","DM":"DMA","DO":"DOM","EC":"ECU","EG":"EGY","SV":"SLV","GQ":"GNQ","ER":"ERI","EE":"EST","SZ":"SWZ","ET":"ETH","FJ":"FJI","FI":"FIN","FR":"FRA","GA":"GAB","GM":"GMB","GE":"GEO","DE":"DEU","GH":"GHA","GR":"GRC","GD":"GRD","GT":"GTM","GN":"GIN","GW":"GNB","GY":"GUY","HT":"HTI","HN":"HND","HU":"HUN","IS":"ISL","IN":"IND","ID":"IDN","IR":"IRN","IQ":"IRQ","IE":"IRL","IL":"ISR","IT":"ITA","JM":"JAM","JP":"JPN","JO":"JOR","KZ":"KAZ","KE":"KEN","KI":"KIR","KP":"PRK","KR":"KOR","KW":"KWT","KG":"KGZ","LA":"LAO","LV":"LVA","LB":"LBN","LS":"LSO","LR":"LBR","LY":"LBY","LI":"LIE","LT":"LTU","LU":"LUX","MG":"MDG","MW":"MWI","MY":"MYS","MV":"MDV","ML":"MLI","MT":"MLT","MR":"MRT","MU":"MUS","MX":"MEX","MD":"MDA","MN":"MNG","ME":"MNE","MA":"MAR","MZ":"MOZ","MM":"MMR","NA":"NAM","NP":"NPL","NL":"NLD","NZ":"NZL","NI":"NIC","NE":"NER","NG":"NGA","MK":"MKD","NO":"NOR","OM":"OMN","PK":"PAK","PA":"PAN","PG":"PNG","PY":"PRY","PE":"PER","PH":"PHL","PL":"POL","PT":"PRT","QA":"QAT","RO":"ROU","RU":"RUS","RW":"RWA","SA":"SAU","SN":"SEN","RS":"SRB","SL":"SLE","SG":"SGP","SK":"SVK","SI":"SVN","SB":"SLB","SO":"SOM","ZA":"ZAF","SS":"SSD","ES":"ESP","LK":"LKA","SD":"SDN","SR":"SUR","SE":"SWE","CH":"CHE","SY":"SYR","TW":"TWN","TJ":"TJK","TZ":"TZA","TH":"THA","TL":"TLS","TG":"TGO","TT":"TTO","TN":"TUN","TR":"TUR","TM":"TKM","UG":"UGA","UA":"UKR","AE":"ARE","GB":"GBR","US":"USA","UY":"URY","UZ":"UZB","VU":"VUT","VE":"VEN","VN":"VNM","YE":"YEM","ZM":"ZMB","ZW":"ZWE"} let A2_TO_A3: Record = { ...A2_TO_A3_BASE } export default function AtlasPage(): React.ReactElement { const { t, language } = useTranslation() const { settings } = useSettingsStore() const navigate = useNavigate() const resolveName = useCountryNames(language) const dm = settings.dark_mode const dark = dm === true || dm === 'dark' || (dm === 'auto' && window.matchMedia('(prefers-color-scheme: dark)').matches) const mapRef = useRef(null) const mapInstance = useRef(null) const geoLayerRef = useRef(null) const glareRef = useRef(null) const borderGlareRef = useRef(null) const panelRef = useRef(null) const handlePanelMouseMove = (e: React.MouseEvent): void => { if (!panelRef.current || !glareRef.current || !borderGlareRef.current) return const rect = panelRef.current.getBoundingClientRect() const x = e.clientX - rect.left const y = e.clientY - rect.top // Subtle inner glow glareRef.current.style.background = `radial-gradient(circle 300px at ${x}px ${y}px, ${dark ? 'rgba(255,255,255,0.025)' : 'rgba(255,255,255,0.25)'} 0%, transparent 70%)` glareRef.current.style.opacity = '1' // Border glow that follows cursor borderGlareRef.current.style.opacity = '1' borderGlareRef.current.style.maskImage = `radial-gradient(circle 150px at ${x}px ${y}px, black 0%, transparent 100%)` borderGlareRef.current.style.WebkitMaskImage = `radial-gradient(circle 150px at ${x}px ${y}px, black 0%, transparent 100%)` } const handlePanelMouseLeave = () => { if (glareRef.current) glareRef.current.style.opacity = '0' if (borderGlareRef.current) borderGlareRef.current.style.opacity = '0' } const [data, setData] = useState(null) const [loading, setLoading] = useState(true) const [sidebarOpen, setSidebarOpen] = useState(true) const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false) const [selectedCountry, setSelectedCountry] = useState(null) const [countryDetail, setCountryDetail] = useState(null) const [geoData, setGeoData] = useState(null) const [confirmAction, setConfirmAction] = useState<{ type: 'mark' | 'unmark' | 'choose' | 'bucket'; code: string; name: string } | null>(null) const [bucketMonth, setBucketMonth] = useState(new Date().getMonth() + 1) const [bucketYear, setBucketYear] = useState(new Date().getFullYear()) // Bucket list interface BucketItem { id: number; name: string; lat: number | null; lng: number | null; country_code: string | null; notes: string | null } const [bucketList, setBucketList] = useState([]) const [showBucketAdd, setShowBucketAdd] = useState(false) const [bucketForm, setBucketForm] = useState({ name: '', notes: '' }) const [bucketTab, setBucketTab] = useState<'stats' | 'bucket'>('stats') const bucketMarkersRef = useRef(null) // Load atlas data + bucket list useEffect(() => { Promise.all([ apiClient.get('/addons/atlas/stats'), apiClient.get('/addons/atlas/bucket-list'), ]).then(([statsRes, bucketRes]) => { setData(statsRes.data) setBucketList(bucketRes.data.items || []) setLoading(false) }).catch(() => setLoading(false)) }, []) // Load GeoJSON world data (direct GeoJSON, no conversion needed) useEffect(() => { fetch('https://raw.githubusercontent.com/nvkelso/natural-earth-vector/master/geojson/ne_50m_admin_0_countries.geojson') .then(r => r.json()) .then(geo => { // Dynamically build A2→A3 mapping from GeoJSON for (const f of geo.features) { const a2 = f.properties?.ISO_A2 const a3 = f.properties?.ADM0_A3 || f.properties?.ISO_A3 if (a2 && a3 && a2 !== '-99' && a3 !== '-99' && !A2_TO_A3[a2]) { A2_TO_A3[a2] = a3 } } setGeoData(geo) }) .catch(() => {}) }, []) // Initialize map — runs after loading is done and mapRef is available useEffect(() => { if (loading || !mapRef.current) return if (mapInstance.current) { mapInstance.current.remove(); mapInstance.current = null } const map = L.map(mapRef.current, { center: [25, 0], zoom: 3, minZoom: 3, maxZoom: 7, zoomControl: false, attributionControl: false, maxBounds: [[-90, -220], [90, 220]], maxBoundsViscosity: 1.0, fadeAnimation: false, preferCanvas: true, }) L.control.zoom({ position: 'bottomright' }).addTo(map) const tileUrl = dark ? 'https://{s}.basemaps.cartocdn.com/dark_nolabels/{z}/{x}/{y}{r}.png' : 'https://{s}.basemaps.cartocdn.com/light_nolabels/{z}/{x}/{y}{r}.png' L.tileLayer(tileUrl, { maxZoom: 8, keepBuffer: 25, updateWhenZooming: true, updateWhenIdle: false, tileSize: 256, zoomOffset: 0, crossOrigin: true, loading: true, }).addTo(map) // Preload adjacent zoom level tiles L.tileLayer(tileUrl, { maxZoom: 8, keepBuffer: 10, opacity: 0, tileSize: 256, crossOrigin: true, }).addTo(map) mapInstance.current = map return () => { map.remove(); mapInstance.current = null } }, [dark, loading]) // Render GeoJSON countries useEffect(() => { if (!mapInstance.current || !geoData || !data) return const visitedA3 = new Set(data.countries.map(c => A2_TO_A3[c.code]).filter(Boolean)) const countryMap = {} data.countries.forEach(c => { if (A2_TO_A3[c.code]) countryMap[A2_TO_A3[c.code]] = c }) // Preserve current map view const currentCenter = mapInstance.current.getCenter() const currentZoom = mapInstance.current.getZoom() if (geoLayerRef.current) { mapInstance.current.removeLayer(geoLayerRef.current) } // Generate deterministic color per country code const VISITED_COLORS = ['#6366f1','#ec4899','#14b8a6','#f97316','#8b5cf6','#ef4444','#3b82f6','#22c55e','#06b6d4','#f43f5e','#a855f7','#10b981','#0ea5e9','#e11d48','#0d9488','#7c3aed','#2563eb','#dc2626','#059669','#d946ef'] // Assign colors in order of visit (by index in countries array) so no two neighbors share a color easily const visitedA3List = [...visitedA3] const colorMap = {} visitedA3List.forEach((a3, i) => { colorMap[a3] = VISITED_COLORS[i % VISITED_COLORS.length] }) const colorForCode = (a3) => colorMap[a3] || VISITED_COLORS[0] const canvasRenderer = L.canvas({ padding: 0.5, tolerance: 5 }) geoLayerRef.current = L.geoJSON(geoData, { renderer: canvasRenderer, interactive: true, bubblingMouseEvents: false, style: (feature) => { const a3 = feature.properties?.ADM0_A3 || feature.properties?.ISO_A3 || feature.properties?.['ISO3166-1-Alpha-3'] || feature.id const visited = visitedA3.has(a3) return { fillColor: visited ? colorForCode(a3) : (dark ? '#1e1e2e' : '#e2e8f0'), fillOpacity: visited ? 0.7 : 0.3, color: dark ? '#333' : '#cbd5e1', weight: 0.5, } }, onEachFeature: (feature, layer) => { const a3 = feature.properties?.ADM0_A3 || feature.properties?.ISO_A3 || feature.properties?.['ISO3166-1-Alpha-3'] || feature.id const c = countryMap[a3] if (c) { const name = resolveName(c.code) const formatDate = (d) => { if (!d) return '—'; const dt = new Date(d); return dt.toLocaleDateString(getLocaleForLanguage(language), { month: 'short', year: 'numeric' }) } const tooltipHtml = `
${name}
${c.tripCount} ${c.tripCount === 1 ? t('atlas.tripSingular') : t('atlas.tripPlural')}
${c.placeCount} ${c.placeCount === 1 ? t('atlas.placeVisited') : t('atlas.placesVisited')}
${t('atlas.firstVisit')} ${formatDate(c.firstVisit)}
${t('atlas.lastVisitLabel')} ${formatDate(c.lastVisit)}
` layer.bindTooltip(tooltipHtml, { sticky: false, permanent: false, className: 'atlas-tooltip', direction: 'top', offset: [0, -10], opacity: 1 }) layer.on('click', () => { if (c.placeCount === 0 && c.tripCount === 0) { // Manually marked only — show unmark popup handleUnmarkCountry(c.code) } else { loadCountryDetail(c.code) } }) layer.on('mouseover', (e) => { e.target.setStyle({ fillOpacity: 0.9, weight: 2, color: dark ? '#818cf8' : '#4f46e5' }) }) layer.on('mouseout', (e) => { geoLayerRef.current.resetStyle(e.target) }) } else { // Unvisited country — allow clicking to mark as visited // Reverse lookup: find A2 code from A3, or use A3 directly const a3ToA2Entry = Object.entries(A2_TO_A3).find(([, v]) => v === a3) const isoA2 = feature.properties?.ISO_A2 const countryCode = a3ToA2Entry ? a3ToA2Entry[0] : (isoA2 && isoA2 !== '-99' ? isoA2 : null) if (countryCode && countryCode !== '-99') { const name = feature.properties?.NAME || feature.properties?.ADMIN || resolveName(countryCode) layer.bindTooltip(`
${name}
`, { sticky: false, className: 'atlas-tooltip', direction: 'top', offset: [0, -10], opacity: 1 }) layer.on('click', () => handleMarkCountry(countryCode, name)) layer.on('mouseover', (e) => { e.target.setStyle({ fillOpacity: 0.5, weight: 1.5, color: dark ? '#555' : '#94a3b8' }) }) layer.on('mouseout', (e) => { geoLayerRef.current.resetStyle(e.target) }) } } } }).addTo(mapInstance.current) // Restore map view after re-render mapInstance.current.setView(currentCenter, currentZoom, { animate: false }) }, [geoData, data, dark]) const handleMarkCountry = (code: string, name: string): void => { setConfirmAction({ type: 'choose', code, name }) } const handleUnmarkCountry = (code: string): void => { const country = data?.countries.find(c => c.code === code) setConfirmAction({ type: 'unmark', code, name: resolveName(code) }) } const executeConfirmAction = async (): Promise => { if (!confirmAction) return const { type, code } = confirmAction setConfirmAction(null) // Update local state immediately (no API reload = no map re-render flash) if (type === 'mark') { apiClient.post(`/addons/atlas/country/${code}/mark`).catch(() => {}) setData(prev => { if (!prev || prev.countries.find(c => c.code === code)) return prev return { ...prev, countries: [...prev.countries, { code, placeCount: 0, tripCount: 0, firstVisit: null, lastVisit: null }], stats: { ...prev.stats, totalCountries: prev.stats.totalCountries + 1 }, } }) } else { apiClient.delete(`/addons/atlas/country/${code}/mark`).catch(() => {}) setSelectedCountry(null) setCountryDetail(null) setData(prev => { if (!prev) return prev const c = prev.countries.find(c => c.code === code) if (!c || c.placeCount > 0 || c.tripCount > 0) return prev return { ...prev, countries: prev.countries.filter(c => c.code !== code), stats: { ...prev.stats, totalCountries: Math.max(0, prev.stats.totalCountries - 1) }, } }) } } const handleAddBucketItem = async (): Promise => { if (!bucketForm.name.trim()) return try { const r = await apiClient.post('/addons/atlas/bucket-list', { name: bucketForm.name.trim(), notes: bucketForm.notes.trim() || null }) setBucketList(prev => [r.data.item, ...prev]) setBucketForm({ name: '', notes: '' }) setShowBucketAdd(false) } catch { /* */ } } const handleDeleteBucketItem = async (id: number): Promise => { try { await apiClient.delete(`/addons/atlas/bucket-list/${id}`) setBucketList(prev => prev.filter(i => i.id !== id)) } catch { /* */ } } // Render bucket list markers on map useEffect(() => { if (!mapInstance.current) return if (bucketMarkersRef.current) { mapInstance.current.removeLayer(bucketMarkersRef.current) } if (bucketList.length === 0) return const markers = bucketList.filter(b => b.lat && b.lng).map(b => { const icon = L.divIcon({ className: '', html: `
`, iconSize: [28, 28], iconAnchor: [14, 14], }) return L.marker([b.lat!, b.lng!], { icon }).bindTooltip( `
${b.name}
${b.notes ? `
${b.notes}
` : ''}`, { className: 'atlas-tooltip', direction: 'top', offset: [0, -14] } ) }) bucketMarkersRef.current = L.layerGroup(markers).addTo(mapInstance.current) }, [bucketList]) const loadCountryDetail = async (code: string): Promise => { setSelectedCountry(code) try { const r = await apiClient.get(`/addons/atlas/country/${code}`) setCountryDetail(r.data) } catch { /* */ } } const stats = data?.stats || { totalTrips: 0, totalPlaces: 0, totalCountries: 0, totalDays: 0 } const countries = data?.countries || [] if (loading) { return (
) } return (
{/* Map */}
{/* Mobile: Bottom bar */}
{/* Countries highlighted */}

{stats.totalCountries}

{t('atlas.countries')}

{[[stats.totalTrips, t('atlas.trips')], [stats.totalPlaces, t('atlas.places')], [stats.totalCities || 0, t('atlas.cities')], [stats.totalDays, t('atlas.days')]].map(([v, l], i) => (

{v}

{l}

))}
{/* Desktop Panel — bottom center, glass effect */}
{/* Liquid glass glare effect */}
{/* Border glow that follows cursor */}
navigate(`/trips/${id}`)} onUnmarkCountry={handleUnmarkCountry} bucketList={bucketList} bucketTab={bucketTab} setBucketTab={setBucketTab} showBucketAdd={showBucketAdd} setShowBucketAdd={setShowBucketAdd} bucketForm={bucketForm} setBucketForm={setBucketForm} onAddBucket={handleAddBucketItem} onDeleteBucket={handleDeleteBucketItem} t={t} dark={dark} />
{/* Country action popup */} {confirmAction && (
setConfirmAction(null)}>
e.stopPropagation()}> {confirmAction.code.length === 2 ? ( {confirmAction.code} ) : (
{countryCodeToFlag(confirmAction.code)}
)}

{confirmAction.name}

{confirmAction.type === 'choose' && (
)} {confirmAction.type === 'unmark' && ( <>

{t('atlas.confirmUnmark')}

)} {confirmAction.type === 'bucket' && ( <>

{t('atlas.bucketWhen')}

setBucketMonth(Number(v))} options={Array.from({ length: 12 }, (_, i) => ({ value: i + 1, label: new Date(2000, i).toLocaleString(language, { month: 'long' }) }))} size="sm" />
setBucketYear(Number(v))} options={Array.from({ length: 20 }, (_, i) => ({ value: new Date().getFullYear() + i, label: String(new Date().getFullYear() + i) }))} size="sm" />
)} {confirmAction.type === 'mark' && ( <>

{t('atlas.confirmMark')}

)}
)}
) } interface SidebarContentProps { data: AtlasData | null stats: AtlasStats countries: AtlasCountry[] selectedCountry: string | null countryDetail: CountryDetail | null resolveName: (code: string) => string onCountryClick: (code: string) => void onTripClick: (id: number) => void onUnmarkCountry?: (code: string) => void bucketList: any[] bucketTab: 'stats' | 'bucket' setBucketTab: (tab: 'stats' | 'bucket') => void showBucketAdd: boolean setShowBucketAdd: (v: boolean) => void bucketForm: { name: string; notes: string } setBucketForm: (f: { name: string; notes: string }) => void onAddBucket: () => Promise onDeleteBucket: (id: number) => Promise t: TranslationFn dark: boolean } function SidebarContent({ data, stats, countries, selectedCountry, countryDetail, resolveName, onCountryClick, onTripClick, onUnmarkCountry, bucketList, bucketTab, setBucketTab, showBucketAdd, setShowBucketAdd, bucketForm, setBucketForm, onAddBucket, onDeleteBucket, t, dark }: SidebarContentProps): React.ReactElement { const bg = (o) => dark ? `rgba(255,255,255,${o})` : `rgba(0,0,0,${o})` const tp = dark ? '#f1f5f9' : '#0f172a' const tm = dark ? '#94a3b8' : '#64748b' const tf = dark ? '#475569' : '#94a3b8' const accent = '#818cf8' const { mostVisited, continents, lastTrip, nextTrip, streak, firstYear, tripsThisYear } = data || {} const contEntries = continents ? Object.entries(continents).sort((a, b) => b[1] - a[1]) : [] const maxCont = contEntries.length > 0 ? contEntries[0][1] : 1 const CL = { 'Europe': t('atlas.europe'), 'Asia': t('atlas.asia'), 'North America': t('atlas.northAmerica'), 'South America': t('atlas.southAmerica'), 'Africa': t('atlas.africa'), 'Oceania': t('atlas.oceania') } const contColors = ['#818cf8', '#f472b6', '#34d399', '#fbbf24', '#fb923c', '#22d3ee'] // Tab switcher const tabBar = (
{[{ id: 'stats', label: t('atlas.statsTab'), icon: Globe }, { id: 'bucket', label: t('atlas.bucketTab'), icon: Star }].map(tab => ( ))}
) if (countries.length === 0 && !lastTrip && bucketTab !== 'bucket') { return ( <> {tabBar}

{t('atlas.noData')}

{t('atlas.noDataHint')}

) } const thisYear = new Date().getFullYear() const divider = `2px solid ${bg(0.08)}` // Bucket list content const bucketContent = (
{bucketList.map(item => (
{(() => { const code = item.country_code?.length === 2 ? item.country_code : (Object.entries(A2_TO_A3).find(([, v]) => v === item.country_code)?.[0] || '') return code ? ( {code} ) : })()} {item.name} {item.notes && {item.notes}}
))} {bucketList.length === 0 && (
{t('atlas.bucketEmptyHint')}
)}
) return ( <> {tabBar} {/* Both tabs always rendered so the wider one sets the panel width */}
{/* ═══ SECTION 1: Numbers ═══ */} {/* Countries hero */}
{stats.totalCountries} {t('atlas.countries')}
{/* Other stats */} {[[stats.totalTrips, t('atlas.trips')], [stats.totalPlaces, t('atlas.places')], [stats.totalCities || 0, t('atlas.cities')], [stats.totalDays, t('atlas.days')]].map(([v, l], i) => (
{v} {l}
))} {/* ═══ DIVIDER ═══ */}
{/* ═══ SECTION 2: Continents ═══ */}
{['Europe', 'Asia', 'North America', 'South America', 'Africa', 'Oceania'].map((cont) => { const count = continents?.[cont] || 0 const active = count > 0 return (
{count} {CL[cont]}
) })}
{/* ═══ DIVIDER ═══ */}
{/* ═══ SECTION 3: Highlights & Streaks ═══ */}
{/* Last trip */} {lastTrip && ( )} {/* Streak */} {streak > 0 && (
{streak} {streak === 1 ? t('atlas.yearInRow') : t('atlas.yearsInRow')}
)} {/* This year */} {tripsThisYear > 0 && (
{tripsThisYear} {tripsThisYear === 1 ? t('atlas.tripIn') : t('atlas.tripsIn')} {thisYear}
)}
{/* ═══ Country detail overlay ═══ */} {selectedCountry && countryDetail && ( <>
{countryCodeToFlag(selectedCountry)}

{resolveName(selectedCountry)}

{countryDetail.places.length} {t('atlas.places')} · {countryDetail.trips.length} Trips

{countryDetail.trips.slice(0, 3).map(trip => ( ))} {countryDetail.manually_marked && onUnmarkCountry && ( )}
)}
{bucketContent}
) }