From 9409e52a7fe14575966962c4fc3fbf248699cd96 Mon Sep 17 00:00:00 2001 From: Ariel Costas Guerrero Date: Sun, 1 Mar 2026 11:26:27 +0100 Subject: Route detail visualisation improvement Squashed commit of the following: commit 2f2261f764e0a0a52652bceda306f39f6f568b87 Author: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Date: Sun Mar 1 10:21:52 2026 +0000 Implement route-specific realtime filtering and route detail UI updates Co-authored-by: arielcostas <94913521+arielcostas@users.noreply.github.com> commit e5ab68b158558e0f6577bf0fdd95e652fb269e6a Author: Ariel Costas Guerrero Date: Sun Mar 1 11:17:19 2026 +0100 Fix delay formatting to ensure absolute values are displayed for arrivals and rejections commit df7d61c089a4e55a3b2efad8556b17e1f7f25e1c Author: Ariel Costas Guerrero Date: Sun Mar 1 11:14:11 2026 +0100 Improve the formatting a bit for the arrival schedules commit 4b65cdb43824ba936234be6b5bdd6cf8ac9c56bb Author: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Date: Sun Mar 1 10:05:34 2026 +0000 Fix hook-order violation in route details stop departures Co-authored-by: arielcostas <94913521+arielcostas@users.noreply.github.com> commit 2da4fb594f1433ddd1a26e267bbc7e917145b3b5 Author: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Date: Sun Mar 1 09:50:14 2026 +0000 Polish selected-stop realtime display in route details Co-authored-by: arielcostas <94913521+arielcostas@users.noreply.github.com> commit dc7fc11085773a030bc9109e8c435a62a3567051 Author: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Date: Sun Mar 1 09:48:33 2026 +0000 Load route-details realtime only for selected stop Co-authored-by: arielcostas <94913521+arielcostas@users.noreply.github.com> commit b9408664fd0c0d115f6aa0341deb9fa5b74f2b26 Author: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Date: Fri Feb 27 18:20:16 2026 +0000 Initial plan --- src/frontend/app/api/schema.ts | 1 + src/frontend/app/routes/routes-$id.tsx | 175 +++++++++++++++++++++++++++++++-- 2 files changed, 167 insertions(+), 9 deletions(-) diff --git a/src/frontend/app/api/schema.ts b/src/frontend/app/api/schema.ts index c0c97a4..57f34b1 100644 --- a/src/frontend/app/api/schema.ts +++ b/src/frontend/app/api/schema.ts @@ -1,6 +1,7 @@ import { z } from "zod"; export const RouteInfoSchema = z.object({ + gtfsId: z.string().optional().nullable(), shortName: z.string(), colour: z.string(), textColour: z.string(), diff --git a/src/frontend/app/routes/routes-$id.tsx b/src/frontend/app/routes/routes-$id.tsx index 32f1fb7..6cc872d 100644 --- a/src/frontend/app/routes/routes-$id.tsx +++ b/src/frontend/app/routes/routes-$id.tsx @@ -24,6 +24,7 @@ import { usePageTitle, usePageTitleNode, } from "~/contexts/PageTitleContext"; +import { useStopArrivals } from "~/hooks/useArrivals"; import { formatHex } from "~/utils/colours"; import "../tailwind-full.css"; @@ -55,12 +56,51 @@ export default function RouteDetailsPage() { () => formatDateKey(selectedWeekDate), [selectedWeekDate] ); + const ONE_HOUR_SECONDS = 3600; + const isTodaySelectedDate = selectedDateKey === formatDateKey(new Date()); + const now = new Date(); + const nowSeconds = + now.getHours() * 3600 + now.getMinutes() * 60 + now.getSeconds(); + const formatDelayMinutes = (delayMinutes: number) => { + if (delayMinutes === 0) return "OK"; + return delayMinutes > 0 + ? ` (R${Math.abs(delayMinutes)})` + : ` (A${Math.abs(delayMinutes)})`; + }; const { data: route, isLoading } = useQuery({ queryKey: ["route", id, selectedDateKey], queryFn: () => fetchRouteDetails(id!, selectedDateKey), enabled: !!id, }); + const { data: selectedStopRealtime, isLoading: isRealtimeLoading } = + useStopArrivals( + selectedStopId ?? "", + true, + Boolean(selectedStopId) && isTodaySelectedDate + ); + const filteredRealtimeArrivals = useMemo(() => { + const arrivals = selectedStopRealtime?.arrivals ?? []; + if (arrivals.length === 0) { + return []; + } + + const routeId = id?.trim(); + const routeShortName = route?.shortName?.trim().toLowerCase(); + + return arrivals.filter((arrival) => { + const arrivalGtfsId = arrival.route.gtfsId?.trim(); + if (routeId && arrivalGtfsId) { + return arrivalGtfsId === routeId; + } + + if (routeShortName) { + return arrival.route.shortName.trim().toLowerCase() === routeShortName; + } + + return true; + }); + }, [selectedStopRealtime?.arrivals, id, route?.shortName]); usePageTitle( route?.shortName @@ -161,6 +201,35 @@ export default function RouteDetailsPage() { const selectedPatternLabel = selectedPattern ? selectedPattern.headsign || selectedPattern.name : t("routes.details", "Detalles de ruta"); + const sameDirectionPatterns = selectedPattern + ? (patternsByDirection[selectedPattern.directionId] ?? []) + : []; + const departuresByStop = (() => { + const byStop = new Map< + string, + { departure: number; patternId: string; tripId?: string | null }[] + >(); + + for (const pattern of sameDirectionPatterns) { + for (const stop of pattern.stops) { + const current = byStop.get(stop.id) ?? []; + current.push( + ...stop.scheduledDepartures.map((departure) => ({ + departure, + patternId: pattern.id, + tripId: null, + })) + ); + byStop.set(stop.id, current); + } + } + + for (const stopDepartures of byStop.values()) { + stopDepartures.sort((a, b) => a.departure - b.departure); + } + + return byStop; + })(); const mapHeightClass = layoutMode === "map" @@ -274,14 +343,33 @@ export default function RouteDetailsPage() { {selectedPattern?.geometry && ( + @@ -551,24 +639,93 @@ export default function RouteDetailsPage() { )} {selectedStopId === stop.id && - stop.scheduledDepartures.length > 0 && ( + (departuresByStop.get(stop.id)?.length ?? 0) > 0 && (
- {stop.scheduledDepartures.map((dep, i) => ( + {( + departuresByStop + .get(stop.id) + ?.filter((item) => + isTodaySelectedDate + ? item.departure >= + nowSeconds - ONE_HOUR_SECONDS + : true + ) ?? [] + ).map((item, i) => ( - {Math.floor(dep / 3600) + {Math.floor(item.departure / 3600) .toString() .padStart(2, "0")} : - {Math.floor((dep % 3600) / 60) + {Math.floor((item.departure % 3600) / 60) .toString() .padStart(2, "0")} ))}
)} + + {selectedStopId === stop.id && isTodaySelectedDate && ( +
+
+ {t("routes.realtime", "Tiempo real")} +
+ {isRealtimeLoading ? ( +
+ {t("routes.loading_realtime", "Cargando...")} +
+ ) : filteredRealtimeArrivals.length === 0 ? ( +
+ {t( + "routes.realtime_no_route_estimates", + "Sin estimaciones para esta línea" + )} +
+ ) : ( + <> +
+ + {t("routes.next_arrival", "Próximo")} + + + {filteredRealtimeArrivals[0].estimate.minutes}′ + {filteredRealtimeArrivals[0].delay?.minutes + ? formatDelayMinutes( + filteredRealtimeArrivals[0].delay.minutes + ) + : ""} + +
+ + {filteredRealtimeArrivals.length > 1 && ( +
+ {filteredRealtimeArrivals + .slice(1) + .map((arrival, i) => ( + + {arrival.estimate.minutes}′ + {arrival.delay?.minutes + ? formatDelayMinutes( + arrival.delay.minutes + ) + : ""} + + ))} +
+ )} + + )} +
+ )} ))} -- cgit v1.3