import { useState, useEffect, useCallback, useMemo } from 'react'; import MapPage, { type ExportState } from './components/map/MapPage'; import PricingPage from './components/pricing/PricingPage'; import HomePage from './components/home/HomePage'; import SavedSearchesPage from './components/saved-searches/SavedSearchesPage'; import LearnPage from './components/learn/LearnPage'; import AccountPage from './components/account/AccountPage'; import Header, { type Page } from './components/ui/Header'; import AuthModal from './components/ui/AuthModal'; import SaveSearchModal from './components/ui/SaveSearchModal'; 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'; import { useIsMobile } from './hooks/useIsMobile'; import { useAuth } from './hooks/useAuth'; import { useSavedSearches } from './hooks/useSavedSearches'; declare global { interface Window { __screenshot_ready?: boolean; } } function pageToPath(page: Page): string { switch (page) { case 'dashboard': return '/dashboard'; case 'saved-searches': return '/saved'; case 'learn': return '/learn'; case 'pricing': return '/pricing'; case 'account': return '/account'; default: return '/'; } } function pathToPage(pathname: string): Page | null { if (pathname === '/dashboard') return 'dashboard'; if (pathname === '/saved') return 'saved-searches'; if (pathname === '/learn') return 'learn'; if (pathname === '/pricing') return 'pricing'; if (pathname === '/account') return 'account'; if (pathname === '/') return 'home'; return null; } export default function App() { const urlState = useMemo(() => parseUrlState(), []); const initialViewState = useMemo( () => urlState.viewState || INITIAL_VIEW_STATE, [urlState.viewState] ); const isScreenshotMode = useMemo(() => { const params = new URLSearchParams(window.location.search); return params.get('screenshot') === '1'; }, []); const isOgMode = useMemo(() => { const params = new URLSearchParams(window.location.search); return params.get('og') === '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'; // Derive page from URL pathname const fromPath = pathToPage(window.location.pathname); if (fromPath) return fromPath; // Restore from history state (e.g. popstate) if (window.history.state?.page) return window.history.state.page; return 'home'; }); const { theme, toggleTheme } = useTheme(); const isMobile = useIsMobile(); const { user, loading: authLoading, error: authError, login, register, logout, requestPasswordReset, refreshAuth, clearError, } = useAuth(); const [showAuthModal, setShowAuthModal] = useState(false); const [authModalTab, setAuthModalTab] = useState<'login' | 'register'>('login'); const savedSearches = useSavedSearches(user?.id ?? null); const [showSaveModal, setShowSaveModal] = useState(false); // 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 — MapPage sets __screenshot_ready once map data loads // Navigation const navigateTo = useCallback((page: Page, hash?: string, infoFeature?: string) => { if (infoFeature) { window.history.replaceState({ ...window.history.state, infoFeature }, ''); } const path = pageToPath(page); const url = hash ? `${path}#${hash}` : path; window.history.pushState({ page }, '', url); setActivePage(page); }, []); useEffect(() => { if (!window.history.state?.page) { window.history.replaceState( { page: activePage }, '', pageToPath(activePage) + window.location.search + window.location.hash ); } const handlePopState = (e: PopStateEvent) => { if (e.state?.page) { setActivePage(e.state.page); if (e.state.infoFeature) { setPendingInfoFeature(e.state.infoFeature); } } else { // Fall back to deriving page from pathname const page = pathToPage(window.location.pathname); setActivePage(page || 'home'); } }; window.addEventListener('popstate', handlePopState); return () => window.removeEventListener('popstate', handlePopState); }, []); // eslint-disable-line react-hooks/exhaustive-deps // Fetch saved searches when page becomes active const { fetchSearches } = savedSearches; useEffect(() => { if (activePage === 'saved-searches') { fetchSearches(); } }, [activePage, fetchSearches]); const [exportState, setExportState] = useState(null); if (isScreenshotMode) { return ( {}} onNavigateTo={() => {}} screenshotMode ogMode={isOgMode} initialTravelTime={urlState.travelTime} /> ); } return (
setShowSaveModal(true) : null} savingSearch={savedSearches.saving} user={user} onLoginClick={() => { setAuthModalTab('login'); setShowAuthModal(true); }} onRegisterClick={() => { setAuthModalTab('register'); setShowAuthModal(true); }} onLogout={logout} isMobile={isMobile} /> {activePage === 'home' ? ( navigateTo('dashboard')} onOpenPricing={() => navigateTo('pricing')} theme={theme} features={features} /> ) : activePage === 'pricing' ? ( navigateTo('dashboard')} /> ) : activePage === 'learn' ? ( ) : activePage === 'account' && user ? ( ) : activePage === 'saved-searches' ? ( { window.location.href = `/?${params}`; }} /> ) : ( setPendingInfoFeature(null)} onNavigateTo={navigateTo} onExportStateChange={setExportState} isMobile={isMobile} initialTravelTime={urlState.travelTime} /> )} {showAuthModal && ( setShowAuthModal(false)} onLogin={login} onRegister={register} onForgotPassword={requestPasswordReset} loading={authLoading} error={authError} onClearError={clearError} initialTab={authModalTab} /> )} {showSaveModal && ( setShowSaveModal(false)} onSave={savedSearches.saveSearch} saving={savedSearches.saving} error={savedSearches.error} /> )}
); }