aboutsummaryrefslogtreecommitdiff
path: root/src/frontend/app
diff options
context:
space:
mode:
authorAriel Costas Guerrero <ariel@costas.dev>2025-10-13 00:14:56 +0200
committerAriel Costas Guerrero <ariel@costas.dev>2025-10-13 00:14:56 +0200
commit2da9964e49e64c02767342d2de675b776e8e6cda (patch)
tree71bdae38ccf86afb01a673c9a9f3c90421b5b64c /src/frontend/app
parent497a2893465bf0cd84cf6d3cc9023daba336f253 (diff)
Use openfreemapinstead of self-hosting, improve stop display, improve dark mode
Diffstat (limited to 'src/frontend/app')
-rw-r--r--src/frontend/app/AppContext.tsx70
-rw-r--r--src/frontend/app/i18n/locales/en-GB.json5
-rw-r--r--src/frontend/app/i18n/locales/es-ES.json5
-rw-r--r--src/frontend/app/i18n/locales/gl-ES.json5
-rw-r--r--src/frontend/app/maps/styleloader.ts15
-rw-r--r--src/frontend/app/root.css3
-rw-r--r--src/frontend/app/routes/map.tsx26
-rw-r--r--src/frontend/app/routes/settings.css1
-rw-r--r--src/frontend/app/routes/settings.tsx8
9 files changed, 114 insertions, 24 deletions
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<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) {
- 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<StyleSpecification> {
+ 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,
}}
/>
+ <Layer
+ id="stops-label"
+ type="symbol"
+ source="stops-source"
+ minzoom={16}
+ layout={{
+ "text-field": ["get", "name"],
+ "text-font": ["Noto Sans Regular"],
+ "text-offset": [0, 2.5],
+ "text-anchor": "center",
+ "text-justify": "center",
+ "text-size": ["interpolate", ["linear"], ["zoom"], 11, 8, 22, 14]
+ }}
+ paint={{
+ "text-color": "#45a15a",
+ "text-halo-color": "#fff",
+ "text-halo-width": 1.5
+ }}
+ />
+
+
{selectedStop && (
<StopSheet
isOpen={isSheetOpen}
diff --git a/src/frontend/app/routes/settings.css b/src/frontend/app/routes/settings.css
index 47de391..ef9fbd5 100644
--- a/src/frontend/app/routes/settings.css
+++ b/src/frontend/app/routes/settings.css
@@ -51,7 +51,6 @@
margin-left: 0.5em;
padding: 0.5rem;
font-size: 1rem;
- border: 1px solid var(--border-color);
border-radius: 8px;
}
diff --git a/src/frontend/app/routes/settings.tsx b/src/frontend/app/routes/settings.tsx
index 3bc3492..bcda311 100644
--- a/src/frontend/app/routes/settings.tsx
+++ b/src/frontend/app/routes/settings.tsx
@@ -1,4 +1,4 @@
-import { useApp } from "../AppContext";
+import { type Theme, useApp } from "../AppContext";
import "./settings.css";
import { useTranslation } from "react-i18next";
import { useState } from "react";
@@ -14,9 +14,6 @@ export default function Settings() {
setMapPositionMode,
} = useApp();
- const [isCheckingUpdates, setIsCheckingUpdates] = useState(false);
- const [updateMessage, setUpdateMessage] = useState<string | null>(null);
-
return (
<div className="page-container">
<h1 className="page-title">{t("about.title")}</h1>
@@ -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)}
>
<option value="light">{t("about.theme_light")}</option>
<option value="dark">{t("about.theme_dark")}</option>
+ <option value="system">{t("about.theme_system")}</option>
</select>
</div>
<div className="settings-content-inline">