aboutsummaryrefslogtreecommitdiff
path: root/src/frontend/app/routes
diff options
context:
space:
mode:
authorAriel Costas Guerrero <ariel@costas.dev>2026-04-02 12:38:10 +0200
committerAriel Costas Guerrero <ariel@costas.dev>2026-04-02 12:45:33 +0200
commit1b4f4a674ac533c0b51260ba35ab91dd2cf9486d (patch)
tree9fdaf418bef86c51737bcf203483089c9e2b908b /src/frontend/app/routes
parent749e04d6fc2304bb29920db297d1fa4d73b57648 (diff)
Basic push notification system for service alerts
Co-authored-by: Copilot <copilot@github.com>
Diffstat (limited to 'src/frontend/app/routes')
-rw-r--r--src/frontend/app/routes/favourites.tsx5
-rw-r--r--src/frontend/app/routes/routes.tsx35
-rw-r--r--src/frontend/app/routes/settings.tsx8
3 files changed, 38 insertions, 10 deletions
diff --git a/src/frontend/app/routes/favourites.tsx b/src/frontend/app/routes/favourites.tsx
index 3d786b6..1b1d09b 100644
--- a/src/frontend/app/routes/favourites.tsx
+++ b/src/frontend/app/routes/favourites.tsx
@@ -99,7 +99,10 @@ export default function Favourites() {
return routes.reduce(
(acc, route) => {
const agency = route.agencyName || t("routes.unknown_agency", "Otros");
- if (!isFavoriteAgency(agency)) {
+ // Match by the agency's own gtfsId (feedId:agencyId) — consistent with
+ // what routes.tsx stores and with the alert selector format.
+ const agencyId = route.agencyId ?? route.id.split(":")[0];
+ if (!isFavoriteAgency(agencyId)) {
return acc;
}
diff --git a/src/frontend/app/routes/routes.tsx b/src/frontend/app/routes/routes.tsx
index 57dfe00..f65adaa 100644
--- a/src/frontend/app/routes/routes.tsx
+++ b/src/frontend/app/routes/routes.tsx
@@ -63,16 +63,30 @@ export default function RoutesPage() {
const sortedAgencyEntries = useMemo(() => {
if (!routesByAgency) return [];
- return Object.entries(routesByAgency).sort(([a], [b]) => {
+ return Object.entries(routesByAgency).sort(([a, routesA], [b, routesB]) => {
+ // Use the agency's own gtfsId (feedId:agencyId) as the stable key — this
+ // matches the "agency#feedId:agencyId" alert selector format and correctly
+ // handles feeds that contain multiple agencies.
+ const agencyIdA =
+ routesA?.[0]?.agencyId ??
+ routesA?.[0]?.id.split(":")[0] ??
+ a.toLowerCase();
+ const agencyIdB =
+ routesB?.[0]?.agencyId ??
+ routesB?.[0]?.id.split(":")[0] ??
+ b.toLowerCase();
+ const feedIdA = agencyIdA.split(":")[0];
+ const feedIdB = agencyIdB.split(":")[0];
+
// First, sort by favorite status
- const isFavA = isFavoriteAgency(a);
- const isFavB = isFavoriteAgency(b);
+ const isFavA = isFavoriteAgency(agencyIdA);
+ const isFavB = isFavoriteAgency(agencyIdB);
if (isFavA && !isFavB) return -1;
if (!isFavA && isFavB) return 1;
// Then by fixed order
- const indexA = orderedAgencies.indexOf(a.toLowerCase());
- const indexB = orderedAgencies.indexOf(b.toLowerCase());
+ const indexA = orderedAgencies.indexOf(feedIdA);
+ const indexB = orderedAgencies.indexOf(feedIdB);
if (indexA === -1 && indexB === -1) {
return a.localeCompare(b);
}
@@ -156,10 +170,15 @@ export default function RoutesPage() {
)}
{sortedAgencyEntries.map(([agency, agencyRoutes]) => {
- const isFav = isFavoriteAgency(agency);
+ // Use the agency's own gtfsId (feedId:agencyId) as the stable favourite key.
+ const agencyId =
+ agencyRoutes?.[0]?.agencyId ??
+ agencyRoutes?.[0]?.id.split(":")[0] ??
+ agency.toLowerCase();
+ const isFav = isFavoriteAgency(agencyId);
const isExpanded = searchQuery
? true
- : (expandedAgencies[agency] ?? false);
+ : (expandedAgencies[agency] ?? isFav);
return (
<div
@@ -190,7 +209,7 @@ export default function RoutesPage() {
</button>
<button
type="button"
- onClick={() => toggleFavoriteAgency(agency)}
+ onClick={() => toggleFavoriteAgency(agencyId)}
className={`rounded-full p-2 transition-colors ${
isFav
? "text-yellow-500"
diff --git a/src/frontend/app/routes/settings.tsx b/src/frontend/app/routes/settings.tsx
index 0497f34..a716030 100644
--- a/src/frontend/app/routes/settings.tsx
+++ b/src/frontend/app/routes/settings.tsx
@@ -2,6 +2,7 @@ import { Computer, Moon, Sun, Trash2 } from "lucide-react";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { Link } from "react-router";
+import { PushNotificationSettings } from "~/components/PushNotificationSettings";
import { usePageTitle } from "~/contexts/PageTitleContext";
import { useApp, type Theme } from "../AppContext";
import "../tailwind-full.css";
@@ -178,7 +179,7 @@ export default function Settings() {
className="block text-lg font-medium text-text mb-3"
>
{t("about.language", "Idioma")}
- </label>
+ </label>{" "}
<select
id="language"
className="
@@ -197,6 +198,11 @@ export default function Settings() {
</select>
</section>
+ {/* Push Notifications */}
+ <div className="mt-8 pt-8 border-t border-border">
+ <PushNotificationSettings />
+ </div>
+
{/* Privacy / Clear data */}
<section className="mt-8 pt-8 border-t border-border">
<h2 className="text-xl font-semibold mb-4 text-text">