import { useState, useEffect, useCallback, useMemo } from 'react'; import { trackPageview } from './hooks/usePlausible'; import MapPage from './components/map/MapPage'; import DataSourcesPage from './components/data-sources/DataSourcesPage'; import FAQPage from './components/faq/FAQPage'; import HomePage from './components/home/HomePage'; import Header, { type Page } from './components/ui/Header'; import type { FeatureMeta, FeatureGroup, POICategoriesResponse, POICategoryGroup } from './types'; import { fetchWithRetry, apiUrl } from './lib/api'; import { parseUrlState } from './lib/url-state'; import { INITIAL_VIEW_STATE } from './lib/consts'; import { useTheme } from './hooks/useTheme'; declare global { interface Window { __og_ready?: boolean; } } export default function App() { const urlState = useMemo(() => parseUrlState(), []); const initialViewState = useMemo(() => urlState.viewState || INITIAL_VIEW_STATE, []); const isScreenshotMode = useMemo(() => { const params = new URLSearchParams(window.location.search); return params.get('screenshot') === '1'; }, []); // Core data const [features, setFeatures] = useState([]); const [poiCategoryGroups, setPOICategoryGroups] = useState([]); const [initialLoading, setInitialLoading] = useState(true); // UI state const [pendingInfoFeature, setPendingInfoFeature] = useState(null); const [activePage, setActivePage] = useState(() => { if (isScreenshotMode) return 'dashboard'; if (window.history.state?.page) return window.history.state.page; const params = new URLSearchParams(window.location.search); return params.has('v') || params.has('f') || params.has('poi') || params.has('tab') ? 'dashboard' : 'home'; }); const { theme, toggleTheme } = useTheme(); // Load features and POI categories on mount useEffect(() => { const controller = new AbortController(); let featuresLoaded = false; let poisLoaded = false; const checkDone = () => { if (featuresLoaded && poisLoaded) setInitialLoading(false); }; fetchWithRetry<{ groups: FeatureGroup[] }>( apiUrl('features'), (json) => { const flat: FeatureMeta[] = json.groups.flatMap((g) => g.features.map((f) => ({ ...f, group: g.name })) ); setFeatures(flat); featuresLoaded = true; checkDone(); }, controller.signal ); fetchWithRetry( apiUrl('poi-categories'), (json) => { setPOICategoryGroups(json.groups); poisLoaded = true; checkDone(); }, controller.signal ); return () => controller.abort(); }, []); // Screenshot mode ready signal useEffect(() => { if (isScreenshotMode && !initialLoading && features.length > 0) { window.__og_ready = true; } }, [isScreenshotMode, initialLoading, features]); // Navigation const navigateTo = useCallback((page: Page, hash?: string, infoFeature?: string) => { if (infoFeature) { window.history.replaceState({ ...window.history.state, infoFeature }, ''); } const url = hash ? `${window.location.pathname}${window.location.search}#${hash}` : `${window.location.pathname}${window.location.search}`; window.history.pushState({ page }, '', url); setActivePage(page); trackPageview(); }, []); useEffect(() => { if (!window.history.state?.page) { window.history.replaceState({ page: activePage }, ''); } const handlePopState = (e: PopStateEvent) => { if (e.state?.page) { setActivePage(e.state.page); if (e.state.infoFeature) { setPendingInfoFeature(e.state.infoFeature); } } }; window.addEventListener('popstate', handlePopState); return () => window.removeEventListener('popstate', handlePopState); }, []); // eslint-disable-line react-hooks/exhaustive-deps if (isScreenshotMode) { return ( {}} onNavigateTo={() => {}} screenshotMode /> ); } return (
{activePage === 'home' ? ( navigateTo('dashboard')} theme={theme} /> ) : activePage === 'data-sources' ? ( ) : activePage === 'faq' ? ( ) : ( setPendingInfoFeature(null)} onNavigateTo={navigateTo} /> )}
); }