From d51169f6411b68a226d76d2d39826904de484929 Mon Sep 17 00:00:00 2001
From: Ariel Costas Guerrero <ariel@costas.dev>
Date: Wed, 19 Nov 2025 15:04:55 +0100
Subject: feat: Add About and Favourites pages, update routing and context
 management

- Added new routes for About and Favourites pages.
- Implemented About page with version information and credits.
- Created Favourites page with a placeholder message for empty favourites.
- Refactored RegionConfig import paths for consistency.
- Introduced PageTitleContext to manage page titles dynamically.
- Updated various components to utilize the new context for setting page titles.
- Enhanced AppShell layout with a responsive Drawer for navigation.
- Added CSS styles for new components and pages.
- Integrated commit hash display in the About page for version tracking.
---
 src/frontend/app/AppContext.tsx | 318 +++-------------------------------------
 1 file changed, 24 insertions(+), 294 deletions(-)

(limited to 'src/frontend/app/AppContext.tsx')

diff --git a/src/frontend/app/AppContext.tsx b/src/frontend/app/AppContext.tsx
index 9c2521f..59f2724 100644
--- a/src/frontend/app/AppContext.tsx
+++ b/src/frontend/app/AppContext.tsx
@@ -1,304 +1,34 @@
 /* eslint-disable react-refresh/only-export-components */
+import { type ReactNode } from "react";
+import { type RegionId } from "./config/RegionConfig";
+import { MapProvider, useMap } from "./contexts/MapContext";
 import {
-  createContext,
-  useContext,
-  useEffect,
-  useState,
-  type ReactNode,
-} from "react";
-import { type LngLatLike } from "maplibre-gl";
-import {
-  type RegionId,
-  DEFAULT_REGION,
-  getRegionConfig,
-  isValidRegion,
-  REGIONS,
-} from "./data/RegionConfig";
-
-export type Theme = "light" | "dark" | "system";
-type TableStyle = "regular" | "grouped" | "experimental_consolidated";
-type MapPositionMode = "gps" | "last";
-
-interface MapState {
-  center: LngLatLike;
-  zoom: number;
-  userLocation: LngLatLike | null;
-  hasLocationPermission: boolean;
-}
-
-interface AppContextProps {
-  theme: Theme;
-  setTheme: React.Dispatch<React.SetStateAction<Theme>>;
-  toggleTheme: () => void;
-
-  tableStyle: TableStyle;
-  setTableStyle: React.Dispatch<React.SetStateAction<TableStyle>>;
-  toggleTableStyle: () => void;
-
-  mapState: MapState;
-  setMapCenter: (center: LngLatLike) => void;
-  setMapZoom: (zoom: number) => void;
-  setUserLocation: (location: LngLatLike | null) => void;
-  setLocationPermission: (hasPermission: boolean) => void;
-  updateMapState: (center: LngLatLike, zoom: number) => void;
-
-  mapPositionMode: MapPositionMode;
-  setMapPositionMode: (mode: MapPositionMode) => void;
-
-  region: RegionId;
-  setRegion: (region: RegionId) => void;
-}
-
-const AppContext = createContext<AppContextProps | undefined>(undefined);
-
-export const AppProvider = ({ children }: { children: ReactNode }) => {
-  //#region Theme
-  const getPreferredScheme = () => {
-    if (typeof window === "undefined" || !window.matchMedia) {
-      return "light" as const;
-    }
-    return window.matchMedia("(prefers-color-scheme: dark)").matches
-      ? "dark"
-      : "light";
-  };
-
-  const [systemTheme, setSystemTheme] = useState<"light" | "dark">(
-    getPreferredScheme
-  );
-
-  const [theme, setTheme] = useState<Theme>(() => {
-    const savedTheme = localStorage.getItem("theme");
-    if (
-      savedTheme === "light" ||
-      savedTheme === "dark" ||
-      savedTheme === "system"
-    ) {
-      return savedTheme;
-    }
-    return "system";
-  });
-
-  useEffect(() => {
-    if (typeof window === "undefined" || !window.matchMedia) {
-      return;
-    }
-
-    const media = window.matchMedia("(prefers-color-scheme: dark)");
-    const handleChange = (event: MediaQueryListEvent) => {
-      setSystemTheme(event.matches ? "dark" : "light");
-    };
-
-    // Sync immediately in case theme changed before subscription
-    setSystemTheme(media.matches ? "dark" : "light");
-
-    if (media.addEventListener) {
-      media.addEventListener("change", handleChange);
-    } else {
-      media.addListener(handleChange);
-    }
-
-    return () => {
-      if (media.removeEventListener) {
-        media.removeEventListener("change", handleChange);
-      } else {
-        media.removeListener(handleChange);
-      }
-    };
-  }, []);
+    SettingsProvider,
+    useSettings,
+    type MapPositionMode,
+    type TableStyle,
+    type Theme,
+} from "./contexts/SettingsContext";
 
-  const resolvedTheme = theme === "system" ? systemTheme : theme;
+// Re-export types for compatibility
+export type { MapPositionMode, RegionId, TableStyle, Theme };
 
-  const toggleTheme = () => {
-    setTheme((prevTheme) => {
-      if (prevTheme === "light") {
-        return "dark";
-      }
-      if (prevTheme === "dark") {
-        return "system";
-      }
-      return "light";
-    });
-  };
-
-  useEffect(() => {
-    document.documentElement.setAttribute("data-theme", resolvedTheme);
-    document.documentElement.style.colorScheme = resolvedTheme;
-  }, [resolvedTheme]);
-
-  useEffect(() => {
-    localStorage.setItem("theme", theme);
-  }, [theme]);
-  //#endregion
-
-  //#region Table Style
-  const [tableStyle, setTableStyle] = useState<TableStyle>(() => {
-    const savedTableStyle = localStorage.getItem("tableStyle");
-    if (savedTableStyle) {
-      return savedTableStyle as TableStyle;
-    }
-    return "regular";
-  });
-
-  const toggleTableStyle = () => {
-    setTableStyle((prevTableStyle) =>
-      prevTableStyle === "regular" ? "grouped" : "regular"
-    );
-  };
-
-  useEffect(() => {
-    localStorage.setItem("tableStyle", tableStyle);
-  }, [tableStyle]);
-  //#endregion
-
-  //#region Map Position Mode
-  const [mapPositionMode, setMapPositionMode] = useState<MapPositionMode>(
-    () => {
-      const saved = localStorage.getItem("mapPositionMode");
-      return saved === "last" ? "last" : "gps";
-    }
-  );
-
-  useEffect(() => {
-    localStorage.setItem("mapPositionMode", mapPositionMode);
-  }, [mapPositionMode]);
-  //#endregion
-
-  //#region Region
-  const [region, setRegionState] = useState<RegionId>(() => {
-    const savedRegion = localStorage.getItem("region");
-    if (savedRegion && isValidRegion(savedRegion)) {
-      return savedRegion;
-    }
-    return DEFAULT_REGION;
-  });
-
-  const setRegion = (newRegion: RegionId) => {
-    setRegionState(newRegion);
-    localStorage.setItem("region", newRegion);
-
-    // Update map to region's default center and zoom
-    const regionConfig = getRegionConfig(newRegion);
-    updateMapState(regionConfig.defaultCenter, regionConfig.defaultZoom);
-  };
-
-  useEffect(() => {
-    localStorage.setItem("region", region);
-  }, [region]);
-  //#endregion
-
-  //#region Map State
-  const [mapState, setMapState] = useState<MapState>(() => {
-    const savedMapState = localStorage.getItem("mapState");
-    if (savedMapState) {
-      try {
-        const parsed = JSON.parse(savedMapState);
-        return {
-          center: parsed.center || REGIONS[region].defaultCenter,
-          zoom: parsed.zoom || REGIONS[region].defaultZoom,
-          userLocation: parsed.userLocation || null,
-          hasLocationPermission: parsed.hasLocationPermission || false,
-        };
-      } catch (e) {
-        console.error("Error parsing saved map state", e);
-      }
-    }
-    return {
-      center: REGIONS[region].defaultCenter,
-      zoom: REGIONS[region].defaultZoom,
-      userLocation: null,
-      hasLocationPermission: false,
-    };
-  });
-
-  const setMapCenter = (center: LngLatLike) => {
-    setMapState((prev) => {
-      const newState = { ...prev, center };
-      localStorage.setItem("mapState", JSON.stringify(newState));
-      return newState;
-    });
-  };
-
-  const setMapZoom = (zoom: number) => {
-    setMapState((prev) => {
-      const newState = { ...prev, zoom };
-      localStorage.setItem("mapState", JSON.stringify(newState));
-      return newState;
-    });
-  };
-
-  const setUserLocation = (userLocation: LngLatLike | null) => {
-    setMapState((prev) => {
-      const newState = { ...prev, userLocation };
-      localStorage.setItem("mapState", JSON.stringify(newState));
-      return newState;
-    });
-  };
-
-  const setLocationPermission = (hasLocationPermission: boolean) => {
-    setMapState((prev) => {
-      const newState = { ...prev, hasLocationPermission };
-      localStorage.setItem("mapState", JSON.stringify(newState));
-      return newState;
-    });
-  };
+// Combined hook for backward compatibility
+export const useApp = () => {
+  const settings = useSettings();
+  const map = useMap();
 
-  const updateMapState = (center: LngLatLike, zoom: number) => {
-    setMapState((prev) => {
-      const newState = { ...prev, center, zoom };
-      localStorage.setItem("mapState", JSON.stringify(newState));
-      return newState;
-    });
+  return {
+    ...settings,
+    ...map,
   };
-  //#endregion
-
-  // Tratar de obtener la ubicación del usuario cuando se carga la aplicación si ya se había concedido permiso antes
-  useEffect(() => {
-    if (mapState.hasLocationPermission && !mapState.userLocation) {
-      if (navigator.geolocation) {
-        navigator.geolocation.getCurrentPosition(
-          (position) => {
-            const { latitude, longitude } = position.coords;
-            setUserLocation([latitude, longitude]);
-          },
-          (error) => {
-            console.error("Error getting location:", error);
-            setLocationPermission(false);
-          }
-        );
-      }
-    }
-  }, [mapState.hasLocationPermission, mapState.userLocation]);
+};
 
+// Wrapper provider
+export const AppProvider = ({ children }: { children: ReactNode }) => {
   return (
-    <AppContext.Provider
-      value={{
-        theme,
-        setTheme,
-        toggleTheme,
-        tableStyle,
-        setTableStyle,
-        toggleTableStyle,
-        mapState,
-        setMapCenter,
-        setMapZoom,
-        setUserLocation,
-        setLocationPermission,
-        updateMapState,
-        mapPositionMode,
-        setMapPositionMode,
-        region,
-        setRegion,
-      }}
-    >
-      {children}
-    </AppContext.Provider>
+    <SettingsProvider>
+      <MapProvider>{children}</MapProvider>
+    </SettingsProvider>
   );
 };
-
-export const useApp = () => {
-  const context = useContext(AppContext);
-  if (!context) {
-    throw new Error("useApp must be used within a AppProvider");
-  }
-  return context;
-};
-- 
cgit v1.3

