import React, { useEffect, useRef, useState } from 'react' import { useTranslation } from '../i18n' 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, Search } from 'lucide-react' import type { TranslationFn } from '../types' import { A2_TO_A3, countryCodeToFlag, type AtlasCountry, type AtlasStats, type AtlasData, type CountryDetail } from './atlas/atlasModel' import { useAtlas } from './atlas/useAtlas' 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}

)}
) } export default function AtlasPage(): React.ReactElement { // Page = wiring container: the whole interactive globe (map lifecycle, atlas + // bucket data, mark/unmark flows, country search) lives in useAtlas. The page // only wires that state into JSX and its presentational SidebarContent helper. const { t, language, navigate, resolveName, dark, loading, mapRef, regionTooltipRef, panelRef, glareRef, borderGlareRef, handlePanelMouseMove, handlePanelMouseLeave, data, setData, stats, countries, selectedCountry, countryDetail, loadCountryDetail, handleUnmarkCountry, select_country_from_search, visitedRegions, setVisitedRegions, atlas_country_search, set_atlas_country_search, atlas_country_results, set_atlas_country_results, atlas_country_open, set_atlas_country_open, atlas_country_options, confirmAction, setConfirmAction, executeConfirmAction, bucketMonth, setBucketMonth, bucketYear, setBucketYear, bucketList, setBucketList, bucketTab, setBucketTab, showBucketAdd, setShowBucketAdd, bucketForm, setBucketForm, handleAddBucketItem, handleDeleteBucketItem, handleBucketPoiSearch, handleSelectBucketPoi, bucketSearchResults, setBucketSearchResults, bucketPoiMonth, setBucketPoiMonth, bucketPoiYear, setBucketPoiYear, bucketSearching, bucketSearch, setBucketSearch, } = useAtlas() if (loading) { return (
) } return (
{/* Map */}
{/* Region tooltip (custom, always on top, ref-controlled to avoid re-renders) */}
{ const raw = e.target.value set_atlas_country_search(raw) const q = raw.trim().toLowerCase() if (!q) { set_atlas_country_results([]) set_atlas_country_open(false) return } const results = atlas_country_options .filter(o => o.label.toLowerCase().includes(q) || o.code.toLowerCase() === q) .slice(0, 8) set_atlas_country_results(results) set_atlas_country_open(true) }} onFocus={() => { if (atlas_country_results.length > 0) set_atlas_country_open(true) }} onKeyDown={(e) => { if (e.key === 'Escape') { set_atlas_country_open(false) return } if (e.key === 'Enter') { const first = atlas_country_results[0] if (first) select_country_from_search(first.code) } }} placeholder={t('atlas.searchCountry')} autoComplete="off" spellCheck={false} style={{ flex: 1, border: 'none', outline: 'none', background: 'transparent', fontSize: 13, fontFamily: 'inherit', color: 'var(--text-primary)', }} /> {atlas_country_search.trim() && ( )}
{atlas_country_open && atlas_country_results.length > 0 && (
set_atlas_country_open(false)} > {atlas_country_results.map((r) => ( ))}
)}
{/* 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} onSearchBucket={handleBucketPoiSearch} onSelectBucketPoi={handleSelectBucketPoi} bucketSearchResults={bucketSearchResults} setBucketSearchResults={setBucketSearchResults} bucketPoiMonth={bucketPoiMonth} setBucketPoiMonth={setBucketPoiMonth} bucketPoiYear={bucketPoiYear} setBucketPoiYear={setBucketPoiYear} bucketSearching={bucketSearching} bucketSearch={bucketSearch} setBucketSearch={setBucketSearch} 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 === 'choose-region' && (
{confirmAction.countryName && (

{confirmAction.countryName}

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

{t('atlas.confirmUnmark')}

)} {confirmAction.type === 'unmark-region' && ( <> {confirmAction.countryName && (

{confirmAction.countryName}

)}

{t('atlas.confirmUnmarkRegion')}

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

{t('atlas.bucketWhen')}

setBucketMonth(Number(v))} placeholder={t('atlas.month')} options={[ { value: '0', label: '—' }, ...Array.from({ length: 12 }, (_, i) => ({ value: String(i + 1), label: new Date(2000, i).toLocaleString(language, { month: 'long' }) })), ]} size="sm" />
setBucketYear(Number(v))} placeholder={t('atlas.year')} options={[ { value: '0', label: '—' }, ...Array.from({ length: 20 }, (_, i) => ({ value: String(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; lat: string; lng: string; target_date: string } setBucketForm: (f: { name: string; notes: string; lat: string; lng: string; target_date: string }) => void onAddBucket: () => Promise onDeleteBucket: (id: number) => Promise onSearchBucket: () => Promise onSelectBucketPoi: (result: any) => void bucketSearchResults: any[] setBucketSearchResults: (v: string[]) => void bucketPoiMonth: number setBucketPoiMonth: (v: number) => void bucketPoiYear: number setBucketPoiYear: (v: number) => void bucketSearching: boolean bucketSearch: string setBucketSearch: (v: string) => void t: TranslationFn dark: boolean } function SidebarContent({ data, stats, countries, selectedCountry, countryDetail, resolveName, onTripClick, onUnmarkCountry, bucketList, bucketTab, setBucketTab, showBucketAdd, setShowBucketAdd, bucketForm, setBucketForm, onAddBucket, onDeleteBucket, onSearchBucket, onSelectBucketPoi, bucketSearchResults, setBucketSearchResults, bucketPoiMonth, setBucketPoiMonth, bucketPoiYear, setBucketPoiYear, bucketSearching, bucketSearch, setBucketSearch, t, dark }: SidebarContentProps): React.ReactElement { const { language } = useTranslation() const statsContentRef = useRef(null) const [statsWidth, setStatsWidth] = useState(undefined) useEffect(() => { const el = statsContentRef.current if (!el || typeof ResizeObserver === 'undefined') return const ro = new ResizeObserver(() => setStatsWidth(el.offsetWidth)) ro.observe(el) return () => ro.disconnect() }, []) 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.target_date && (() => { const [y, m] = item.target_date.split('-') const label = m ? new Date(Number(y), Number(m) - 1).toLocaleString(language, { month: 'short', year: 'numeric' }) : y return {label} })()} {!item.target_date && item.notes && {item.notes}}
))} {bucketList.length === 0 && !showBucketAdd && (
{t('atlas.bucketEmptyHint')}
)}
{showBucketAdd ? (
{/* Search or manual name */}
{ const v = e.target.value; if (bucketForm.name) setBucketForm({ ...bucketForm, name: v }); else setBucketSearch(v) }} onKeyDown={e => { if (e.key === 'Enter' && !bucketForm.name) onSearchBucket(); else if (e.key === 'Enter') onAddBucket(); if (e.key === 'Escape') setShowBucketAdd(false) }} placeholder={t('atlas.bucketNamePlaceholder')} autoFocus style={{ flex: 1, padding: '6px 10px', borderRadius: 8, border: '1px solid var(--border-primary)', fontSize: 12, fontFamily: 'inherit', outline: 'none', boxSizing: 'border-box', color: 'var(--text-primary)', background: 'var(--bg-input)' }} /> {!bucketForm.name && ( )} {bucketForm.name && ( )}
{bucketSearchResults.length > 0 && (
{bucketSearchResults.slice(0, 6).map((r, i) => ( ))}
)}
{/* Selected place indicator */} {bucketForm.lat && bucketForm.lng && (
{Number(bucketForm.lat).toFixed(4)}, {Number(bucketForm.lng).toFixed(4)}
)} {/* Month / Year with CustomSelect */}
setBucketPoiMonth(Number(v))} placeholder={t('atlas.month')} size="sm" options={[{ value: '0', label: '—' }, ...Array.from({ length: 12 }, (_, i) => ({ value: String(i + 1), label: new Date(2000, i).toLocaleString(language, { month: 'short' }) }))]} />
setBucketPoiYear(Number(v))} placeholder={t('atlas.year')} size="sm" options={[{ value: '0', label: '—' }, ...Array.from({ length: 20 }, (_, i) => ({ value: String(new Date().getFullYear() + i), label: String(new Date().getFullYear() + i) }))]} />
) : (
)} ) 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}
) }