From 2da9964e49e64c02767342d2de675b776e8e6cda Mon Sep 17 00:00:00 2001 From: Ariel Costas Guerrero Date: Mon, 13 Oct 2025 00:14:56 +0200 Subject: Use openfreemapinstead of self-hosting, improve stop display, improve dark mode --- src/frontend/app/AppContext.tsx | 70 ++++++++++++++++++++++++++++---- src/frontend/app/i18n/locales/en-GB.json | 5 ++- src/frontend/app/i18n/locales/es-ES.json | 5 ++- src/frontend/app/i18n/locales/gl-ES.json | 5 ++- src/frontend/app/maps/styleloader.ts | 15 ++++++- src/frontend/app/root.css | 3 ++ src/frontend/app/routes/map.tsx | 26 +++++++++++- src/frontend/app/routes/settings.css | 1 - src/frontend/app/routes/settings.tsx | 8 ++-- 9 files changed, 114 insertions(+), 24 deletions(-) (limited to 'src/frontend/app') diff --git a/src/frontend/app/AppContext.tsx b/src/frontend/app/AppContext.tsx index e6d8971..9013463 100644 --- a/src/frontend/app/AppContext.tsx +++ b/src/frontend/app/AppContext.tsx @@ -8,7 +8,7 @@ import { } from "react"; import { type LngLatLike } from "maplibre-gl"; -type Theme = "light" | "dark"; +export type Theme = "light" | "dark" | "system"; type TableStyle = "regular" | "grouped"; type MapPositionMode = "gps" | "last"; @@ -47,23 +47,75 @@ const AppContext = createContext(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(() => { const savedTheme = localStorage.getItem("theme"); - if (savedTheme) { - return savedTheme as Theme; + if (savedTheme === "light" || savedTheme === "dark" || savedTheme === "system") { + return savedTheme; } - const prefersDark = - window.matchMedia && - window.matchMedia("(prefers-color-scheme: dark)").matches; - return prefersDark ? "dark" : "light"; + 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); + } + }; + }, []); + + const resolvedTheme = theme === "system" ? systemTheme : theme; + const toggleTheme = () => { - setTheme((prevTheme) => (prevTheme === "light" ? "dark" : "light")); + setTheme((prevTheme) => { + if (prevTheme === "light") { + return "dark"; + } + if (prevTheme === "dark") { + return "system"; + } + return "light"; + }); }; useEffect(() => { - document.documentElement.setAttribute("data-theme", theme); + document.documentElement.setAttribute("data-theme", resolvedTheme); + document.documentElement.style.colorScheme = resolvedTheme; + }, [resolvedTheme]); + + useEffect(() => { localStorage.setItem("theme", theme); }, [theme]); //#endregion diff --git a/src/frontend/app/i18n/locales/en-GB.json b/src/frontend/app/i18n/locales/en-GB.json index ee8ec7a..4d0c91b 100644 --- a/src/frontend/app/i18n/locales/en-GB.json +++ b/src/frontend/app/i18n/locales/en-GB.json @@ -9,8 +9,9 @@ "data_source_middle": "under license", "settings": "Settings", "theme": "Mode:", - "theme_light": "Light", - "theme_dark": "Dark", + "theme_light": "Light", + "theme_dark": "Dark", + "theme_system": "System", "table_style": "Table style:", "table_style_regular": "Show in order", "table_style_grouped": "Group by line", diff --git a/src/frontend/app/i18n/locales/es-ES.json b/src/frontend/app/i18n/locales/es-ES.json index a152564..0915361 100644 --- a/src/frontend/app/i18n/locales/es-ES.json +++ b/src/frontend/app/i18n/locales/es-ES.json @@ -9,8 +9,9 @@ "data_source_middle": "bajo licencia", "settings": "Ajustes", "theme": "Modo:", - "theme_light": "Claro", - "theme_dark": "Oscuro", + "theme_light": "Claro", + "theme_dark": "Oscuro", + "theme_system": "Sistema", "table_style": "Estilo de tabla:", "table_style_regular": "Mostrar por orden", "table_style_grouped": "Agrupar por línea", diff --git a/src/frontend/app/i18n/locales/gl-ES.json b/src/frontend/app/i18n/locales/gl-ES.json index b683fc0..8949333 100644 --- a/src/frontend/app/i18n/locales/gl-ES.json +++ b/src/frontend/app/i18n/locales/gl-ES.json @@ -9,8 +9,9 @@ "data_source_middle": "baixo licenza", "settings": "Axustes", "theme": "Modo:", - "theme_light": "Claro", - "theme_dark": "Escuro", + "theme_light": "Claro", + "theme_dark": "Escuro", + "theme_system": "Sistema", "table_style": "Estilo de táboa:", "table_style_regular": "Mostrar por orde", "table_style_grouped": "Agrupar por liña", diff --git a/src/frontend/app/maps/styleloader.ts b/src/frontend/app/maps/styleloader.ts index cf285a5..08086f1 100644 --- a/src/frontend/app/maps/styleloader.ts +++ b/src/frontend/app/maps/styleloader.ts @@ -1,9 +1,22 @@ import type { StyleSpecification } from "react-map-gl/maplibre"; +import type { Theme } from "~/AppContext"; export async function loadStyle( styleName: string, - colorScheme: string, + colorScheme: Theme, ): Promise { + if (styleName == "openfreemap") { + const url = "/maps/styles/openfreemap-any.json"; + + const resp = await fetch(url); + if (!resp.ok) { + throw new Error(`Failed to load style: ${url}`); + } + + const style = await resp.json(); + return style as StyleSpecification; + } + const stylePath = `/maps/styles/${styleName}-${colorScheme}.json`; const resp = await fetch(stylePath); diff --git a/src/frontend/app/root.css b/src/frontend/app/root.css index b77f44d..202e6f1 100644 --- a/src/frontend/app/root.css +++ b/src/frontend/app/root.css @@ -10,6 +10,7 @@ --star-color: #ffcc00; --message-background-color: #f8f9fa; + color-scheme: light; font-family: "Roboto Variable", Roboto, Arial, sans-serif; } @@ -24,6 +25,8 @@ --button-disabled-background-color: #555555; --star-color: #ffcc00; --message-background-color: #333333; + + color-scheme: dark; } body { diff --git a/src/frontend/app/routes/map.tsx b/src/frontend/app/routes/map.tsx index 5887b9c..56a9c79 100644 --- a/src/frontend/app/routes/map.tsx +++ b/src/frontend/app/routes/map.tsx @@ -73,7 +73,8 @@ export default function StopMap() { }, []); useEffect(() => { - const styleName = "carto"; + //const styleName = "carto"; + const styleName = "openfreemap"; loadStyle(styleName, theme) .then((style) => setMapStyle(style)) .catch((error) => console.error("Failed to load map style:", error)); @@ -158,12 +159,33 @@ export default function StopMap() { source="stops-source" layout={{ "icon-image": "stop", - "icon-size": ["interpolate", ["linear"], ["zoom"], 11, 0.4, 16, 0.8], + "icon-size": ["interpolate", ["linear"], ["zoom"], 11, 0.4, 18, 0.8], "icon-allow-overlap": true, "icon-ignore-placement": true, }} /> + + + {selectedStop && ( (null); - return (

{t("about.title")}

@@ -31,10 +28,11 @@ export default function Settings() { id="theme" className="form-select-inline" value={theme} - onChange={(e) => setTheme(e.target.value as "light" | "dark")} + onChange={(e) => setTheme(e.target.value as Theme)} > +
-- cgit v1.3