From 12ecc97b07093f3cac6567c70ff75d57b429c674 Mon Sep 17 00:00:00 2001 From: Ariel Costas Guerrero Date: Tue, 21 Oct 2025 17:38:01 +0200 Subject: Implement new Santiago region (WIP) --- src/frontend/app/components/GroupedTable.tsx | 24 +- src/frontend/app/components/LineIcon.css | 295 ++++++------------------- src/frontend/app/components/LineIcon.tsx | 17 +- src/frontend/app/components/RegionSelector.tsx | 33 +++ src/frontend/app/components/RegularTable.tsx | 23 +- src/frontend/app/components/StopItem.tsx | 7 +- src/frontend/app/components/StopSheet.tsx | 17 +- src/frontend/app/components/TimetableTable.tsx | 4 +- 8 files changed, 166 insertions(+), 254 deletions(-) create mode 100644 src/frontend/app/components/RegionSelector.tsx (limited to 'src/frontend/app/components') diff --git a/src/frontend/app/components/GroupedTable.tsx b/src/frontend/app/components/GroupedTable.tsx index 47c2d31..fd97d5b 100644 --- a/src/frontend/app/components/GroupedTable.tsx +++ b/src/frontend/app/components/GroupedTable.tsx @@ -1,12 +1,14 @@ import { type StopDetails } from "../routes/estimates-$id"; import LineIcon from "./LineIcon"; +import { type RegionConfig } from "../data/RegionConfig"; interface GroupedTable { data: StopDetails; dataDate: Date | null; + regionConfig: RegionConfig; } -export const GroupedTable: React.FC = ({ data, dataDate }) => { +export const GroupedTable: React.FC = ({ data, dataDate, regionConfig }) => { const formatDistance = (meters: number) => { if (meters > 1024) { return `${(meters / 1000).toFixed(1)} km`; @@ -43,7 +45,7 @@ export const GroupedTable: React.FC = ({ data, dataDate }) => { Línea Ruta Llegada - Distancia + {regionConfig.showMeters && Distancia} @@ -53,16 +55,18 @@ export const GroupedTable: React.FC = ({ data, dataDate }) => { {idx === 0 && ( - + )} {estimate.route} {`${estimate.minutes} min`} - - {estimate.meters > -1 - ? formatDistance(estimate.meters) - : "No disponible"} - + {regionConfig.showMeters && ( + + {estimate.meters > -1 + ? formatDistance(estimate.meters) + : "No disponible"} + + )} )), )} @@ -71,7 +75,9 @@ export const GroupedTable: React.FC = ({ data, dataDate }) => { {data?.estimates.length === 0 && ( - No hay estimaciones disponibles + + No hay estimaciones disponibles + )} diff --git a/src/frontend/app/components/LineIcon.css b/src/frontend/app/components/LineIcon.css index 4613a85..7d46b98 100644 --- a/src/frontend/app/components/LineIcon.css +++ b/src/frontend/app/components/LineIcon.css @@ -1,49 +1,75 @@ +/* Vigo line colors */ :root { - --line-c1: rgb(237, 71, 19); - --line-c3d: rgb(255, 204, 0); - --line-c3i: rgb(255, 204, 0); - --line-l4a: rgb(0, 153, 0); - --line-l4c: rgb(0, 153, 0); - --line-l5a: rgb(0, 176, 240); - --line-l5b: rgb(0, 176, 240); - --line-l6: rgb(204, 51, 153); - --line-l7: rgb(150, 220, 153); - --line-l9b: rgb(244, 202, 140); - --line-l10: rgb(153, 51, 0); - --line-l11: rgb(226, 0, 38); - --line-l12a: rgb(106, 150, 190); - --line-l12b: rgb(106, 150, 190); - --line-l13: rgb(0, 176, 240); - --line-l14: rgb(129, 142, 126); - --line-l15a: rgb(216, 168, 206); - --line-l15b: rgb(216, 168, 206); - --line-l15c: rgb(216, 168, 168); - --line-l16: rgb(129, 142, 126); - --line-l17: rgb(214, 245, 31); - --line-l18a: rgb(212, 80, 168); - --line-l18b: rgb(0, 0, 0); - --line-l18h: rgb(0, 0, 0); - --line-l23: rgb(0, 70, 210); - --line-l24: rgb(191, 191, 191); - --line-l25: rgb(172, 100, 4); - --line-l27: rgb(112, 74, 42); - --line-l28: rgb(176, 189, 254); - --line-l29: rgb(248, 184, 90); - --line-l31: rgb(255, 255, 0); - --line-a: rgb(119, 41, 143); - --line-h: rgb(0, 96, 168); - --line-h1: rgb(0, 96, 168); - --line-h2: rgb(0, 96, 168); - --line-h3: rgb(0, 96, 168); - --line-lzd: rgb(61, 78, 167); - --line-n1: rgb(191, 191, 191); - --line-n4: rgb(102, 51, 102); - --line-psa1: rgb(0, 153, 0); - --line-psa4: rgb(0, 153, 0); - --line-ptl: rgb(150, 220, 153); - --line-turistico: rgb(102, 51, 102); - --line-u1: rgb(172, 100, 4); - --line-u2: rgb(172, 100, 4); + --line-vigo-c1: rgb(237, 71, 19); + --line-vigo-c3d: rgb(255, 204, 0); + --line-vigo-c3i: rgb(255, 204, 0); + --line-vigo-l4a: rgb(0, 153, 0); + --line-vigo-l4c: rgb(0, 153, 0); + --line-vigo-l5a: rgb(0, 176, 240); + --line-vigo-l5b: rgb(0, 176, 240); + --line-vigo-l6: rgb(204, 51, 153); + --line-vigo-l7: rgb(150, 220, 153); + --line-vigo-l9b: rgb(244, 202, 140); + --line-vigo-l10: rgb(153, 51, 0); + --line-vigo-l11: rgb(226, 0, 38); + --line-vigo-l12a: rgb(106, 150, 190); + --line-vigo-l12b: rgb(106, 150, 190); + --line-vigo-l13: rgb(0, 176, 240); + --line-vigo-l14: rgb(129, 142, 126); + --line-vigo-l15a: rgb(216, 168, 206); + --line-vigo-l15b: rgb(216, 168, 206); + --line-vigo-l15c: rgb(216, 168, 168); + --line-vigo-l16: rgb(129, 142, 126); + --line-vigo-l17: rgb(214, 245, 31); + --line-vigo-l18a: rgb(212, 80, 168); + --line-vigo-l18b: rgb(0, 0, 0); + --line-vigo-l18h: rgb(0, 0, 0); + --line-vigo-l23: rgb(0, 70, 210); + --line-vigo-l24: rgb(191, 191, 191); + --line-vigo-l25: rgb(172, 100, 4); + --line-vigo-l27: rgb(112, 74, 42); + --line-vigo-l28: rgb(176, 189, 254); + --line-vigo-l29: rgb(248, 184, 90); + --line-vigo-l31: rgb(255, 255, 0); + --line-vigo-a: rgb(119, 41, 143); + --line-vigo-h: rgb(0, 96, 168); + --line-vigo-h1: rgb(0, 96, 168); + --line-vigo-h2: rgb(0, 96, 168); + --line-vigo-h3: rgb(0, 96, 168); + --line-vigo-lzd: rgb(61, 78, 167); + --line-vigo-n1: rgb(191, 191, 191); + --line-vigo-n4: rgb(102, 51, 102); + --line-vigo-psa1: rgb(0, 153, 0); + --line-vigo-psa4: rgb(0, 153, 0); + --line-vigo-ptl: rgb(150, 220, 153); + --line-vigo-turistico: rgb(102, 51, 102); + --line-vigo-u1: rgb(172, 100, 4); + --line-vigo-u2: rgb(172, 100, 4); + + --line-santiago-l1: #f32621; + --line-santiago-l4: #ffcc33; + --line-santiago-l5: #fa8405; + --line-santiago-l6: #d73983; + --line-santiago-l6a: #d73983; + --line-santiago-l7: #488bc1; + --line-santiago-l8: #6aaf48; + --line-santiago-l9: #46b8bb; + --line-santiago-c11: #aec741; + --line-santiago-l12: #842e14; + --line-santiago-l13: #336600; + --line-santiago-l15: #7a4b2a; + --line-santiago-c2: #283a87; + --line-santiago-c4: #283a87; + --line-santiago-c5: #999999; + --line-santiago-c6: #006666; + --line-santiago-p1: #537eb3; + --line-santiago-p2: #d23354; + --line-santiago-p3: #75bd96; + --line-santiago-p4: #f1c54f; + --line-santiago-p6: #999999; + --line-santiago-p7: #d2438c; + --line-santiago-p8: #e28c3a; + } .line-icon { @@ -55,187 +81,8 @@ font-weight: 600; text-transform: uppercase; border-radius: 0.25rem 0.25rem 0 0; - color: var(--text-color); background-color: var(--background-color); } -.line-c1 { - border-color: var(--line-c1); -} - -.line-c3d { - border-color: var(--line-c3d); -} - -.line-c3i { - border-color: var(--line-c3i); -} - -.line-l4a { - border-color: var(--line-l4a); -} - -.line-l4c { - border-color: var(--line-l4c); -} - -.line-l5a { - border-color: var(--line-l5a); -} - -.line-l5b { - border-color: var(--line-l5b); -} - -.line-l6 { - border-color: var(--line-l6); -} - -.line-l7 { - border-color: var(--line-l7); -} - -.line-l9b { - border-color: var(--line-l9b); -} - -.line-l10 { - border-color: var(--line-l10); -} - -.line-l11 { - border-color: var(--line-l11); -} - -.line-l12a { - border-color: var(--line-l12a); -} - -.line-l12b { - border-color: var(--line-l12b); -} - -.line-l13 { - border-color: var(--line-l13); -} - -.line-l14 { - border-color: var(--line-l14); -} - -.line-l15a { - border-color: var(--line-l15a); -} - -.line-l15b { - border-color: var(--line-l15b); -} - -.line-l15c { - border-color: var(--line-l15c); -} - -.line-l16 { - border-color: var(--line-l16); -} - -.line-l17 { - border-color: var(--line-l17); -} - -.line-l18a { - border-color: var(--line-l18a); -} - -.line-l18b { - border-color: var(--line-l18b); -} - -.line-l18h { - border-color: var(--line-l18h); -} - -.line-l23 { - border-color: var(--line-l23); -} - -.line-l24 { - border-color: var(--line-l24); -} - -.line-l25 { - border-color: var(--line-l25); -} - -.line-l27 { - border-color: var(--line-l27); -} -.line-l28 { - border-color: var(--line-l28); -} - -.line-l29 { - border-color: var(--line-l29); -} - -.line-l31 { - border-color: var(--line-l31); -} - -.line-a { - border-color: var(--line-a); -} - -.line-h { - border-color: var(--line-h); -} - -.line-h1 { - border-color: var(--line-h1); -} - -.line-h2 { - border-color: var(--line-h2); -} - -.line-h3 { - border-color: var(--line-h3); -} - -.line-lzd { - border-color: var(--line-lzd); -} - -.line-n1 { - border-color: var(--line-n1); -} - -.line-n4 { - border-color: var(--line-n4); -} - -.line-psa1 { - border-color: var(--line-psa1); -} - -.line-psa4 { - border-color: var(--line-psa4); -} - -.line-ptl { - border-color: var(--line-ptl); -} - -.line-turistico { - border-color: var(--line-turistico); -} - -.line-u1 { - border-color: var(--line-u1); -} - -.line-u2 { - border-color: var(--line-u2); -} diff --git a/src/frontend/app/components/LineIcon.tsx b/src/frontend/app/components/LineIcon.tsx index 3d613e6..4f4bfd9 100644 --- a/src/frontend/app/components/LineIcon.tsx +++ b/src/frontend/app/components/LineIcon.tsx @@ -1,14 +1,23 @@ -import React from "react"; +import React, { useMemo } from "react"; import "./LineIcon.css"; +import { type RegionId } from "../data/RegionConfig"; interface LineIconProps { line: string; + region?: RegionId; } -const LineIcon: React.FC = ({ line }) => { - const formattedLine = /^[a-zA-Z]/.test(line) ? line : `L${line}`; +const LineIcon: React.FC = ({ line, region = "vigo" }) => { + const formattedLine = useMemo(() => { + return /^[a-zA-Z]/.test(line) ? line : `L${line}`; + }, [line]); + const cssVarName = `--line-${region}-${formattedLine.toLowerCase()}`; + return ( - + {formattedLine} ); diff --git a/src/frontend/app/components/RegionSelector.tsx b/src/frontend/app/components/RegionSelector.tsx new file mode 100644 index 0000000..6c9fe8b --- /dev/null +++ b/src/frontend/app/components/RegionSelector.tsx @@ -0,0 +1,33 @@ +import { useApp } from "../AppContext"; +import { getAvailableRegions } from "../data/RegionConfig"; +import "./RegionSelector.css"; + +export function RegionSelector() { + const { region, setRegion } = useApp(); + const regions = getAvailableRegions(); + + const handleRegionChange = (e: React.ChangeEvent) => { + const newRegion = e.target.value as any; + setRegion(newRegion); + }; + + return ( +
+ + +
+ ); +} diff --git a/src/frontend/app/components/RegularTable.tsx b/src/frontend/app/components/RegularTable.tsx index 8b01410..68b732a 100644 --- a/src/frontend/app/components/RegularTable.tsx +++ b/src/frontend/app/components/RegularTable.tsx @@ -1,15 +1,18 @@ import { useTranslation } from "react-i18next"; import { type StopDetails } from "../routes/estimates-$id"; import LineIcon from "./LineIcon"; +import { type RegionConfig } from "../data/RegionConfig"; interface RegularTableProps { data: StopDetails; dataDate: Date | null; + regionConfig: RegionConfig; } export const RegularTable: React.FC = ({ data, dataDate, + regionConfig, }) => { const { t } = useTranslation(); @@ -46,7 +49,9 @@ export const RegularTable: React.FC = ({ {t("estimates.line", "Línea")} {t("estimates.route", "Ruta")} {t("estimates.arrival", "Llegada")} - {t("estimates.distance", "Distancia")} + {regionConfig.showMeters && ( + {t("estimates.distance", "Distancia")} + )} @@ -56,7 +61,7 @@ export const RegularTable: React.FC = ({ .map((estimate, idx) => ( - + {estimate.route} @@ -64,11 +69,13 @@ export const RegularTable: React.FC = ({ ? absoluteArrivalTime(estimate.minutes) : `${estimate.minutes} ${t("estimates.minutes", "min")}`} - - {estimate.meters > -1 - ? formatDistance(estimate.meters) - : t("estimates.not_available", "No disponible")} - + {regionConfig.showMeters && ( + + {estimate.meters > -1 + ? formatDistance(estimate.meters) + : t("estimates.not_available", "No disponible")} + + )} ))} @@ -76,7 +83,7 @@ export const RegularTable: React.FC = ({ {data?.estimates.length === 0 && ( - + {t("estimates.none", "No hay estimaciones disponibles")} diff --git a/src/frontend/app/components/StopItem.tsx b/src/frontend/app/components/StopItem.tsx index b781eb9..7d89d7d 100644 --- a/src/frontend/app/components/StopItem.tsx +++ b/src/frontend/app/components/StopItem.tsx @@ -2,19 +2,22 @@ import React from "react"; import { Link } from "react-router"; import StopDataProvider, { type Stop } from "../data/StopDataProvider"; import LineIcon from "./LineIcon"; +import { useApp } from "../AppContext"; interface StopItemProps { stop: Stop; } const StopItem: React.FC = ({ stop }) => { + const { region } = useApp(); + return (
  • {stop.favourite && } ( - {stop.stopId}) {StopDataProvider.getDisplayName(stop)} + {stop.stopId}) {StopDataProvider.getDisplayName(region, stop)}
    - {stop.lines?.map((line) => )} + {stop.lines?.map((line) => )}
  • diff --git a/src/frontend/app/components/StopSheet.tsx b/src/frontend/app/components/StopSheet.tsx index 702c574..7255884 100644 --- a/src/frontend/app/components/StopSheet.tsx +++ b/src/frontend/app/components/StopSheet.tsx @@ -7,6 +7,8 @@ import LineIcon from "./LineIcon"; import { StopSheetSkeleton } from "./StopSheetSkeleton"; import { ErrorDisplay } from "./ErrorDisplay"; import { type StopDetails } from "../routes/estimates-$id"; +import { type RegionId, getRegionConfig } from "../data/RegionConfig"; +import { useApp } from "../AppContext"; import "./StopSheet.css"; interface StopSheetProps { @@ -22,8 +24,9 @@ interface ErrorInfo { message?: string; } -const loadStopData = async (stopId: number): Promise => { - const resp = await fetch(`/api/GetStopEstimates?id=${stopId}`, { +const loadStopData = async (region: RegionId, stopId: number): Promise => { + const regionConfig = getRegionConfig(region); + const resp = await fetch(`${regionConfig.estimatesEndpoint}?id=${stopId}`, { headers: { Accept: "application/json", }, @@ -43,6 +46,8 @@ export const StopSheet: React.FC = ({ stopName, }) => { const { t } = useTranslation(); + const { region } = useApp(); + const regionConfig = getRegionConfig(region); const [data, setData] = useState(null); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); @@ -72,7 +77,7 @@ export const StopSheet: React.FC = ({ setError(null); setData(null); - const stopData = await loadStopData(stopId); + const stopData = await loadStopData(region, stopId); setData(stopData); setLastUpdated(new Date()); } catch (err) { @@ -87,7 +92,7 @@ export const StopSheet: React.FC = ({ if (isOpen && stopId) { loadData(); } - }, [isOpen, stopId]); + }, [isOpen, stopId, region]); const formatTime = (minutes: number) => { if (minutes > 15) { @@ -157,7 +162,7 @@ export const StopSheet: React.FC = ({ {limitedEstimates.map((estimate, idx) => (
    - +
    @@ -165,7 +170,7 @@ export const StopSheet: React.FC = ({
    {formatTime(estimate.minutes)} - {estimate.meters > -1 && ( + {regionConfig.showMeters && estimate.meters > -1 && ( {" • "} {formatDistance(estimate.meters)} diff --git a/src/frontend/app/components/TimetableTable.tsx b/src/frontend/app/components/TimetableTable.tsx index 86896ca..8215141 100644 --- a/src/frontend/app/components/TimetableTable.tsx +++ b/src/frontend/app/components/TimetableTable.tsx @@ -1,6 +1,7 @@ import { useTranslation } from "react-i18next"; import LineIcon from "./LineIcon"; import "./TimetableTable.css"; +import { useApp } from "../AppContext"; export interface TimetableEntry { line: { @@ -97,6 +98,7 @@ export const TimetableTable: React.FC = ({ currentTime }) => { const { t } = useTranslation(); + const { region } = useApp(); const displayData = showAll ? data : findNearbyEntries(data, currentTime || ''); const nowMinutes = currentTime ? timeToMinutes(currentTime) : timeToMinutes(new Date().toTimeString().slice(0, 8)); @@ -126,7 +128,7 @@ export const TimetableTable: React.FC = ({ >
    - +
    -- cgit v1.3