perfect-postcode/frontend/src/App.tsx

169 lines
5.4 KiB
TypeScript

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<FeatureMeta[]>([]);
const [poiCategoryGroups, setPOICategoryGroups] = useState<POICategoryGroup[]>([]);
const [initialLoading, setInitialLoading] = useState(true);
// UI state
const [pendingInfoFeature, setPendingInfoFeature] = useState<string | null>(null);
const [activePage, setActivePage] = useState<Page>(() => {
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<POICategoriesResponse>(
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 (
<MapPage
features={features}
poiCategoryGroups={poiCategoryGroups}
initialFilters={urlState.filters || {}}
initialViewState={initialViewState}
initialPOICategories={urlState.poiCategories || new Set()}
initialTab={urlState.tab || 'pois'}
initialLoading={initialLoading}
theme={theme}
pendingInfoFeature={null}
onClearPendingInfoFeature={() => {}}
onNavigateTo={() => {}}
screenshotMode
/>
);
}
return (
<div className="h-screen flex flex-col">
<Header
activePage={activePage}
onPageChange={navigateTo}
theme={theme}
onToggleTheme={toggleTheme}
/>
{activePage === 'home' ? (
<HomePage onOpenDashboard={() => navigateTo('dashboard')} theme={theme} />
) : activePage === 'data-sources' ? (
<DataSourcesPage />
) : activePage === 'faq' ? (
<FAQPage />
) : (
<MapPage
features={features}
poiCategoryGroups={poiCategoryGroups}
initialFilters={urlState.filters || {}}
initialViewState={initialViewState}
initialPOICategories={urlState.poiCategories || new Set()}
initialTab={urlState.tab || 'pois'}
initialLoading={initialLoading}
theme={theme}
pendingInfoFeature={pendingInfoFeature}
onClearPendingInfoFeature={() => setPendingInfoFeature(null)}
onNavigateTo={navigateTo}
/>
)}
</div>
);
}