aboutsummaryrefslogtreecommitdiff
path: root/src/frontend/app/routes
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/routes
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/routes')
-rw-r--r--src/frontend/app/routes/about.css7
-rw-r--r--src/frontend/app/routes/about.tsx61
-rw-r--r--src/frontend/app/routes/estimates-$id.tsx2
-rw-r--r--src/frontend/app/routes/favourites.tsx13
-rw-r--r--src/frontend/app/routes/home.tsx18
-rw-r--r--src/frontend/app/routes/map.tsx26
-rw-r--r--src/frontend/app/routes/settings.tsx51
-rw-r--r--src/frontend/app/routes/stops-$id.css29
-rw-r--r--src/frontend/app/routes/stops-$id.tsx30
-rw-r--r--src/frontend/app/routes/timetable-$id.tsx26
10 files changed, 137 insertions, 126 deletions
diff --git a/src/frontend/app/routes/about.css b/src/frontend/app/routes/about.css
new file mode 100644
index 0000000..8f13015
--- /dev/null
+++ b/src/frontend/app/routes/about.css
@@ -0,0 +1,7 @@
+.about-version {
+ margin-top: 2rem;
+ text-align: center;
+ color: var(--subtitle-color);
+ border-top: 1px solid var(--border-color);
+ padding-top: 1rem;
+}
diff --git a/src/frontend/app/routes/about.tsx b/src/frontend/app/routes/about.tsx
new file mode 100644
index 0000000..d41268d
--- /dev/null
+++ b/src/frontend/app/routes/about.tsx
@@ -0,0 +1,61 @@
+import { useTranslation } from "react-i18next";
+import { usePageTitle } from "~/contexts/PageTitleContext";
+import { useApp } from "../AppContext";
+import "./about.css";
+import "./settings.css"; // Reusing settings CSS for now
+
+export default function About() {
+ const { t } = useTranslation();
+ usePageTitle(t("about.title", "Acerca de"));
+ const { region } = useApp();
+
+ return (
+ <div className="page-container">
+ <p className="about-description">{t("about.description")}</p>
+
+ <h2>{t("about.credits")}</h2>
+ <p>
+ <a
+ href="https://github.com/arielcostas/busurbano"
+ className="about-link"
+ rel="nofollow noreferrer noopener"
+ >
+ {t("about.github")}
+ </a>{" "}
+ - {t("about.developed_by")}{" "}
+ <a
+ href="https://www.costas.dev"
+ className="about-link"
+ rel="nofollow noreferrer noopener"
+ >
+ Ariel Costas
+ </a>
+ </p>
+ {region === "vigo" && (
+ <p>
+ {t("about.data_source_prefix")}{" "}
+ <a
+ href="https://datos.vigo.org"
+ className="about-link"
+ rel="nofollow noreferrer noopener"
+ >
+ datos.vigo.org
+ </a>{" "}
+ {t("about.data_source_middle")}{" "}
+ <a
+ href="https://opendefinition.org/licenses/odc-by/"
+ className="about-link"
+ rel="nofollow noreferrer noopener"
+ >
+ Open Data Commons Attribution License
+ </a>
+ .
+ </p>
+ )}
+
+ <div className="about-version">
+ <small>Version: {__COMMIT_HASH__}</small>
+ </div>
+ </div>
+ );
+}
diff --git a/src/frontend/app/routes/estimates-$id.tsx b/src/frontend/app/routes/estimates-$id.tsx
index 74f24e6..b92e59d 100644
--- a/src/frontend/app/routes/estimates-$id.tsx
+++ b/src/frontend/app/routes/estimates-$id.tsx
@@ -15,7 +15,7 @@ import {
} from "~/components/SchedulesTableSkeleton";
import { StopAlert } from "~/components/StopAlert";
import { TimetableSkeleton } from "~/components/TimetableSkeleton";
-import { type RegionId, getRegionConfig } from "~/data/RegionConfig";
+import { type RegionId, getRegionConfig } from "~/config/RegionConfig";
import { useAutoRefresh } from "~/hooks/useAutoRefresh";
import { useApp } from "../AppContext";
import { GroupedTable } from "../components/GroupedTable";
diff --git a/src/frontend/app/routes/favourites.tsx b/src/frontend/app/routes/favourites.tsx
new file mode 100644
index 0000000..5b74391
--- /dev/null
+++ b/src/frontend/app/routes/favourites.tsx
@@ -0,0 +1,13 @@
+import { useTranslation } from "react-i18next";
+import { usePageTitle } from "~/contexts/PageTitleContext";
+
+export default function Favourites() {
+ const { t } = useTranslation();
+ usePageTitle(t("navbar.favourites", "Favoritos"));
+
+ return (
+ <div className="page-container">
+ <p>{t("favourites.empty", "No tienes paradas favoritas.")}</p>
+ </div>
+ );
+}
diff --git a/src/frontend/app/routes/home.tsx b/src/frontend/app/routes/home.tsx
index 2909999..8a1e3b3 100644
--- a/src/frontend/app/routes/home.tsx
+++ b/src/frontend/app/routes/home.tsx
@@ -1,18 +1,20 @@
"use client";
-import { useEffect, useMemo, useRef, useState, useCallback } from "react";
-import StopDataProvider, { type Stop } from "../data/StopDataProvider";
-import StopItem from "../components/StopItem";
-import StopItemSkeleton from "../components/StopItemSkeleton";
-import StopGallery from "../components/StopGallery";
-import ServiceAlerts from "../components/ServiceAlerts";
import Fuse from "fuse.js";
-import "./home.css";
+import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
+import { REGIONS } from "~/config/RegionConfig";
+import { usePageTitle } from "~/contexts/PageTitleContext";
import { useApp } from "../AppContext";
-import { REGIONS } from "~/data/RegionConfig";
+import ServiceAlerts from "../components/ServiceAlerts";
+import StopGallery from "../components/StopGallery";
+import StopItem from "../components/StopItem";
+import StopItemSkeleton from "../components/StopItemSkeleton";
+import StopDataProvider, { type Stop } from "../data/StopDataProvider";
+import "./home.css";
export default function StopList() {
const { t } = useTranslation();
+ usePageTitle(t("navbar.stops", "Paradas"));
const { region } = useApp();
const [data, setData] = useState<Stop[] | null>(null);
const [loading, setLoading] = useState(true);
diff --git a/src/frontend/app/routes/map.tsx b/src/frontend/app/routes/map.tsx
index b8fb881..57fb04e 100644
--- a/src/frontend/app/routes/map.tsx
+++ b/src/frontend/app/routes/map.tsx
@@ -6,17 +6,18 @@ import type { Feature as GeoJsonFeature, Point } from "geojson";
import { useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import Map, {
- GeolocateControl,
- Layer,
- NavigationControl,
- Source,
- type MapLayerMouseEvent,
- type MapRef,
- type StyleSpecification
+ GeolocateControl,
+ Layer,
+ NavigationControl,
+ Source,
+ type MapLayerMouseEvent,
+ type MapRef,
+ type StyleSpecification
} from "react-map-gl/maplibre";
-import { useApp } from "~/AppContext";
import { StopSheet } from "~/components/StopSheet";
-import { REGIONS } from "~/data/RegionConfig";
+import { getRegionConfig } from "~/config/RegionConfig";
+import { usePageTitle } from "~/contexts/PageTitleContext";
+import { useApp } from "../AppContext";
// Default minimal fallback style before dynamic loading
const defaultStyle: StyleSpecification = {
@@ -30,6 +31,7 @@ const defaultStyle: StyleSpecification = {
// Componente principal del mapa
export default function StopMap() {
const { t } = useTranslation();
+ usePageTitle(t("navbar.map", "Mapa"));
const [stops, setStops] = useState<
GeoJsonFeature<
Point,
@@ -162,8 +164,8 @@ export default function StopMap() {
}}
attributionControl={{ compact: false }}
maxBounds={
- REGIONS[region].bounds
- ? [REGIONS[region].bounds!.sw, REGIONS[region].bounds!.ne]
+ getRegionConfig(region).bounds
+ ? [getRegionConfig(region).bounds!.sw, getRegionConfig(region).bounds!.ne]
: undefined
}
>
@@ -218,7 +220,7 @@ export default function StopMap() {
"text-size": ["interpolate", ["linear"], ["zoom"], 11, 8, 22, 16],
}}
paint={{
- "text-color": `${REGIONS[region].textColour || "#000"}`,
+ "text-color": `${getRegionConfig(region).textColour || "#000"}`,
"text-halo-color": "#FFF",
"text-halo-width": 1,
}}
diff --git a/src/frontend/app/routes/settings.tsx b/src/frontend/app/routes/settings.tsx
index 2134b4c..351ccf0 100644
--- a/src/frontend/app/routes/settings.tsx
+++ b/src/frontend/app/routes/settings.tsx
@@ -1,12 +1,14 @@
-import { type Theme, useApp } from "../AppContext";
-import "./settings.css";
-import { useTranslation } from "react-i18next";
import { useState } from "react";
-import { getAvailableRegions, REGIONS } from "../data/RegionConfig";
+import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router";
+import { usePageTitle } from "~/contexts/PageTitleContext";
+import { type Theme, useApp } from "../AppContext";
+import { getAvailableRegions } from "../config/RegionConfig";
+import "./settings.css";
export default function Settings() {
const { t, i18n } = useTranslation();
+ usePageTitle(t("navbar.settings", "Ajustes"));
const navigate = useNavigate();
const {
theme,
@@ -46,8 +48,6 @@ export default function Settings() {
return (
<div className="page-container">
- <h1 className="page-title">{t("about.title")}</h1>
- <p className="about-description">{t("about.description")}</p>
<section className="settings-section">
<h2>{t("about.settings")}</h2>
<div className="settings-content-inline">
@@ -151,45 +151,6 @@ export default function Settings() {
</dl>
</details>
</section>
- <h2>{t("about.credits")}</h2>
- <p>
- <a
- href="https://github.com/arielcostas/busurbano"
- className="about-link"
- rel="nofollow noreferrer noopener"
- >
- {t("about.github")}
- </a>{" "}
- - {t("about.developed_by")}{" "}
- <a
- href="https://www.costas.dev"
- className="about-link"
- rel="nofollow noreferrer noopener"
- >
- Ariel Costas
- </a>
- </p>
- {region === "vigo" && (
- <p>
- {t("about.data_source_prefix")}{" "}
- <a
- href="https://datos.vigo.org"
- className="about-link"
- rel="nofollow noreferrer noopener"
- >
- datos.vigo.org
- </a>{" "}
- {t("about.data_source_middle")}{" "}
- <a
- href="https://opendefinition.org/licenses/odc-by/"
- className="about-link"
- rel="nofollow noreferrer noopener"
- >
- Open Data Commons Attribution License
- </a>
- .
- </p>
- )}
{showModal && (
<div className="modal-overlay" onClick={cancelRegionChange}>
diff --git a/src/frontend/app/routes/stops-$id.css b/src/frontend/app/routes/stops-$id.css
index c515435..9ecac16 100644
--- a/src/frontend/app/routes/stops-$id.css
+++ b/src/frontend/app/routes/stops-$id.css
@@ -1,8 +1,3 @@
-.page-title {
- margin-block: 0;
- font-size: 1.5rem;
-}
-
.estimates-content-wrapper {
display: flex;
flex-direction: column;
@@ -266,30 +261,6 @@
flex-shrink: 0;
}
-.experimental-notice {
- background-color: #fff3cd;
- border: 1px solid #ffc107;
- border-radius: 8px;
- padding: 0.5rem 1rem;
- color: #856404;
- flex-shrink: 0;
-}
-
-.experimental-notice strong {
- display: block;
- color: #856404;
-}
-
-[data-theme="dark"] .experimental-notice {
- background-color: #3d3100;
- border-color: #ffc107;
- color: #ffd966;
-}
-
-[data-theme="dark"] .experimental-notice strong {
- color: #ffd966;
-}
-
.refresh-status {
display: flex;
align-items: center;
diff --git a/src/frontend/app/routes/stops-$id.tsx b/src/frontend/app/routes/stops-$id.tsx
index 372582b..f340009 100644
--- a/src/frontend/app/routes/stops-$id.tsx
+++ b/src/frontend/app/routes/stops-$id.tsx
@@ -8,7 +8,8 @@ import { StopAlert } from "~/components/StopAlert";
import { StopMap } from "~/components/StopMapSheet";
import { ConsolidatedCirculationList } from "~/components/Stops/ConsolidatedCirculationList";
import { ConsolidatedCirculationListSkeleton } from "~/components/Stops/ConsolidatedCirculationListSkeleton";
-import { type RegionId, getRegionConfig } from "~/data/RegionConfig";
+import { type RegionId, getRegionConfig } from "~/config/RegionConfig";
+import { usePageTitle } from "~/contexts/PageTitleContext";
import { useAutoRefresh } from "~/hooks/useAutoRefresh";
import { useApp } from "../AppContext";
import StopDataProvider, { type Stop } from "../data/StopDataProvider";
@@ -79,6 +80,16 @@ export default function Estimates() {
const { region } = useApp();
const regionConfig = getRegionConfig(region);
+ // Helper function to get the display name for the stop
+ const getStopDisplayName = useCallback(() => {
+ if (customName) return customName;
+ if (stopData?.name.intersect) return stopData.name.intersect;
+ if (stopData?.name.original) return stopData.name.original;
+ return `Parada ${stopIdNum}`;
+ }, [customName, stopData, stopIdNum]);
+
+ usePageTitle(getStopDisplayName());
+
const parseError = (error: any): ErrorInfo => {
if (!navigator.onLine) {
return { type: "network", message: "No internet connection" };
@@ -165,14 +176,6 @@ export default function Estimates() {
}
};
- // Helper function to get the display name for the stop
- const getStopDisplayName = () => {
- if (customName) return customName;
- if (stopData?.name.intersect) return stopData.name.intersect;
- if (stopData?.name.original) return stopData.name.original;
- return `Parada ${stopIdNum}`;
- };
-
const handleRename = () => {
const current = getStopDisplayName();
const input = window.prompt("Custom name for this stop:", current);
@@ -202,9 +205,6 @@ export default function Estimates() {
onClick={handleRename}
width={20} />
</div>
- <h1 className="page-title">
- {getStopDisplayName()}
- </h1>
<button
className="manual-refresh-button"
@@ -230,12 +230,6 @@ export default function Estimates() {
{stopData && <StopAlert stop={stopData} />}
- <div className="experimental-notice">
- <strong>
- {t("estimates.experimental_feature", "Experimental feature")}
- </strong>
- </div>
-
<div className="estimates-list-container">
{dataLoading ? (
<ConsolidatedCirculationListSkeleton />
diff --git a/src/frontend/app/routes/timetable-$id.tsx b/src/frontend/app/routes/timetable-$id.tsx
index 8a1cba7..c036cb3 100644
--- a/src/frontend/app/routes/timetable-$id.tsx
+++ b/src/frontend/app/routes/timetable-$id.tsx
@@ -1,21 +1,21 @@
-import { useEffect, useState, useRef } from "react";
-import { useParams, Link } from "react-router";
-import StopDataProvider from "../data/StopDataProvider";
import {
- ArrowLeft,
- Eye,
- EyeOff,
- ChevronUp,
- ChevronDown,
- Clock,
+ ArrowLeft,
+ ChevronDown,
+ ChevronUp,
+ Clock,
+ Eye,
+ EyeOff,
} from "lucide-react";
+import { useEffect, useRef, useState } from "react";
+import { useTranslation } from "react-i18next";
+import { Link, useParams } from "react-router";
+import { useApp } from "~/AppContext";
+import { ErrorDisplay } from "~/components/ErrorDisplay";
import { type ScheduledTable } from "~/components/SchedulesTable";
import { TimetableSkeleton } from "~/components/TimetableSkeleton";
-import { ErrorDisplay } from "~/components/ErrorDisplay";
+import { type RegionId, getRegionConfig } from "~/config/RegionConfig";
import LineIcon from "../components/LineIcon";
-import { useTranslation } from "react-i18next";
-import { type RegionId, getRegionConfig } from "~/data/RegionConfig";
-import { useApp } from "~/AppContext";
+import StopDataProvider from "../data/StopDataProvider";
import "./timetable-$id.css";
interface ErrorInfo {