diff options
| author | Ariel Costas Guerrero <ariel@costas.dev> | 2026-03-01 11:26:27 +0100 |
|---|---|---|
| committer | Ariel Costas Guerrero <ariel@costas.dev> | 2026-03-01 11:26:27 +0100 |
| commit | 9409e52a7fe14575966962c4fc3fbf248699cd96 (patch) | |
| tree | 364ad679d948333f32e6a30713c5df7aa5eff9ad /src | |
| parent | 3573ecae94aa328591d4b3a6e2d05e4fc9e261fc (diff) | |
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 <ariel@costas.dev>
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 <ariel@costas.dev>
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
Diffstat (limited to 'src')
| -rw-r--r-- | src/frontend/app/api/schema.ts | 1 | ||||
| -rw-r--r-- | 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 && ( <Source type="geojson" data={geojson}> <Layer - id="route-line" + id="route-line-border" + type="line" + paint={{ + "line-color": + route.textColor && route.textColor.trim() + ? formatHex(route.textColor) + : "#111827", + "line-width": 7, + "line-opacity": 0.75, + }} + layout={{ + "line-cap": "round", + "line-join": "round", + }} + /> + <Layer + id="route-line-inner" type="line" paint={{ "line-color": route.color ? formatHex(route.color) : "#3b82f6", - "line-width": 4, - "line-opacity": 0.8, + "line-width": 5, + }} + layout={{ + "line-cap": "round", + "line-join": "round", }} /> </Source> @@ -551,24 +639,93 @@ export default function RouteDetailsPage() { )} {selectedStopId === stop.id && - stop.scheduledDepartures.length > 0 && ( + (departuresByStop.get(stop.id)?.length ?? 0) > 0 && ( <div className="mt-2 flex flex-wrap gap-1"> - {stop.scheduledDepartures.map((dep, i) => ( + {( + departuresByStop + .get(stop.id) + ?.filter((item) => + isTodaySelectedDate + ? item.departure >= + nowSeconds - ONE_HOUR_SECONDS + : true + ) ?? [] + ).map((item, i) => ( <span - key={i} - className="text-[10px] px-1.5 py-0.5 bg-gray-100 dark:bg-gray-800 rounded" + key={`${item.patternId}-${item.departure}-${i}`} + className={`text-[11px] px-2 py-0.5 rounded ${ + item.patternId === selectedPattern?.id + ? "bg-gray-100 dark:bg-gray-900" + : "bg-gray-50 dark:bg-gray-900 text-gray-400 font-light" + }`} > - {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")} </span> ))} </div> )} + + {selectedStopId === stop.id && isTodaySelectedDate && ( + <div className="mt-2"> + <div className="text-[10px] uppercase tracking-wide text-muted mb-1"> + {t("routes.realtime", "Tiempo real")} + </div> + {isRealtimeLoading ? ( + <div className="text-[11px] text-muted"> + {t("routes.loading_realtime", "Cargando...")} + </div> + ) : filteredRealtimeArrivals.length === 0 ? ( + <div className="text-[11px] text-muted"> + {t( + "routes.realtime_no_route_estimates", + "Sin estimaciones para esta línea" + )} + </div> + ) : ( + <> + <div className="flex items-center justify-between gap-2 rounded-lg border border-green-500/30 bg-green-500/10 px-2.5 py-2"> + <span className="text-[11px] font-semibold uppercase tracking-wide text-green-700 dark:text-green-300"> + {t("routes.next_arrival", "Próximo")} + </span> + <span className="inline-flex min-w-16 items-center justify-center rounded-xl bg-green-600 px-3 py-1.5 text-base font-bold leading-none text-white"> + {filteredRealtimeArrivals[0].estimate.minutes}′ + {filteredRealtimeArrivals[0].delay?.minutes + ? formatDelayMinutes( + filteredRealtimeArrivals[0].delay.minutes + ) + : ""} + </span> + </div> + + {filteredRealtimeArrivals.length > 1 && ( + <div className="mt-2 flex flex-wrap justify-end gap-1"> + {filteredRealtimeArrivals + .slice(1) + .map((arrival, i) => ( + <span + key={`${arrival.tripId}-${i}`} + className="text-[11px] px-2 py-0.5 bg-primary/10 text-primary rounded" + > + {arrival.estimate.minutes}′ + {arrival.delay?.minutes + ? formatDelayMinutes( + arrival.delay.minutes + ) + : ""} + </span> + ))} + </div> + )} + </> + )} + </div> + )} </div> </div> ))} |
