diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index ce7963c..24fd6fe 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -23,6 +23,8 @@ import type { HexagonPropertiesResponse, } from './types'; +type Theme = 'light' | 'dark'; + const DEBOUNCE_MS = 150; const URL_DEBOUNCE_MS = 300; @@ -180,9 +182,13 @@ type Page = 'home' | 'dashboard' | 'data-sources'; function Header({ activePage, onPageChange, + theme, + onToggleTheme, }: { activePage: Page; onPageChange: (page: Page) => void; + theme: Theme; + onToggleTheme: () => void; }) { const [copied, setCopied] = useState(false); @@ -240,7 +246,23 @@ function Header({ - {activePage === 'dashboard' && ( +
+ + {activePage === 'dashboard' && ( - )} + )} +
); } @@ -335,6 +358,28 @@ export default function App() { const [rightPaneTab, setRightPaneTab] = useState<'pois' | 'properties'>(urlState.tab || 'pois'); const [activePage, setActivePage] = useState('home'); + // Theme state — defaults to system preference on first visit + const [theme, setTheme] = useState(() => { + const stored = localStorage.getItem('theme'); + if (stored === 'light' || stored === 'dark') return stored; + return 'light'; + }); + + // Sync dark class on and persist to localStorage + useEffect(() => { + const root = document.documentElement; + if (theme === 'dark') { + root.classList.add('dark'); + } else { + root.classList.remove('dark'); + } + localStorage.setItem('theme', theme); + }, [theme]); + + const toggleTheme = useCallback(() => { + setTheme((prev) => (prev === 'light' ? 'dark' : 'light')); + }, []); + // Derive enabled features from filter keys const enabledFeatures = useMemo(() => new Set(Object.keys(filters)), [filters]); @@ -704,9 +749,9 @@ export default function App() { return (
-
+
{activePage === 'home' ? ( - setActivePage('dashboard')} /> + setActivePage('dashboard')} theme={theme} /> ) : activePage === 'data-sources' ? ( ) : ( @@ -742,22 +787,23 @@ export default function App() { selectedHexagonId={selectedHexagon?.h3 || null} onHexagonClick={handleHexagonClick} initialViewState={initialViewState} + theme={theme} /> {loading && ( -
+
Loading...
)} setActivePage('data-sources')} />
-
+
{/* Tab headers */} -
+
diff --git a/frontend/src/components/DataSourcesPage.tsx b/frontend/src/components/DataSourcesPage.tsx index 67a533a..f86b18a 100644 --- a/frontend/src/components/DataSourcesPage.tsx +++ b/frontend/src/components/DataSourcesPage.tsx @@ -87,30 +87,30 @@ const DATA_SOURCES = [ export default function DataSourcesPage() { return ( -
+
-

Data Sources

-

+

Data Sources

+

This application combines {DATA_SOURCES.length} open datasets covering property prices, energy performance, transport, demographics, crime, environment, and more.

{DATA_SOURCES.map((source) => ( -
+
-

{source.name}

- +

{source.name}

+ {source.license}
-

Source: {source.origin}

-

{source.use}

+

Source: {source.origin}

+

{source.use}

{source.url} diff --git a/frontend/src/components/Filters.tsx b/frontend/src/components/Filters.tsx index 544f64c..203657d 100644 --- a/frontend/src/components/Filters.tsx +++ b/frontend/src/components/Filters.tsx @@ -66,22 +66,22 @@ function FilterDropdown({ return (
{open && ( -
+
{grouped.map((group) => (
-
+
{group.name}
{group.features.map((f) => (
{allValues.map((val) => ( -