aboutsummaryrefslogtreecommitdiff
path: root/src/frontend/app/routes
diff options
context:
space:
mode:
authorCopilot <198982749+Copilot@users.noreply.github.com>2026-03-24 20:32:17 +0100
committerGitHub <noreply@github.com>2026-03-24 20:32:17 +0100
commit695c7a65a1e9ab3b95beeaf02a1e3b10bb16996b (patch)
treef302b91a050e3ecfb295b5d16c6ab2962de1a713 /src/frontend/app/routes
parent757960525576038898d655b630cbaac44671f599 (diff)
feat: client-side trip tracking with browser notifications (#151)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: arielcostas <94913521+arielcostas@users.noreply.github.com>
Diffstat (limited to 'src/frontend/app/routes')
-rw-r--r--src/frontend/app/routes/stops-$id.tsx47
1 files changed, 46 insertions, 1 deletions
diff --git a/src/frontend/app/routes/stops-$id.tsx b/src/frontend/app/routes/stops-$id.tsx
index 4b32040..b3d7e86 100644
--- a/src/frontend/app/routes/stops-$id.tsx
+++ b/src/frontend/app/routes/stops-$id.tsx
@@ -1,7 +1,7 @@
import { CircleHelp, Eye, EyeClosed, Star } from "lucide-react";
import { useCallback, useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
-import { useParams } from "react-router";
+import { useLocation, useParams } from "react-router";
import { fetchArrivals } from "~/api/arrivals";
import {
type Arrival,
@@ -17,6 +17,7 @@ import { StopHelpModal } from "~/components/stop/StopHelpModal";
import { StopMapModal } from "~/components/stop/StopMapModal";
import { StopUsageChart } from "~/components/stop/StopUsageChart";
import { usePageRightNode, usePageTitle } from "~/contexts/PageTitleContext";
+import { useJourney } from "~/contexts/JourneyContext";
import { formatHex } from "~/utils/colours";
import StopDataProvider from "../data/StopDataProvider";
import "../tailwind-full.css";
@@ -66,6 +67,7 @@ interface ErrorInfo {
export default function Estimates() {
const { t } = useTranslation();
const params = useParams();
+ const location = useLocation();
const stopId = params.id ?? "";
const stopFeedId = stopId.split(":")[0] || stopId;
const fallbackStopCode = stopId.split(":")[1] || stopId;
@@ -89,6 +91,47 @@ export default function Estimates() {
string | undefined
>(undefined);
+ // Journey tracking
+ const { activeJourney, startJourney, stopJourney } = useJourney();
+ const trackedTripId =
+ activeJourney?.stopId === stopId ? activeJourney.tripId : undefined;
+
+ // If navigated from the journey banner, open the map for the tracked trip.
+ // Empty dependency array is intentional: we only consume the navigation state
+ // once on mount (location.state is fixed for the lifetime of this component
+ // instance; setters from useState are stable and don't need to be listed).
+ useEffect(() => {
+ const state = location.state as
+ | { openMap?: boolean; selectedTripId?: string }
+ | null
+ | undefined;
+ if (state?.openMap && state?.selectedTripId) {
+ setSelectedArrivalId(state.selectedTripId);
+ setIsMapModalOpen(true);
+ }
+ }, []); // mount-only: see comment above
+
+ const handleTrackArrival = useCallback(
+ (arrival: Arrival) => {
+ if (activeJourney?.tripId === arrival.tripId) {
+ stopJourney();
+ return;
+ }
+ startJourney({
+ tripId: arrival.tripId,
+ stopId,
+ stopName: stopName ?? stopId,
+ routeShortName: arrival.route.shortName,
+ routeColour: arrival.route.colour,
+ routeTextColour: arrival.route.textColour,
+ headsignDestination: arrival.headsign.destination,
+ initialMinutes: arrival.estimate.minutes,
+ notifyAtMinutes: 2,
+ });
+ },
+ [activeJourney, startJourney, stopJourney, stopId, stopName]
+ );
+
// Helper function to get the display name for the stop
const getStopDisplayName = useCallback(() => {
if (stopName) return stopName;
@@ -251,6 +294,8 @@ export default function Estimates() {
setSelectedArrivalId(getArrivalId(arrival));
setIsMapModalOpen(true);
}}
+ onTrackArrival={handleTrackArrival}
+ trackedTripId={trackedTripId}
/>
{data.usage && data.usage.length > 0 && (