From 4b7eaa318f22d7cc768491c421cb7aeac477f95d Mon Sep 17 00:00:00 2001 From: Ariel Costas Guerrero Date: Mon, 22 Dec 2025 18:16:57 +0100 Subject: Implement retrieving next arrivals for a stop (scheduled only) --- src/frontend/app/components/Stops/ArrivalCard.css | 17 ++++ src/frontend/app/components/Stops/ArrivalCard.tsx | 72 ++++++++++++++ src/frontend/app/components/Stops/ArrivalList.tsx | 25 +++++ .../app/components/map/StopSummarySheet.tsx | 109 ++++----------------- 4 files changed, 132 insertions(+), 91 deletions(-) create mode 100644 src/frontend/app/components/Stops/ArrivalCard.css create mode 100644 src/frontend/app/components/Stops/ArrivalCard.tsx create mode 100644 src/frontend/app/components/Stops/ArrivalList.tsx (limited to 'src/frontend/app/components') diff --git a/src/frontend/app/components/Stops/ArrivalCard.css b/src/frontend/app/components/Stops/ArrivalCard.css new file mode 100644 index 0000000..5835352 --- /dev/null +++ b/src/frontend/app/components/Stops/ArrivalCard.css @@ -0,0 +1,17 @@ +@import "../../tailwind.css"; + +.time-running { + @apply bg-green-600/20 dark:bg-green-600/25 text-[#1a9e56] dark:text-[#22c55e]; +} + +.time-delayed { + @apply bg-orange-600/20 dark:bg-orange-600/25 text-[#d06100] dark:text-[#fb923c]; +} + +.time-past { + @apply bg-gray-600/20 dark:bg-gray-600/25 text-gray-600 dark:text-gray-400; +} + +.time-scheduled { + @apply bg-blue-900/20 dark:bg-blue-600/25 text-[#0b3d91] dark:text-[#93c5fd]; +} diff --git a/src/frontend/app/components/Stops/ArrivalCard.tsx b/src/frontend/app/components/Stops/ArrivalCard.tsx new file mode 100644 index 0000000..96d0af0 --- /dev/null +++ b/src/frontend/app/components/Stops/ArrivalCard.tsx @@ -0,0 +1,72 @@ +import React, { useMemo } from "react"; +import { useTranslation } from "react-i18next"; +import LineIcon from "~/components/LineIcon"; +import { type Arrival } from "../../api/schema"; +import "./ArrivalCard.css"; + +interface ArrivalCardProps { + arrival: Arrival; + reduced?: boolean; +} + +export const ArrivalCard: React.FC = ({ + arrival, + reduced, +}) => { + const { t } = useTranslation(); + const { route, headsign, estimate } = arrival; + + const etaValue = Math.max(0, Math.round(estimate.minutes)).toString(); + const etaUnit = t("estimates.minutes", "min"); + + const timeClass = useMemo(() => { + switch (estimate.precission) { + case "confident": + return "time-running"; + case "unsure": + return "time-delayed"; + case "past": + return "time-past"; + default: + return "time-scheduled"; + } + }, [estimate.precission]); + + return ( +
+
+ +
+
+ + {headsign.destination} + +
+
+
+ {etaValue} + + {etaUnit} + +
+
+
+ ); +}; diff --git a/src/frontend/app/components/Stops/ArrivalList.tsx b/src/frontend/app/components/Stops/ArrivalList.tsx new file mode 100644 index 0000000..a1210d5 --- /dev/null +++ b/src/frontend/app/components/Stops/ArrivalList.tsx @@ -0,0 +1,25 @@ +import React from "react"; +import { type Arrival } from "../../api/schema"; +import { ArrivalCard } from "./ArrivalCard"; + +interface ArrivalListProps { + arrivals: Arrival[]; + reduced?: boolean; +} + +export const ArrivalList: React.FC = ({ + arrivals, + reduced, +}) => { + return ( +
+ {arrivals.map((arrival, index) => ( + + ))} +
+ ); +}; diff --git a/src/frontend/app/components/map/StopSummarySheet.tsx b/src/frontend/app/components/map/StopSummarySheet.tsx index b24e71c..16a9cbe 100644 --- a/src/frontend/app/components/map/StopSummarySheet.tsx +++ b/src/frontend/app/components/map/StopSummarySheet.tsx @@ -1,11 +1,10 @@ import { RefreshCw } from "lucide-react"; -import React, { useEffect, useState } from "react"; +import React from "react"; import { useTranslation } from "react-i18next"; import { Sheet } from "react-modal-sheet"; import { Link } from "react-router"; -import { ConsolidatedCirculationList } from "~/components/Stops/ConsolidatedCirculationList"; -import { APP_CONSTANTS } from "~/config/constants"; -import { type ConsolidatedCirculation } from "../../routes/stops-$id"; +import { ArrivalList } from "~/components/Stops/ArrivalList"; +import { useStopArrivals } from "../../hooks/useArrivals"; import { ErrorDisplay } from "../ErrorDisplay"; import LineIcon from "../LineIcon"; import "./StopSummarySheet.css"; @@ -27,95 +26,24 @@ export interface StopSheetProps { }; } -interface ErrorInfo { - type: "network" | "server" | "unknown"; - status?: number; - message?: string; -} - -const loadConsolidatedData = async ( - stopId: string -): Promise => { - const resp = await fetch( - `${APP_CONSTANTS.consolidatedCirculationsEndpoint}?stopId=${stopId}`, - { - headers: { - Accept: "application/json", - }, - } - ); - - if (!resp.ok) { - throw new Error(`HTTP ${resp.status}: ${resp.statusText}`); - } - - return await resp.json(); -}; - export const StopSheet: React.FC = ({ isOpen, onClose, stop, }) => { const { t } = useTranslation(); - const [data, setData] = useState(null); - const [loading, setLoading] = useState(false); - const [error, setError] = useState(null); - const [lastUpdated, setLastUpdated] = useState(null); - - const parseError = (error: any): ErrorInfo => { - if (!navigator.onLine) { - return { type: "network", message: "No internet connection" }; - } - - if ( - error.message?.includes("Failed to fetch") || - error.message?.includes("NetworkError") - ) { - return { type: "network" }; - } - - if (error.message?.includes("HTTP")) { - const statusMatch = error.message.match(/HTTP (\d+):/); - const status = statusMatch ? parseInt(statusMatch[1]) : undefined; - return { type: "server", status }; - } - - return { type: "unknown", message: error.message }; - }; - - const loadData = async () => { - try { - setLoading(true); - setError(null); - setData(null); - - const stopData = await loadConsolidatedData(stop.stopId); - setData(stopData); - setLastUpdated(new Date()); - } catch (err) { - console.error("Failed to load stop data:", err); - setError(parseError(err)); - } finally { - setLoading(false); - } - }; + const { + data, + isLoading: loading, + error, + refetch: loadData, + dataUpdatedAt, + } = useStopArrivals(stop.stopId, true, isOpen); - useEffect(() => { - if (isOpen && stop.stopId) { - loadData(); - } - }, [isOpen, stop.stopId]); + const lastUpdated = dataUpdatedAt ? new Date(dataUpdatedAt) : null; // Show only the next 4 arrivals - const sortedData = data - ? [...data].sort( - (a, b) => - (a.realTime?.minutes ?? a.schedule?.minutes ?? 999) - - (b.realTime?.minutes ?? b.schedule?.minutes ?? 999) - ) - : []; - const limitedEstimates = sortedData.slice(0, 4); + const limitedEstimates = data?.arrivals.slice(0, 4) ?? []; return ( @@ -147,8 +75,11 @@ export const StopSheet: React.FC = ({ ) : error ? ( loadData()} title={t( "errors.estimates_title", "Error al cargar estimaciones" @@ -167,11 +98,7 @@ export const StopSheet: React.FC = ({ {t("estimates.none", "No hay estimaciones disponibles")} ) : ( - + )} -- cgit v1.3