// ── URL hash ↔ state sync ───────────────────────────────────────────────── // Encodes base, amount, mapMode, and visibleCountries into a URL hash // so views are shareable. Parse once at boot; re-write on change (debounced). function parseHash() { try { var h = window.location.hash.replace(/^#/, ''); if (!h) return {}; var params = new URLSearchParams(h); var out = {}; if (params.get('base')) out.base = params.get('base').toUpperCase(); if (params.get('amt')) { var a = parseFloat(params.get('amt')); if (isFinite(a) && a > 0) out.amount = a; } if (params.get('mode')) out.mapMode = (params.get('mode') === 'globe' ? 'globe' : 'flat'); if (params.get('show')) out.visibleCountries = params.get('show').split(',').filter(Boolean); return out; } catch(e) { return {}; } } function writeHash(state) { try { var p = new URLSearchParams(); if (state.base) p.set('base', state.base); if (state.amount && state.amount !== 1) p.set('amt', String(state.amount)); if (state.mapMode === 'globe') p.set('mode', 'globe'); if (state.visibleCountries && Array.isArray(state.visibleCountries) && state.visibleCountries.length > 0) { p.set('show', state.visibleCountries.join(',')); } var s = p.toString(); var target = s ? ('#' + s) : ' '; if (window.location.hash !== target) { history.replaceState(null, '', window.location.pathname + window.location.search + (s ? '#' + s : '')); } } catch(e) {} } // ── Debounced localStorage helper ───────────────────────────────────────── // Collapses rapid writes (slider drag, typing) into one per 300ms. var __lsTimers = {}; function lsSet(key, value) { if (__lsTimers[key]) clearTimeout(__lsTimers[key]); __lsTimers[key] = setTimeout(function(){ try { localStorage.setItem(key, value); } catch(e) {} delete __lsTimers[key]; }, 300); } function App() { var hashInit = parseHash(); const [phase, setPhase] = React.useState(() => localStorage.getItem("atlas.phase") || "entry"); const [base, setBase] = React.useState(() => hashInit.base || localStorage.getItem("atlas.base") || "USD"); // Always start in flat mode on a fresh session — globe is opt-in via toggle. // Hash override wins (shared links can deep-link to globe). const [mapMode, setMapMode] = React.useState(() => hashInit.mapMode || "flat"); const [amount, setAmount] = React.useState(() => { if (hashInit.amount) return hashInit.amount; const v = parseFloat(localStorage.getItem("atlas.amount")); return isFinite(v) && v > 0 ? v : 1; }); const [visibleCountries, setVisibleCountries] = React.useState(hashInit.visibleCountries || null); React.useEffect(() => { window.RatesService.init(); }, []); React.useEffect(() => { lsSet("atlas.phase", phase); }, [phase]); React.useEffect(() => { lsSet("atlas.base", base); }, [base]); React.useEffect(() => { lsSet("atlas.amount", String(amount)); }, [amount]); // Sync state → URL hash (debounced via RAF — rapid changes collapse) React.useEffect(() => { var id = requestAnimationFrame(function(){ writeHash({ base: base, amount: amount, mapMode: mapMode, visibleCountries: visibleCountries }); }); return function(){ cancelAnimationFrame(id); }; }, [base, amount, mapMode, visibleCountries]); const [fading, setFading] = React.useState(false); const [transDir, setTransDir] = React.useState('in'); const enterMap = (code) => { setBase(code); setTransDir('in'); setFading(true); setTimeout(() => { setPhase("map"); setFading(false); }, 420); }; const exit = () => { setTransDir('out'); setFading(true); setTimeout(() => { setPhase("entry"); setFading(false); }, 320); }; return (
{phase === "entry" ? : }
); } const root = ReactDOM.createRoot(document.getElementById("root")); root.render();