diff options
Diffstat (limited to 'src/frontend/app/contexts/SettingsContext.tsx')
| -rw-r--r-- | src/frontend/app/contexts/SettingsContext.tsx | 198 |
1 files changed, 198 insertions, 0 deletions
diff --git a/src/frontend/app/contexts/SettingsContext.tsx b/src/frontend/app/contexts/SettingsContext.tsx new file mode 100644 index 0000000..ed20fcb --- /dev/null +++ b/src/frontend/app/contexts/SettingsContext.tsx @@ -0,0 +1,198 @@ +import { + createContext, + useContext, + useEffect, + useState, + type ReactNode, +} from "react"; +import { APP_CONFIG } from "../config/AppConfig"; +import { + DEFAULT_REGION, + isValidRegion, + type RegionId +} from "../config/RegionConfig"; + +export type Theme = "light" | "dark" | "system"; +export type TableStyle = "regular" | "grouped" | "experimental_consolidated"; +export type MapPositionMode = "gps" | "last"; + +interface SettingsContextProps { + theme: Theme; + setTheme: React.Dispatch<React.SetStateAction<Theme>>; + toggleTheme: () => void; + + tableStyle: TableStyle; + setTableStyle: React.Dispatch<React.SetStateAction<TableStyle>>; + toggleTableStyle: () => void; + + mapPositionMode: MapPositionMode; + setMapPositionMode: (mode: MapPositionMode) => void; + + region: RegionId; + setRegion: (region: RegionId) => void; +} + +const SettingsContext = createContext<SettingsContextProps | undefined>( + undefined +); + +export const SettingsProvider = ({ 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 APP_CONFIG.defaultTheme; + }); + + 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); + } + }; + }, []); + + const resolvedTheme = theme === "system" ? systemTheme : 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 APP_CONFIG.defaultTableStyle; + }); + + 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" || saved === "gps" + ? (saved as MapPositionMode) + : APP_CONFIG.defaultMapPositionMode; + } + ); + + 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); + }; + + useEffect(() => { + localStorage.setItem("region", region); + }, [region]); + //#endregion + + return ( + <SettingsContext.Provider + value={{ + theme, + setTheme, + toggleTheme, + tableStyle, + setTableStyle, + toggleTableStyle, + mapPositionMode, + setMapPositionMode, + region, + setRegion, + }} + > + {children} + </SettingsContext.Provider> + ); +}; + +export const useSettings = () => { + const context = useContext(SettingsContext); + if (!context) { + throw new Error("useSettings must be used within a SettingsProvider"); + } + return context; +}; |
