From 276e73412abef28c222c52a84334d49f5e414f3c Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Mon, 17 Nov 2025 23:39:08 +0100 Subject: Use consolidated data API in map sheet with shared card component (#100) Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: arielcostas <94913521+arielcostas@users.noreply.github.com> Co-authored-by: Ariel Costas Guerrero --- .../Stops/ConsolidatedCirculationCard.tsx | 180 +++++++++++++++++++++ .../Stops/ConsolidatedCirculationList.css | 5 +- .../Stops/ConsolidatedCirculationList.tsx | 174 ++------------------ .../Stops/ConsolidatedCirculationListSkeleton.tsx | 6 +- 4 files changed, 198 insertions(+), 167 deletions(-) create mode 100644 src/frontend/app/components/Stops/ConsolidatedCirculationCard.tsx (limited to 'src/frontend/app/components/Stops') diff --git a/src/frontend/app/components/Stops/ConsolidatedCirculationCard.tsx b/src/frontend/app/components/Stops/ConsolidatedCirculationCard.tsx new file mode 100644 index 0000000..8c3e922 --- /dev/null +++ b/src/frontend/app/components/Stops/ConsolidatedCirculationCard.tsx @@ -0,0 +1,180 @@ +import { Clock } from "lucide-react"; +import { useTranslation } from "react-i18next"; +import LineIcon from "~components/LineIcon"; +import { type RegionConfig } from "~data/RegionConfig"; +import { type ConsolidatedCirculation } from "~routes/stops-$id"; + +import "./ConsolidatedCirculationList.css"; + +interface ConsolidatedCirculationCardProps { + estimate: ConsolidatedCirculation; + regionConfig: RegionConfig; +} + +// Utility function to parse service ID and get the turn number +const parseServiceId = (serviceId: string): string => { + const parts = serviceId.split("_"); + if (parts.length === 0) return ""; + + const lastPart = parts[parts.length - 1]; + if (lastPart.length < 6) return ""; + + const last6 = lastPart.slice(-6); + const lineCode = last6.slice(0, 3); + const turnCode = last6.slice(-3); + + // Remove leading zeros from turn + const turnNumber = parseInt(turnCode, 10).toString(); + + // Parse line number with special cases + const lineNumber = parseInt(lineCode, 10); + let displayLine: string; + + switch (lineNumber) { + case 1: + displayLine = "C1"; + break; + case 3: + displayLine = "C3"; + break; + case 30: + displayLine = "N1"; + break; + case 33: + displayLine = "N4"; + break; + case 8: + displayLine = "A"; + break; + case 101: + displayLine = "H"; + break; + case 150: + displayLine = "REF"; + break; + case 500: + displayLine = "TUR"; + break; + case 201: + displayLine = "U1"; + break; + case 202: + displayLine = "U2"; + break; + default: + displayLine = `L${lineNumber}`; + } + + return `${displayLine}-${turnNumber}`; +}; + +export const ConsolidatedCirculationCard: React.FC< + ConsolidatedCirculationCardProps +> = ({ estimate, regionConfig }) => { + const { t } = useTranslation(); + + const absoluteArrivalTime = (minutes: number) => { + const now = new Date(); + const arrival = new Date(now.getTime() + minutes * 60000); + return Intl.DateTimeFormat( + typeof navigator !== "undefined" ? navigator.language : "en", + { + hour: "2-digit", + minute: "2-digit", + } + ).format(arrival); + }; + + const formatDistance = (meters: number) => { + if (meters > 1024) { + return `${(meters / 1000).toFixed(1)} km`; + } else { + return `${meters} ${t("estimates.meters", "m")}`; + } + }; + + const getDelayText = (estimate: ConsolidatedCirculation): string | null => { + if (!estimate.schedule || !estimate.realTime) { + return null; + } + + const delay = estimate.realTime.minutes - estimate.schedule.minutes; + + if (delay >= -1 && delay <= 2) { + return "OK"; + } else if (delay > 2) { + return "R" + delay; + } else { + return "A" + Math.abs(delay); + } + }; + + const getTripIdDisplay = (tripId: string): string => { + const parts = tripId.split("_"); + return parts.length > 1 ? parts[1] : tripId; + }; + + const getTimeClass = (estimate: ConsolidatedCirculation): string => { + if (estimate.realTime && estimate.schedule?.running) { + return "time-running"; + } + + if (estimate.realTime && !estimate.schedule) { + return "time-running"; + } else if (estimate.realTime && !estimate.schedule?.running) { + return "time-delayed"; + } + + return "time-scheduled"; + }; + + const displayMinutes = + estimate.realTime?.minutes ?? estimate.schedule?.minutes ?? 0; + const timeClass = getTimeClass(estimate); + const delayText = getDelayText(estimate); + + return ( +
+
+
+ +
+ +
+ {estimate.route} +
+ +
+
+ + {estimate.realTime + ? `${displayMinutes} ${t("estimates.minutes", "min")}` + : absoluteArrivalTime(displayMinutes)} +
+
+ {estimate.schedule && ( + <> + {parseServiceId(estimate.schedule.serviceId)} ( + {getTripIdDisplay(estimate.schedule.tripId)}) + + )} + + {estimate.schedule && + estimate.realTime && + estimate.realTime.distance >= 0 && <> · } + + {estimate.realTime && estimate.realTime.distance >= 0 && ( + <>{formatDistance(estimate.realTime.distance)} + )} + + {estimate.schedule && + estimate.realTime && + estimate.realTime.distance >= 0 && <> · } + + {delayText} +
+
+
+
+ ); +}; diff --git a/src/frontend/app/components/Stops/ConsolidatedCirculationList.css b/src/frontend/app/components/Stops/ConsolidatedCirculationList.css index 4d6a3a8..939f40d 100644 --- a/src/frontend/app/components/Stops/ConsolidatedCirculationList.css +++ b/src/frontend/app/components/Stops/ConsolidatedCirculationList.css @@ -92,7 +92,10 @@ } [data-theme="dark"] .consolidated-circulation-card .arrival-time.time-scheduled, -[data-theme="dark"] .consolidated-circulation-card .arrival-time.time-scheduled svg { +[data-theme="dark"] + .consolidated-circulation-card + .arrival-time.time-scheduled + svg { color: #8fb4ff; /* lighten for dark backgrounds */ } diff --git a/src/frontend/app/components/Stops/ConsolidatedCirculationList.tsx b/src/frontend/app/components/Stops/ConsolidatedCirculationList.tsx index 4ee296d..d95ee03 100644 --- a/src/frontend/app/components/Stops/ConsolidatedCirculationList.tsx +++ b/src/frontend/app/components/Stops/ConsolidatedCirculationList.tsx @@ -1,8 +1,7 @@ -import { Clock } from "lucide-react"; import { useTranslation } from "react-i18next"; -import LineIcon from "~components/LineIcon"; import { type RegionConfig } from "~data/RegionConfig"; import { type ConsolidatedCirculation } from "~routes/stops-$id"; +import { ConsolidatedCirculationCard } from "./ConsolidatedCirculationCard"; import "./ConsolidatedCirculationList.css"; @@ -12,63 +11,6 @@ interface RegularTableProps { regionConfig: RegionConfig; } -// Utility function to parse service ID and get the turn number -const parseServiceId = (serviceId: string): string => { - const parts = serviceId.split("_"); - if (parts.length === 0) return ""; - - const lastPart = parts[parts.length - 1]; - if (lastPart.length < 6) return ""; - - const last6 = lastPart.slice(-6); - const lineCode = last6.slice(0, 3); - const turnCode = last6.slice(-3); - - // Remove leading zeros from turn - const turnNumber = parseInt(turnCode, 10).toString(); - - // Parse line number with special cases - const lineNumber = parseInt(lineCode, 10); - let displayLine: string; - - switch (lineNumber) { - case 1: - displayLine = "C1"; - break; - case 3: - displayLine = "C3"; - break; - case 30: - displayLine = "N1"; - break; - case 33: - displayLine = "N4"; - break; - case 8: - displayLine = "A"; - break; - case 101: - displayLine = "H"; - break; - case 150: - displayLine = "REF"; - break; - case 500: - displayLine = "TUR"; - break; - case 201: - displayLine = "U1"; - break; - case 202: - displayLine = "U2"; - break; - default: - displayLine = `L${lineNumber}`; - } - - return `${displayLine}-${turnNumber}`; -}; - export const ConsolidatedCirculationList: React.FC = ({ data, dataDate, @@ -76,65 +18,10 @@ export const ConsolidatedCirculationList: React.FC = ({ }) => { const { t } = useTranslation(); - const absoluteArrivalTime = (minutes: number) => { - const now = new Date(); - const arrival = new Date(now.getTime() + minutes * 60000); - return Intl.DateTimeFormat( - typeof navigator !== "undefined" ? navigator.language : "en", - { - hour: "2-digit", - minute: "2-digit", - }, - ).format(arrival); - }; - - const formatDistance = (meters: number) => { - if (meters > 1024) { - return `${(meters / 1000).toFixed(1)} km`; - } else { - return `${meters} ${t("estimates.meters", "m")}`; - } - }; - - const getDelayText = (estimate: ConsolidatedCirculation): string | null => { - if (!estimate.schedule || !estimate.realTime) { - return null; - } - - const delay = estimate.realTime.minutes - estimate.schedule.minutes; - - if (delay >= -1 && delay <= 2) { - return "OK" - } else if (delay > 2) { - return "R" + delay; - } else { - return "A" + Math.abs(delay); - } - }; - - const getTripIdDisplay = (tripId: string): string => { - const parts = tripId.split("_"); - return parts.length > 1 ? parts[1] : tripId; - }; - - const getTimeClass = (estimate: ConsolidatedCirculation): string => { - if (estimate.realTime && estimate.schedule?.running) { - return "time-running"; - } - - if (estimate.realTime && !estimate.schedule) { - return "time-running"; - } else if (estimate.realTime && !estimate.schedule?.running) { - return "time-delayed"; - } - - return "time-scheduled"; - }; - const sortedData = [...data].sort( (a, b) => (a.realTime?.minutes ?? a.schedule?.minutes ?? 999) - - (b.realTime?.minutes ?? b.schedule?.minutes ?? 999), + (b.realTime?.minutes ?? b.schedule?.minutes ?? 999) ); return ( @@ -151,56 +38,13 @@ export const ConsolidatedCirculationList: React.FC = ({ ) : ( <> - {sortedData.map((estimate, idx) => { - const displayMinutes = - estimate.realTime?.minutes ?? estimate.schedule?.minutes ?? 0; - const timeClass = getTimeClass(estimate); - const delayText = getDelayText(estimate); - - return ( -
-
-
- -
- -
- {estimate.route} -
- -
-
- - {estimate.realTime - ? `${displayMinutes} ${t("estimates.minutes", "min")}` - : absoluteArrivalTime(displayMinutes)} -
-
- {estimate.schedule && ( - <> - {parseServiceId(estimate.schedule.serviceId)} ({getTripIdDisplay(estimate.schedule.tripId)}) - - )} - - {estimate.schedule && - estimate.realTime && - estimate.realTime.distance >= 0 && <> · } - - {estimate.realTime && estimate.realTime.distance >= 0 && ( - <>{formatDistance(estimate.realTime.distance)} - )} - - {estimate.schedule && - estimate.realTime && - estimate.realTime.distance >= 0 && <> · } - - {delayText} -
-
-
-
- ); - })} + {sortedData.map((estimate, idx) => ( + + ))} )} diff --git a/src/frontend/app/components/Stops/ConsolidatedCirculationListSkeleton.tsx b/src/frontend/app/components/Stops/ConsolidatedCirculationListSkeleton.tsx index 43f02ca..90d92e2 100644 --- a/src/frontend/app/components/Stops/ConsolidatedCirculationListSkeleton.tsx +++ b/src/frontend/app/components/Stops/ConsolidatedCirculationListSkeleton.tsx @@ -34,7 +34,11 @@ export const ConsolidatedCirculationListSkeleton: React.FC = () => {
- +
))} -- cgit v1.3