aboutsummaryrefslogtreecommitdiff
path: root/src/frontend/app/AppContext.tsx
diff options
context:
space:
mode:
authorAriel Costas Guerrero <ariel@costas.dev>2025-11-19 15:04:55 +0100
committerAriel Costas Guerrero <ariel@costas.dev>2025-11-19 15:05:34 +0100
commitd51169f6411b68a226d76d2d39826904de484929 (patch)
tree4d8a403dfcc5b17671a92b8cc1e5d71d20ed9537 /src/frontend/app/AppContext.tsx
parentd434204860fc0409ad6343e815d0057b97ce3573 (diff)
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.
Diffstat (limited to 'src/frontend/app/AppContext.tsx')
-rw-r--r--src/frontend/app/AppContext.tsx318
1 files changed, 24 insertions, 294 deletions
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;
-};