diff options
| author | Ariel Costas Guerrero <ariel@costas.dev> | 2025-12-08 12:04:25 +0100 |
|---|---|---|
| committer | Ariel Costas Guerrero <ariel@costas.dev> | 2025-12-08 12:04:25 +0100 |
| commit | b9bb62cf0c2af848bf02e2a74d9bd109ef570010 (patch) | |
| tree | 9300e05dca96a39a06e8a38bf7ee91dcd7ec77ea /src/frontend | |
| parent | 107295575e3a7c37911ae192baf426b0003975a4 (diff) | |
Update formatting
Diffstat (limited to 'src/frontend')
28 files changed, 605 insertions, 478 deletions
diff --git a/src/frontend/app/AppContext.tsx b/src/frontend/app/AppContext.tsx index 12a54da..2102ad7 100644 --- a/src/frontend/app/AppContext.tsx +++ b/src/frontend/app/AppContext.tsx @@ -23,7 +23,9 @@ export const useApp = () => { ...map, // Mock region support for now since we only have one region region: "vigo" as RegionId, - setRegion: (region: RegionId) => { console.log("Set region", region); }, + setRegion: (region: RegionId) => { + console.log("Set region", region); + }, }; }; diff --git a/src/frontend/app/components/LineIcon.tsx b/src/frontend/app/components/LineIcon.tsx index fc40824..8bbeb20 100644 --- a/src/frontend/app/components/LineIcon.tsx +++ b/src/frontend/app/components/LineIcon.tsx @@ -6,13 +6,10 @@ interface LineIconProps { mode?: "rounded" | "pill" | "default"; } -const LineIcon: React.FC<LineIconProps> = ({ - line, - mode = "default", -}) => { +const LineIcon: React.FC<LineIconProps> = ({ line, mode = "default" }) => { const actualLine = useMemo(() => { - return line.trim().replace('510', 'NAD'); - }, [line]) + return line.trim().replace("510", "NAD"); + }, [line]); const formattedLine = useMemo(() => { return /^[a-zA-Z]/.test(actualLine) ? actualLine : `L${actualLine}`; diff --git a/src/frontend/app/components/StopGallery.tsx b/src/frontend/app/components/StopGallery.tsx index a45bfca..8c13aa1 100644 --- a/src/frontend/app/components/StopGallery.tsx +++ b/src/frontend/app/components/StopGallery.tsx @@ -36,7 +36,9 @@ const StopGallery: React.FC<StopGalleryProps> = ({ if (stops.length === 0 && emptyMessage) { return ( <div className="w-full px-4 flex flex-col gap-2"> - <h3 className="text-lg font-semibold text-gray-900 dark:text-gray-100">{title}</h3> + <h3 className="text-lg font-semibold text-gray-900 dark:text-gray-100"> + {title} + </h3> <div className="text-center"> <p className="text-sm px-4 py-3 bg-gray-100 dark:bg-gray-800 rounded-lg"> {emptyMessage} @@ -52,15 +54,17 @@ const StopGallery: React.FC<StopGalleryProps> = ({ return ( <div className="w-full px-4 flex flex-col gap-2"> - <h3 className="text-lg font-semibold text-gray-900 dark:text-gray-100">{title}</h3> + <h3 className="text-lg font-semibold text-gray-900 dark:text-gray-100"> + {title} + </h3> <div ref={scrollRef} className="overflow-x-auto overflow-y-hidden snap-x snap-mandatory scrollbar-hide pb-2" style={{ - WebkitOverflowScrolling: 'touch', - scrollbarWidth: 'none', - msOverflowStyle: 'none' + WebkitOverflowScrolling: "touch", + scrollbarWidth: "none", + msOverflowStyle: "none", }} > <div className="flex gap-3"> @@ -73,8 +77,11 @@ const StopGallery: React.FC<StopGalleryProps> = ({ {stops.map((_, index) => ( <span key={index} - className={`w-1.5 h-1.5 rounded-full transition-colors duration-200 ${index === activeIndex ? "bg-blue-600" : "bg-gray-300 dark:bg-gray-700" - }`} + className={`w-1.5 h-1.5 rounded-full transition-colors duration-200 ${ + index === activeIndex + ? "bg-blue-600" + : "bg-gray-300 dark:bg-gray-700" + }`} ></span> ))} </div> diff --git a/src/frontend/app/components/StopGalleryItem.tsx b/src/frontend/app/components/StopGalleryItem.tsx index 6c80362..bf60697 100644 --- a/src/frontend/app/components/StopGalleryItem.tsx +++ b/src/frontend/app/components/StopGalleryItem.tsx @@ -22,7 +22,9 @@ const StopGalleryItem: React.FC<StopGalleryItemProps> = ({ stop }) => { to={`/stops/${stop.stopId}`} > <div className="flex items-center gap-2 mb-1"> - {stop.favourite && <span className="text-yellow-500 text-base">★</span>} + {stop.favourite && ( + <span className="text-yellow-500 text-base">★</span> + )} <span className="text-xs text-gray-600 dark:text-gray-400 font-medium"> ({stop.stopId}) </span> @@ -30,10 +32,10 @@ const StopGalleryItem: React.FC<StopGalleryItemProps> = ({ stop }) => { <div className="text-[0.95rem] font-semibold mb-2 leading-snug line-clamp-2 min-h-[2.5em]" style={{ - display: '-webkit-box', + display: "-webkit-box", WebkitLineClamp: 2, - WebkitBoxOrient: 'vertical', - overflow: 'hidden' + WebkitBoxOrient: "vertical", + overflow: "hidden", }} > {StopDataProvider.getDisplayName(stop)} diff --git a/src/frontend/app/components/StopHelpModal.tsx b/src/frontend/app/components/StopHelpModal.tsx index 87e02f9..e8157ab 100644 --- a/src/frontend/app/components/StopHelpModal.tsx +++ b/src/frontend/app/components/StopHelpModal.tsx @@ -21,9 +21,7 @@ export const StopHelpModal: React.FC<StopHelpModalProps> = ({ <Sheet.Content> <div className="p-6 pb-10 flex flex-col gap-8 overflow-y-auto max-h-[80vh] text-slate-900 dark:text-slate-100"> <div> - <h2 className="text-xl font-bold mb-4"> - {t("stop_help.title")} - </h2> + <h2 className="text-xl font-bold mb-4">{t("stop_help.title")}</h2> <div className="space-y-5"> <div className="flex gap-4 items-start"> diff --git a/src/frontend/app/components/StopMapModal.tsx b/src/frontend/app/components/StopMapModal.tsx index 55ad848..1cb6d88 100644 --- a/src/frontend/app/components/StopMapModal.tsx +++ b/src/frontend/app/components/StopMapModal.tsx @@ -1,11 +1,12 @@ import maplibregl from "maplibre-gl"; -import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"; -import Map, { - Layer, - Marker, - Source, - type MapRef -} from "react-map-gl/maplibre"; +import React, { + useCallback, + useEffect, + useMemo, + useRef, + useState, +} from "react"; +import Map, { Layer, Marker, Source, type MapRef } from "react-map-gl/maplibre"; import { Sheet } from "react-modal-sheet"; import { useApp } from "~/AppContext"; import { REGION_DATA } from "~/config/RegionConfig"; @@ -161,7 +162,7 @@ export const StopMapModal: React.FC<StopMapModalProps> = ({ maxZoom: 17, } as any); } - } catch { } + } catch {} }, [stop, selectedBus, shapeData, previousShapeData]); // Load style without traffic layers for the stop map @@ -337,11 +338,7 @@ export const StopMapModal: React.FC<StopMapModalProps> = ({ } return ( - <Sheet - isOpen={isOpen} - onClose={onClose} - detent="content" - > + <Sheet isOpen={isOpen} onClose={onClose} detent="content"> <Sheet.Container style={{ backgroundColor: "var(--background-color)" }}> <Sheet.Header /> <Sheet.Content disableDrag={true}> @@ -358,7 +355,11 @@ export const StopMapModal: React.FC<StopMapModalProps> = ({ }} style={{ width: "100%", height: "50vh" }} mapStyle={styleSpec} - attributionControl={{ compact: false, customAttribution: "Concello de Vigo & Viguesa de Transportes SL" }} + attributionControl={{ + compact: false, + customAttribution: + "Concello de Vigo & Viguesa de Transportes SL", + }} ref={mapRef} interactive={true} onMove={(e) => { diff --git a/src/frontend/app/components/StopSummarySheet.tsx b/src/frontend/app/components/StopSummarySheet.tsx index e85dda3..55cbbd8 100644 --- a/src/frontend/app/components/StopSummarySheet.tsx +++ b/src/frontend/app/components/StopSummarySheet.tsx @@ -102,10 +102,10 @@ export const StopSheet: React.FC<StopSheetProps> = ({ // 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) - ) + (a, b) => + (a.realTime?.minutes ?? a.schedule?.minutes ?? 999) - + (b.realTime?.minutes ?? b.schedule?.minutes ?? 999) + ) : []; const limitedEstimates = sortedData.slice(0, 4); diff --git a/src/frontend/app/components/Stops/ConsolidatedCirculationCard.css b/src/frontend/app/components/Stops/ConsolidatedCirculationCard.css index 9922b03..935c06d 100644 --- a/src/frontend/app/components/Stops/ConsolidatedCirculationCard.css +++ b/src/frontend/app/components/Stops/ConsolidatedCirculationCard.css @@ -1,4 +1,4 @@ -@import '../../tailwind.css'; +@import "../../tailwind.css"; .consolidated-circulation-card { all: unset; @@ -40,7 +40,6 @@ pointer-events: none; } - .consolidated-circulation-card .card-row { display: flex; align-items: center; diff --git a/src/frontend/app/components/Stops/ConsolidatedCirculationCard.tsx b/src/frontend/app/components/Stops/ConsolidatedCirculationCard.tsx index 70a9355..3fa984b 100644 --- a/src/frontend/app/components/Stops/ConsolidatedCirculationCard.tsx +++ b/src/frontend/app/components/Stops/ConsolidatedCirculationCard.tsx @@ -1,5 +1,5 @@ import { useEffect, useMemo, useRef, useState } from "react"; -import Marquee from 'react-fast-marquee'; +import Marquee from "react-fast-marquee"; import { useTranslation } from "react-i18next"; import LineIcon from "~components/LineIcon"; import { type ConsolidatedCirculation } from "~routes/stops-$id"; @@ -109,7 +109,10 @@ const AutoMarquee = ({ text }: { text: string }) => { } return ( - <div ref={containerRef} className="w-full overflow-hidden text-sm font-mono truncate"> + <div + ref={containerRef} + className="w-full overflow-hidden text-sm font-mono truncate" + > {text} </div> ); @@ -175,9 +178,11 @@ export const ConsolidatedCirculationCard: React.FC< const tone = delta <= 2 ? "delay-ok" : delta <= 10 ? "delay-warn" : "delay-critical"; return { - label: reduced ? `R${delta}` : t("estimates.delay_positive", { - minutes: delta, - }), + label: reduced + ? `R${delta}` + : t("estimates.delay_positive", { + minutes: delta, + }), tone, kind: "delay", } as const; @@ -186,22 +191,28 @@ export const ConsolidatedCirculationCard: React.FC< // Early const tone = absDelta <= 2 ? "delay-ok" : "delay-early"; return { - label: reduced ? `A${absDelta}` : t("estimates.delay_negative", { - minutes: absDelta, - }), + label: reduced + ? `A${absDelta}` + : t("estimates.delay_negative", { + minutes: absDelta, + }), tone, kind: "delay", } as const; }, [estimate.schedule, estimate.realTime, t, reduced]); const metaChips = useMemo(() => { - const chips: Array<{ label: string; tone?: string, kind?: "regular" | "gps" | "delay" | "warning" }> = []; + const chips: Array<{ + label: string; + tone?: string; + kind?: "regular" | "gps" | "delay" | "warning"; + }> = []; if (delayChip) { chips.push(delayChip); } - if (estimate.schedule && driver !== 'renfe') { + if (estimate.schedule && driver !== "renfe") { chips.push({ label: `${parseServiceId(estimate.schedule.serviceId)} · ${getTripIdDisplay( estimate.schedule.tripId @@ -211,7 +222,10 @@ export const ConsolidatedCirculationCard: React.FC< } if (estimate.realTime && estimate.realTime.distance >= 0) { - chips.push({ label: formatDistance(estimate.realTime.distance), kind: "regular" }); + chips.push({ + label: formatDistance(estimate.realTime.distance), + kind: "regular", + }); } if (estimate.currentPosition) { @@ -243,7 +257,7 @@ export const ConsolidatedCirculationCard: React.FC< // Check if bus has GPS position (live tracking) const hasGpsPosition = !!estimate.currentPosition; - const isRenfe = driver === 'renfe'; + const isRenfe = driver === "renfe"; const isClickable = hasGpsPosition; const looksDisabled = !isClickable && !isRenfe; @@ -251,10 +265,10 @@ export const ConsolidatedCirculationCard: React.FC< const interactiveProps = readonly ? {} : { - onClick: isClickable ? onMapClick : undefined, - type: "button" as const, - disabled: !isClickable, - }; + onClick: isClickable ? onMapClick : undefined, + type: "button" as const, + disabled: !isClickable, + }; if (reduced) { return ( @@ -263,15 +277,16 @@ export const ConsolidatedCirculationCard: React.FC< flex-none flex items-center gap-2.5 min-h-12 bg-(--message-background-color) border border-(--border-color) rounded-xl px-3 py-2.5 transition-all - ${readonly - ? looksDisabled - ? "opacity-70 cursor-not-allowed" - : "" - : isClickable - ? "cursor-pointer hover:shadow-[0_4px_14px_rgba(0,0,0,0.08)] hover:border-(--button-background-color) hover:bg-[color-mix(in_oklab,var(--button-background-color)_5%,var(--message-background-color))] active:scale-[0.98]" - : looksDisabled + ${ + readonly + ? looksDisabled ? "opacity-70 cursor-not-allowed" : "" + : isClickable + ? "cursor-pointer hover:shadow-[0_4px_14px_rgba(0,0,0,0.08)] hover:border-(--button-background-color) hover:bg-[color-mix(in_oklab,var(--button-background-color)_5%,var(--message-background-color))] active:scale-[0.98]" + : looksDisabled + ? "opacity-70 cursor-not-allowed" + : "" } `.trim()} {...interactiveProps} @@ -281,8 +296,10 @@ export const ConsolidatedCirculationCard: React.FC< </div> <div className="flex-1 min-w-0 flex flex-col gap-1"> <strong className="text-base text-(--text-color) overflow-hidden text-ellipsis line-clamp-2 leading-tight"> - {driver === 'renfe' && estimate.schedule?.tripId && ( - <span className="font-mono text-slate-500 mr-1.5 text-sm">{estimate.schedule.tripId}</span> + {driver === "renfe" && estimate.schedule?.tripId && ( + <span className="font-mono text-slate-500 mr-1.5 text-sm"> + {estimate.schedule.tripId} + </span> )} {estimate.route} </strong> @@ -292,22 +309,28 @@ export const ConsolidatedCirculationCard: React.FC< let chipColourClasses = ""; switch (chip.tone) { case "delay-ok": - chipColourClasses = "bg-green-600/20 dark:bg-green-600/30 text-green-700 dark:text-green-300"; + chipColourClasses = + "bg-green-600/20 dark:bg-green-600/30 text-green-700 dark:text-green-300"; break; case "delay-warn": - chipColourClasses = "bg-amber-600/20 dark:bg-yellow-600/30 text-amber-700 dark:text-yellow-300"; + chipColourClasses = + "bg-amber-600/20 dark:bg-yellow-600/30 text-amber-700 dark:text-yellow-300"; break; case "delay-critical": - chipColourClasses = "bg-red-400/20 dark:bg-red-600/30 text-red-600 dark:text-red-300"; + chipColourClasses = + "bg-red-400/20 dark:bg-red-600/30 text-red-600 dark:text-red-300"; break; case "delay-early": - chipColourClasses = "bg-blue-400/20 dark:bg-blue-600/30 text-blue-700 dark:text-blue-300"; + chipColourClasses = + "bg-blue-400/20 dark:bg-blue-600/30 text-blue-700 dark:text-blue-300"; break; case "warning": - chipColourClasses = "bg-orange-400/20 dark:bg-orange-600/30 text-orange-700 dark:text-orange-300"; + chipColourClasses = + "bg-orange-400/20 dark:bg-orange-600/30 text-orange-700 dark:text-orange-300"; break; default: - chipColourClasses = "bg-black/[0.06] dark:bg-white/[0.12] text-[var(--text-color)]"; + chipColourClasses = + "bg-black/[0.06] dark:bg-white/[0.12] text-[var(--text-color)]"; } return ( @@ -315,8 +338,12 @@ export const ConsolidatedCirculationCard: React.FC< key={`${chip.label}-${idx}`} className={`text-xs px-2 py-0.5 rounded-full flex items-center justify-center gap-1 shrink-0 ${chipColourClasses}`} > - {chip.kind === "gps" && (<LocateIcon className="w-3 h-3 inline-block" />)} - {chip.kind === "warning" && (<AlertTriangle className="w-3 h-3 inline-block" />)} + {chip.kind === "gps" && ( + <LocateIcon className="w-3 h-3 inline-block" /> + )} + {chip.kind === "warning" && ( + <AlertTriangle className="w-3 h-3 inline-block" /> + )} {chip.label} </span> ); @@ -327,17 +354,20 @@ export const ConsolidatedCirculationCard: React.FC< <div className={` inline-flex items-center justify-center px-2 py-1.5 rounded-xl shrink-0 - ${timeClass === "time-running" - ? "bg-green-600/20 dark:bg-green-600/25 text-[#1a9e56] dark:text-[#22c55e]" - : timeClass === "time-delayed" - ? "bg-orange-600/20 dark:bg-orange-600/25 text-[#d06100] dark:text-[#fb923c]" - : "bg-blue-900/20 dark:bg-blue-600/25 text-[#0b3d91] dark:text-[#93c5fd]" + ${ + timeClass === "time-running" + ? "bg-green-600/20 dark:bg-green-600/25 text-[#1a9e56] dark:text-[#22c55e]" + : timeClass === "time-delayed" + ? "bg-orange-600/20 dark:bg-orange-600/25 text-[#d06100] dark:text-[#fb923c]" + : "bg-blue-900/20 dark:bg-blue-600/25 text-[#0b3d91] dark:text-[#93c5fd]" } `.trim()} > <div className="flex flex-col items-center leading-none"> <span className="text-lg font-bold leading-none">{etaValue}</span> - <span className="text-[0.65rem] uppercase tracking-wider mt-0.5 opacity-90">{etaUnit}</span> + <span className="text-[0.65rem] uppercase tracking-wider mt-0.5 opacity-90"> + {etaUnit} + </span> </div> </div> </Tag> @@ -346,16 +376,17 @@ export const ConsolidatedCirculationCard: React.FC< return ( <Tag - className={`consolidated-circulation-card ${readonly - ? looksDisabled - ? "no-gps" - : "" - : isClickable - ? "has-gps" - : looksDisabled + className={`consolidated-circulation-card ${ + readonly + ? looksDisabled ? "no-gps" : "" - }`} + : isClickable + ? "has-gps" + : looksDisabled + ? "no-gps" + : "" + }`} {...interactiveProps} > <> @@ -365,8 +396,10 @@ export const ConsolidatedCirculationCard: React.FC< </div> <div className="route-info"> <strong className="uppercase"> - {driver === 'renfe' && estimate.schedule?.tripId && ( - <span className="font-mono text-slate-500 mr-2 text-[0.9em]">{estimate.schedule.tripId}</span> + {driver === "renfe" && estimate.schedule?.tripId && ( + <span className="font-mono text-slate-500 mr-2 text-[0.9em]"> + {estimate.schedule.tripId} + </span> )} {estimate.route} </strong> @@ -389,8 +422,12 @@ export const ConsolidatedCirculationCard: React.FC< key={`${chip.label}-${idx}`} className={`meta-chip ${chip.tone ?? ""}`.trim()} > - {chip.kind === "gps" && (<LocateIcon className="w-3 h-3 inline-block" />)} - {chip.kind === "warning" && (<AlertTriangle className="w-3 h-3 inline-block" />)} + {chip.kind === "gps" && ( + <LocateIcon className="w-3 h-3 inline-block" /> + )} + {chip.kind === "warning" && ( + <AlertTriangle className="w-3 h-3 inline-block" /> + )} {chip.label} </span> ))} diff --git a/src/frontend/app/components/Stops/ConsolidatedCirculationList.tsx b/src/frontend/app/components/Stops/ConsolidatedCirculationList.tsx index ec79f1c..eea4582 100644 --- a/src/frontend/app/components/Stops/ConsolidatedCirculationList.tsx +++ b/src/frontend/app/components/Stops/ConsolidatedCirculationList.tsx @@ -7,17 +7,17 @@ import "./ConsolidatedCirculationList.css"; interface ConsolidatedCirculationListProps { data: ConsolidatedCirculation[]; - onCirculationClick?: (estimate: ConsolidatedCirculation, index: number) => void; + onCirculationClick?: ( + estimate: ConsolidatedCirculation, + index: number + ) => void; reduced?: boolean; driver?: string; } -export const ConsolidatedCirculationList: React.FC<ConsolidatedCirculationListProps> = ({ - data, - onCirculationClick, - reduced, - driver, -}) => { +export const ConsolidatedCirculationList: React.FC< + ConsolidatedCirculationListProps +> = ({ data, onCirculationClick, reduced, driver }) => { const { t } = useTranslation(); const sortedData = [...data].sort( diff --git a/src/frontend/app/components/ThemeColorManager.tsx b/src/frontend/app/components/ThemeColorManager.tsx index c138dc9..eba0471 100644 --- a/src/frontend/app/components/ThemeColorManager.tsx +++ b/src/frontend/app/components/ThemeColorManager.tsx @@ -9,11 +9,11 @@ export const ThemeColorManager = () => { let meta = document.querySelector('meta[name="theme-color"]'); if (!meta) { - meta = document.createElement('meta'); - meta.setAttribute('name', 'theme-color'); + meta = document.createElement("meta"); + meta.setAttribute("name", "theme-color"); document.head.appendChild(meta); } - meta.setAttribute('content', color); + meta.setAttribute("content", color); }, [resolvedTheme]); return null; diff --git a/src/frontend/app/components/layout/AppShell.tsx b/src/frontend/app/components/layout/AppShell.tsx index 91f6c0d..08aee59 100644 --- a/src/frontend/app/components/layout/AppShell.tsx +++ b/src/frontend/app/components/layout/AppShell.tsx @@ -1,6 +1,9 @@ import React, { useState } from "react"; import { Outlet } from "react-router"; -import { PageTitleProvider, usePageTitleContext } from "~/contexts/PageTitleContext"; +import { + PageTitleProvider, + usePageTitleContext, +} from "~/contexts/PageTitleContext"; import { ThemeColorManager } from "../ThemeColorManager"; import "./AppShell.css"; import { Drawer } from "./Drawer"; diff --git a/src/frontend/app/components/layout/Drawer.css b/src/frontend/app/components/layout/Drawer.css index 27ccce6..4f6bd5f 100644 --- a/src/frontend/app/components/layout/Drawer.css +++ b/src/frontend/app/components/layout/Drawer.css @@ -8,7 +8,9 @@ z-index: 99; opacity: 0; visibility: hidden; - transition: opacity 0.3s ease, visibility 0.3s ease; + transition: + opacity 0.3s ease, + visibility 0.3s ease; } .drawer-overlay.open { diff --git a/src/frontend/app/components/layout/NavBar.tsx b/src/frontend/app/components/layout/NavBar.tsx index 0ac6a71..40591c4 100644 --- a/src/frontend/app/components/layout/NavBar.tsx +++ b/src/frontend/app/components/layout/NavBar.tsx @@ -57,7 +57,7 @@ export default function NavBar({ orientation = "horizontal" }: NavBarProps) { updateMapState(coords, 16); } }, - () => { }, + () => {}, { enableHighAccuracy: false, maximumAge: 5 * 60 * 1000, @@ -70,13 +70,14 @@ export default function NavBar({ orientation = "horizontal" }: NavBarProps) { name: t("navbar.lines", "Líneas"), icon: Route, path: "/lines", - } + }, ]; return ( <nav - className={`${styles.navBar} ${orientation === "vertical" ? styles.vertical : "" - }`} + className={`${styles.navBar} ${ + orientation === "vertical" ? styles.vertical : "" + }`} > {navItems.map((item) => { const Icon = item.icon; diff --git a/src/frontend/app/config/RegionConfig.ts b/src/frontend/app/config/RegionConfig.ts index 43fe70a..75da06d 100644 --- a/src/frontend/app/config/RegionConfig.ts +++ b/src/frontend/app/config/RegionConfig.ts @@ -28,10 +28,7 @@ export const REGION_DATA: RegionData = { consolidatedCirculationsEndpoint: "/api/vigo/GetConsolidatedCirculations", timetableEndpoint: "/api/vigo/GetStopTimetable", shapeEndpoint: "/api/vigo/GetShape", - defaultCenter: [ - 42.229188855975046, - -8.72246955783102 - ] as LngLatLike, + defaultCenter: [42.229188855975046, -8.72246955783102] as LngLatLike, bounds: { sw: [-8.951059, 42.098923] as LngLatLike, ne: [-8.447748, 42.3496] as LngLatLike, @@ -42,4 +39,3 @@ export const REGION_DATA: RegionData = { }; export const getAvailableRegions = (): RegionData[] => [REGION_DATA]; - diff --git a/src/frontend/app/contexts/PageTitleContext.tsx b/src/frontend/app/contexts/PageTitleContext.tsx index 396e409..4a13a8a 100644 --- a/src/frontend/app/contexts/PageTitleContext.tsx +++ b/src/frontend/app/contexts/PageTitleContext.tsx @@ -24,7 +24,9 @@ export const PageTitleProvider: React.FC<{ children: React.ReactNode }> = ({ export const usePageTitleContext = () => { const context = useContext(PageTitleContext); if (!context) { - throw new Error("usePageTitleContext must be used within a PageTitleProvider"); + throw new Error( + "usePageTitleContext must be used within a PageTitleProvider" + ); } return context; }; diff --git a/src/frontend/app/data/LineColors.ts b/src/frontend/app/data/LineColors.ts index 4e5fe8f..d24d870 100644 --- a/src/frontend/app/data/LineColors.ts +++ b/src/frontend/app/data/LineColors.ts @@ -1,4 +1,3 @@ - interface LineColorInfo { background: string; text: string; @@ -61,7 +60,5 @@ export function getLineColour(line: string): LineColorInfo { let formattedLine = /^[a-zA-Z]/.test(line) ? line : `L${line}`; formattedLine = formattedLine.toLowerCase().trim(); - return ( - vigoLineColors[formattedLine.toLowerCase().trim()] ?? defaultLineColor - ); + return vigoLineColors[formattedLine.toLowerCase().trim()] ?? defaultLineColor; } diff --git a/src/frontend/app/data/LinesData.ts b/src/frontend/app/data/LinesData.ts index 13224e6..cd661b3 100644 --- a/src/frontend/app/data/LinesData.ts +++ b/src/frontend/app/data/LinesData.ts @@ -1,7 +1,7 @@ export interface LineInfo { - lineNumber: string; - routeName: string; - scheduleUrl: string; + lineNumber: string; + routeName: string; + scheduleUrl: string; } /** @@ -17,236 +17,240 @@ export interface LineInfo { */ - export const VIGO_LINES: LineInfo[] = [ - { - "lineNumber": "C1", - "routeName": "P.América - C. Castillo - P.Sanz - G.Via - P.América", - "scheduleUrl": "https://vitrasa.es/documents/5893389/6130928/1.pdf" - }, - { - "lineNumber": "C3d", - "routeName": "Bouzas/Coia - E.Fadrique - Encarnación (dereita) - Pza España - Bouzas/Coia", - "scheduleUrl": "https://vitrasa.es/documents/5893389/6130928/3001.pdf" - }, - { - "lineNumber": "C3i", - "routeName": "Bouzas/Coia - Pza España - Encarnación (esquerda) - E.Fadrique - Bouzas/Coia", - "scheduleUrl": "https://vitrasa.es/documents/5893389/6130928/3002.pdf" - }, - { - "lineNumber": "4A", - "routeName": "Coia - Camelias - Centro - Aragón", - "scheduleUrl": "https://vitrasa.es/documents/5893389/6130928/4001.pdf" - }, - { - "lineNumber": "4C", - "routeName": "Coia - Camelias - Centro - M.Garrido - Gregorio Espino", - "scheduleUrl": "https://vitrasa.es/documents/5893389/6130928/4003.pdf" - }, - { - "lineNumber": "5A", - "routeName": "Navia - Florida - L.Mora - Urzaiz - T.Vigo - Teis", - "scheduleUrl": "https://vitrasa.es/documents/5893389/6130928/5001.pdf" - }, - { - "lineNumber": "5B", - "routeName": "Navia - Coia - L.Mora - Pi Margall - G.Barbón - Teis", - "scheduleUrl": "https://vitrasa.es/documents/5893389/6130928/5004.pdf" - }, - { - "lineNumber": "6", - "routeName": "H.Cunqueiro - Beade - Bembrive - Pza. España", - "scheduleUrl": "https://vitrasa.es/documents/5893389/6130928/6.pdf" - }, - { - "lineNumber": "7", - "routeName": "Zamans/Valladares - Fragoso - P.América - P.España - Centro", - "scheduleUrl": "https://vitrasa.es/documents/5893389/6130928/7.pdf" - }, - { - "lineNumber": "9B", - "routeName": "Centro - Choróns - San Cristovo - Rabadeira", - "scheduleUrl": "https://vitrasa.es/documents/5893389/6130928/9002.pdf" - }, - { - "lineNumber": "10", - "routeName": "Teis - G.Barbón - Torrecedeira - Av. Atlántida - Samil - Vao - Saiáns", - "scheduleUrl": "https://vitrasa.es/documents/5893389/6130928/10.pdf" - }, - { - "lineNumber": "11", - "routeName": "San Miguel - Vao - P. América - Urzaiz - Ramón Nieto - Grileira", - "scheduleUrl": "https://vitrasa.es/documents/5893389/6130928/11.pdf" - }, - { - "lineNumber": "12A", - "routeName": "Saiáns - Muiños - Castelao - Pi Margall - P.España - H.Meixoeiro", - "scheduleUrl": "https://vitrasa.es/documents/5893389/6130928/1201.pdf" - }, - { - "lineNumber": "12B", - "routeName": "H.Cunqueiro - Castrelos - Camelias - P.España - H.Meixoeiro", - "scheduleUrl": "https://vitrasa.es/documents/5893389/6130928/1202.pdf" - }, - { - "lineNumber": "13", - "routeName": "Navia - Bouzas - Gran Vía - P.España - H.Meixoeiro", - "scheduleUrl": "https://vitrasa.es/documents/5893389/6130928/13.pdf" - }, - { - "lineNumber": "14", - "routeName": "Gran Vía - Miraflores - Moledo - Chans", - "scheduleUrl": "https://vitrasa.es/documents/5893389/6130928/14.pdf" - }, - { - "lineNumber": "15A", - "routeName": "Av. Ponte - Choróns - Gran Vía - Castelao - Navia - Samil", - "scheduleUrl": "https://vitrasa.es/documents/5893389/6130928/1501.pdf" - }, - { - "lineNumber": "15B", - "routeName": "Xestoso - Choróns - P.Sanz - Beiramar - Bouzas - Samil", - "scheduleUrl": "https://vitrasa.es/documents/5893389/6130928/1506.pdf" - }, - { - "lineNumber": "15C", - "routeName": "CUVI - Choróns - P.Sanz - Torrecedeira - Bouzas - Samil", - "scheduleUrl": "https://vitrasa.es/documents/5893389/6130928/1507.pdf" - }, - { - "lineNumber": "16", - "routeName": "Coia - Balaídos - Zamora - P.España - Colón - Guixar", - "scheduleUrl": "https://vitrasa.es/documents/5893389/6130928/16.pdf" - }, - { - "lineNumber": "17", - "routeName": "Matamá/Freixo - Fragoso - Camelias - G.Barbón - Ríos/A Guía", - "scheduleUrl": "https://vitrasa.es/documents/5893389/6130928/17.pdf" - }, - { - "lineNumber": "18A", - "routeName": "AREAL/COLÓN - SÁRDOMA/POULEIRA", - "scheduleUrl": "https://vitrasa.es/documents/5893389/6130928/18.pdf" - }, - { - "lineNumber": "18B", - "routeName": "URZAIZ / P.ESPAÑA - POULEIRA", - "scheduleUrl": "https://vitrasa.es/documents/5893389/6130928/1801.pdf" - }, - { - "lineNumber": "18H", - "routeName": "URZAIZ / P. ESPAÑA - H. ALV. CUNQUEIRO", - "scheduleUrl": "https://vitrasa.es/documents/5893389/6130928/1802.pdf" - }, - { - "lineNumber": "23", - "routeName": "M. ECHEGARAY - Balaídos - Gran Vía - Choróns - Gregorio Espino", - "scheduleUrl": "https://vitrasa.es/documents/5893389/6130928/23.pdf" - }, - { - "lineNumber": "24", - "routeName": "Poulo - Vía Norte - Colón - Guixar", - "scheduleUrl": "https://vitrasa.es/documents/5893389/6130928/24.pdf" - }, - { - "lineNumber": "25", - "routeName": "PZA. ESPAÑA – SABAXÁNS / CAEIRO", - "scheduleUrl": "https://vitrasa.es/documents/5893389/6130928/25.pdf" - }, - { - "lineNumber": "27", - "routeName": "BEADE (C. CULTURAL) – RABADEIRA", - "scheduleUrl": "https://vitrasa.es/documents/5893389/6130928/27.pdf" - }, - { - "lineNumber": "28", - "routeName": "VIGOZOO - SAN PAIO - BOUZAS", - "scheduleUrl": "https://vitrasa.es/documents/5893389/6130928/28.pdf" - }, - { - "lineNumber": "29", - "routeName": "FRAGOSELO / S. ANDRÉS – PZA. ESPAÑA", - "scheduleUrl": "https://vitrasa.es/documents/5893389/6130928/29.pdf" - }, - { - "lineNumber": "31", - "routeName": "SAN LOURENZO – HOSP. MEIXOEIRO", - "scheduleUrl": "https://vitrasa.es/documents/5893389/6130928/31.pdf" - }, - { - "lineNumber": "A", - "routeName": "ARENAL – PORTO / UNIVERSIDADE", - "scheduleUrl": "https://vitrasa.es/documents/5893389/6130928/8.pdf" - }, - { - "lineNumber": "H", - "routeName": "NAVIA - BOUZAS - HOSPITAL ALVARO CUNQUEIRO", - "scheduleUrl": "https://vitrasa.es/documents/5893389/6130928/104.pdf" - }, - { - "lineNumber": "H1", - "routeName": "POLICARPO SANZ – HOSPITAL ÁLVARO CUNQUEIRO", - "scheduleUrl": "https://vitrasa.es/documents/5893389/6130928/101.pdf" - }, - { - "lineNumber": "H2", - "routeName": "GREGORIO ESPINO – HOSPITAL ÁLVARO CUNQU", - "scheduleUrl": "https://vitrasa.es/documents/5893389/6130928/102.pdf" - }, - { - "lineNumber": "H3", - "routeName": "GARCÍA BARBÓN – HOSPITAL ÁLVARO CUNQUEIRO", - "scheduleUrl": "https://vitrasa.es/documents/5893389/6130928/105.pdf" - }, - { - "lineNumber": "LZD", - "routeName": "STELLANTIS - ALV. CUNQUEIRO", - "scheduleUrl": "https://vitrasa.es/documents/5893389/6130928/751.pdf" - }, - { - "lineNumber": "N1", - "routeName": "SAMIL – BUENOS AIRES", - "scheduleUrl": "https://vitrasa.es/documents/5893389/6130928/30.pdf" - }, - { - "lineNumber": "N4", - "routeName": "NAVIA - G. ESPINO", - "scheduleUrl": "https://vitrasa.es/documents/5893389/6130928/3305.pdf" - }, - { - "lineNumber": "PSA1", - "routeName": "STELLANTIS - G.BARBON", - "scheduleUrl": "https://vitrasa.es/documents/5893389/6130928/301.pdf" - }, - { - "lineNumber": "PSA4", - "routeName": "STELLANTIS - G. BARBON", - "scheduleUrl": "https://vitrasa.es/documents/5893389/6130928/4004.pdf" - }, - { - "lineNumber": "PTL", - "routeName": "PARQUE TECNOLÓXICO", - "scheduleUrl": "https://vitrasa.es/documents/5893389/6130928/304.pdf" - }, - { - "lineNumber": "TUR", - "routeName": "TURISTICO", - "scheduleUrl": "https://vitrasa.es/documents/5893389/6130928/500.pdf" - }, - { - "lineNumber": "U1", - "routeName": "LANZADEIRA PZA. AMÉRICA – UNIVERSIDADE", - "scheduleUrl": "https://vitrasa.es/documents/5893389/6130928/201.pdf" - }, - { - "lineNumber": "U2", - "routeName": "LANZADEIRA PZA. DE ESPAÑA – UNIVERSIDADE", - "scheduleUrl": "https://vitrasa.es/documents/5893389/6130928/202.pdf" - }, - { - "lineNumber": "VTS", - "routeName": "CABRAL - BASE", - "scheduleUrl": "https://vitrasa.es/documents/5893389/6130928/3010.pdf" - } + { + lineNumber: "C1", + routeName: "P.América - C. Castillo - P.Sanz - G.Via - P.América", + scheduleUrl: "https://vitrasa.es/documents/5893389/6130928/1.pdf", + }, + { + lineNumber: "C3d", + routeName: + "Bouzas/Coia - E.Fadrique - Encarnación (dereita) - Pza España - Bouzas/Coia", + scheduleUrl: "https://vitrasa.es/documents/5893389/6130928/3001.pdf", + }, + { + lineNumber: "C3i", + routeName: + "Bouzas/Coia - Pza España - Encarnación (esquerda) - E.Fadrique - Bouzas/Coia", + scheduleUrl: "https://vitrasa.es/documents/5893389/6130928/3002.pdf", + }, + { + lineNumber: "4A", + routeName: "Coia - Camelias - Centro - Aragón", + scheduleUrl: "https://vitrasa.es/documents/5893389/6130928/4001.pdf", + }, + { + lineNumber: "4C", + routeName: "Coia - Camelias - Centro - M.Garrido - Gregorio Espino", + scheduleUrl: "https://vitrasa.es/documents/5893389/6130928/4003.pdf", + }, + { + lineNumber: "5A", + routeName: "Navia - Florida - L.Mora - Urzaiz - T.Vigo - Teis", + scheduleUrl: "https://vitrasa.es/documents/5893389/6130928/5001.pdf", + }, + { + lineNumber: "5B", + routeName: "Navia - Coia - L.Mora - Pi Margall - G.Barbón - Teis", + scheduleUrl: "https://vitrasa.es/documents/5893389/6130928/5004.pdf", + }, + { + lineNumber: "6", + routeName: "H.Cunqueiro - Beade - Bembrive - Pza. España", + scheduleUrl: "https://vitrasa.es/documents/5893389/6130928/6.pdf", + }, + { + lineNumber: "7", + routeName: "Zamans/Valladares - Fragoso - P.América - P.España - Centro", + scheduleUrl: "https://vitrasa.es/documents/5893389/6130928/7.pdf", + }, + { + lineNumber: "9B", + routeName: "Centro - Choróns - San Cristovo - Rabadeira", + scheduleUrl: "https://vitrasa.es/documents/5893389/6130928/9002.pdf", + }, + { + lineNumber: "10", + routeName: + "Teis - G.Barbón - Torrecedeira - Av. Atlántida - Samil - Vao - Saiáns", + scheduleUrl: "https://vitrasa.es/documents/5893389/6130928/10.pdf", + }, + { + lineNumber: "11", + routeName: + "San Miguel - Vao - P. América - Urzaiz - Ramón Nieto - Grileira", + scheduleUrl: "https://vitrasa.es/documents/5893389/6130928/11.pdf", + }, + { + lineNumber: "12A", + routeName: + "Saiáns - Muiños - Castelao - Pi Margall - P.España - H.Meixoeiro", + scheduleUrl: "https://vitrasa.es/documents/5893389/6130928/1201.pdf", + }, + { + lineNumber: "12B", + routeName: "H.Cunqueiro - Castrelos - Camelias - P.España - H.Meixoeiro", + scheduleUrl: "https://vitrasa.es/documents/5893389/6130928/1202.pdf", + }, + { + lineNumber: "13", + routeName: "Navia - Bouzas - Gran Vía - P.España - H.Meixoeiro", + scheduleUrl: "https://vitrasa.es/documents/5893389/6130928/13.pdf", + }, + { + lineNumber: "14", + routeName: "Gran Vía - Miraflores - Moledo - Chans", + scheduleUrl: "https://vitrasa.es/documents/5893389/6130928/14.pdf", + }, + { + lineNumber: "15A", + routeName: "Av. Ponte - Choróns - Gran Vía - Castelao - Navia - Samil", + scheduleUrl: "https://vitrasa.es/documents/5893389/6130928/1501.pdf", + }, + { + lineNumber: "15B", + routeName: "Xestoso - Choróns - P.Sanz - Beiramar - Bouzas - Samil", + scheduleUrl: "https://vitrasa.es/documents/5893389/6130928/1506.pdf", + }, + { + lineNumber: "15C", + routeName: "CUVI - Choróns - P.Sanz - Torrecedeira - Bouzas - Samil", + scheduleUrl: "https://vitrasa.es/documents/5893389/6130928/1507.pdf", + }, + { + lineNumber: "16", + routeName: "Coia - Balaídos - Zamora - P.España - Colón - Guixar", + scheduleUrl: "https://vitrasa.es/documents/5893389/6130928/16.pdf", + }, + { + lineNumber: "17", + routeName: "Matamá/Freixo - Fragoso - Camelias - G.Barbón - Ríos/A Guía", + scheduleUrl: "https://vitrasa.es/documents/5893389/6130928/17.pdf", + }, + { + lineNumber: "18A", + routeName: "AREAL/COLÓN - SÁRDOMA/POULEIRA", + scheduleUrl: "https://vitrasa.es/documents/5893389/6130928/18.pdf", + }, + { + lineNumber: "18B", + routeName: "URZAIZ / P.ESPAÑA - POULEIRA", + scheduleUrl: "https://vitrasa.es/documents/5893389/6130928/1801.pdf", + }, + { + lineNumber: "18H", + routeName: "URZAIZ / P. ESPAÑA - H. ALV. CUNQUEIRO", + scheduleUrl: "https://vitrasa.es/documents/5893389/6130928/1802.pdf", + }, + { + lineNumber: "23", + routeName: "M. ECHEGARAY - Balaídos - Gran Vía - Choróns - Gregorio Espino", + scheduleUrl: "https://vitrasa.es/documents/5893389/6130928/23.pdf", + }, + { + lineNumber: "24", + routeName: "Poulo - Vía Norte - Colón - Guixar", + scheduleUrl: "https://vitrasa.es/documents/5893389/6130928/24.pdf", + }, + { + lineNumber: "25", + routeName: "PZA. ESPAÑA – SABAXÁNS / CAEIRO", + scheduleUrl: "https://vitrasa.es/documents/5893389/6130928/25.pdf", + }, + { + lineNumber: "27", + routeName: "BEADE (C. CULTURAL) – RABADEIRA", + scheduleUrl: "https://vitrasa.es/documents/5893389/6130928/27.pdf", + }, + { + lineNumber: "28", + routeName: "VIGOZOO - SAN PAIO - BOUZAS", + scheduleUrl: "https://vitrasa.es/documents/5893389/6130928/28.pdf", + }, + { + lineNumber: "29", + routeName: "FRAGOSELO / S. ANDRÉS – PZA. ESPAÑA", + scheduleUrl: "https://vitrasa.es/documents/5893389/6130928/29.pdf", + }, + { + lineNumber: "31", + routeName: "SAN LOURENZO – HOSP. MEIXOEIRO", + scheduleUrl: "https://vitrasa.es/documents/5893389/6130928/31.pdf", + }, + { + lineNumber: "A", + routeName: "ARENAL – PORTO / UNIVERSIDADE", + scheduleUrl: "https://vitrasa.es/documents/5893389/6130928/8.pdf", + }, + { + lineNumber: "H", + routeName: "NAVIA - BOUZAS - HOSPITAL ALVARO CUNQUEIRO", + scheduleUrl: "https://vitrasa.es/documents/5893389/6130928/104.pdf", + }, + { + lineNumber: "H1", + routeName: "POLICARPO SANZ – HOSPITAL ÁLVARO CUNQUEIRO", + scheduleUrl: "https://vitrasa.es/documents/5893389/6130928/101.pdf", + }, + { + lineNumber: "H2", + routeName: "GREGORIO ESPINO – HOSPITAL ÁLVARO CUNQU", + scheduleUrl: "https://vitrasa.es/documents/5893389/6130928/102.pdf", + }, + { + lineNumber: "H3", + routeName: "GARCÍA BARBÓN – HOSPITAL ÁLVARO CUNQUEIRO", + scheduleUrl: "https://vitrasa.es/documents/5893389/6130928/105.pdf", + }, + { + lineNumber: "LZD", + routeName: "STELLANTIS - ALV. CUNQUEIRO", + scheduleUrl: "https://vitrasa.es/documents/5893389/6130928/751.pdf", + }, + { + lineNumber: "N1", + routeName: "SAMIL – BUENOS AIRES", + scheduleUrl: "https://vitrasa.es/documents/5893389/6130928/30.pdf", + }, + { + lineNumber: "N4", + routeName: "NAVIA - G. ESPINO", + scheduleUrl: "https://vitrasa.es/documents/5893389/6130928/3305.pdf", + }, + { + lineNumber: "PSA1", + routeName: "STELLANTIS - G.BARBON", + scheduleUrl: "https://vitrasa.es/documents/5893389/6130928/301.pdf", + }, + { + lineNumber: "PSA4", + routeName: "STELLANTIS - G. BARBON", + scheduleUrl: "https://vitrasa.es/documents/5893389/6130928/4004.pdf", + }, + { + lineNumber: "PTL", + routeName: "PARQUE TECNOLÓXICO", + scheduleUrl: "https://vitrasa.es/documents/5893389/6130928/304.pdf", + }, + { + lineNumber: "TUR", + routeName: "TURISTICO", + scheduleUrl: "https://vitrasa.es/documents/5893389/6130928/500.pdf", + }, + { + lineNumber: "U1", + routeName: "LANZADEIRA PZA. AMÉRICA – UNIVERSIDADE", + scheduleUrl: "https://vitrasa.es/documents/5893389/6130928/201.pdf", + }, + { + lineNumber: "U2", + routeName: "LANZADEIRA PZA. DE ESPAÑA – UNIVERSIDADE", + scheduleUrl: "https://vitrasa.es/documents/5893389/6130928/202.pdf", + }, + { + lineNumber: "VTS", + routeName: "CABRAL - BASE", + scheduleUrl: "https://vitrasa.es/documents/5893389/6130928/3010.pdf", + }, ]; diff --git a/src/frontend/app/data/StopDataProvider.ts b/src/frontend/app/data/StopDataProvider.ts index 920c7e1..e523bd1 100644 --- a/src/frontend/app/data/StopDataProvider.ts +++ b/src/frontend/app/data/StopDataProvider.ts @@ -12,7 +12,7 @@ export type StopName = { export interface Stop { stopId: string; - type?: 'bus' | 'train'; + type?: "bus" | "train"; name: StopName; latitude?: number; longitude?: number; @@ -35,7 +35,7 @@ const customNamesByRegion: Record<string, Record<string, string>> = {}; // Helper to normalize ID function normalizeId(id: number | string): string { const s = String(id); - if (s.includes(':')) return s; + if (s.includes(":")) return s; return `vitrasa:${s}`; } @@ -52,8 +52,8 @@ async function initStops() { const entry = { ...raw, stopId: id, - type: raw.type || (id.startsWith('renfe:') ? 'train' : 'bus'), - favourite: false + type: raw.type || (id.startsWith("renfe:") ? "train" : "bus"), + favourite: false, } as Stop; stopsMapByRegion[REGION_DATA.id][id] = entry; return entry; @@ -65,7 +65,7 @@ async function initStops() { const parsed = JSON.parse(rawCustom); const normalized: Record<string, string> = {}; for (const [key, value] of Object.entries(parsed)) { - normalized[normalizeId(key)] = value as string; + normalized[normalizeId(key)] = value as string; } customNamesByRegion[REGION_DATA.id] = normalized; } else { @@ -78,7 +78,9 @@ async function getStops(): Promise<Stop[]> { await initStops(); // update favourites const rawFav = localStorage.getItem("favouriteStops_vigo"); - const favouriteStops = rawFav ? (JSON.parse(rawFav) as (number|string)[]).map(normalizeId) : []; + const favouriteStops = rawFav + ? (JSON.parse(rawFav) as (number | string)[]).map(normalizeId) + : []; cachedStopsByRegion["vigo"]!.forEach( (stop) => (stop.favourite = favouriteStops.includes(stop.stopId)) @@ -87,15 +89,15 @@ async function getStops(): Promise<Stop[]> { } // New: get single stop by id -async function getStopById( - stopId: string | number -): Promise<Stop | undefined> { +async function getStopById(stopId: string | number): Promise<Stop | undefined> { await initStops(); const id = normalizeId(stopId); const stop = stopsMapByRegion[REGION_DATA.id]?.[id]; if (stop) { const rawFav = localStorage.getItem(`favouriteStops_${REGION_DATA.id}`); - const favouriteStops = rawFav ? (JSON.parse(rawFav) as (number|string)[]).map(normalizeId) : []; + const favouriteStops = rawFav + ? (JSON.parse(rawFav) as (number | string)[]).map(normalizeId) + : []; stop.favourite = favouriteStops.includes(id); } return stop; @@ -144,15 +146,14 @@ function addFavourite(stopId: string | number) { const rawFavouriteStops = localStorage.getItem(`favouriteStops_vigo`); let favouriteStops: string[] = []; if (rawFavouriteStops) { - favouriteStops = (JSON.parse(rawFavouriteStops) as (number|string)[]).map(normalizeId); + favouriteStops = (JSON.parse(rawFavouriteStops) as (number | string)[]).map( + normalizeId + ); } if (!favouriteStops.includes(id)) { favouriteStops.push(id); - localStorage.setItem( - `favouriteStops_vigo`, - JSON.stringify(favouriteStops) - ); + localStorage.setItem(`favouriteStops_vigo`, JSON.stringify(favouriteStops)); } } @@ -161,7 +162,9 @@ function removeFavourite(stopId: string | number) { const rawFavouriteStops = localStorage.getItem(`favouriteStops_vigo`); let favouriteStops: string[] = []; if (rawFavouriteStops) { - favouriteStops = (JSON.parse(rawFavouriteStops) as (number|string)[]).map(normalizeId); + favouriteStops = (JSON.parse(rawFavouriteStops) as (number | string)[]).map( + normalizeId + ); } const newFavouriteStops = favouriteStops.filter((sid) => sid !== id); @@ -175,7 +178,9 @@ function isFavourite(stopId: string | number): boolean { const id = normalizeId(stopId); const rawFavouriteStops = localStorage.getItem(`favouriteStops_vigo`); if (rawFavouriteStops) { - const favouriteStops = (JSON.parse(rawFavouriteStops) as (number|string)[]).map(normalizeId); + const favouriteStops = ( + JSON.parse(rawFavouriteStops) as (number | string)[] + ).map(normalizeId); return favouriteStops.includes(id); } return false; @@ -188,7 +193,9 @@ function pushRecent(stopId: string | number) { const rawRecentStops = localStorage.getItem(`recentStops_vigo`); let recentStops: Set<string> = new Set(); if (rawRecentStops) { - recentStops = new Set((JSON.parse(rawRecentStops) as (number|string)[]).map(normalizeId)); + recentStops = new Set( + (JSON.parse(rawRecentStops) as (number | string)[]).map(normalizeId) + ); } recentStops.add(id); @@ -207,7 +214,7 @@ function pushRecent(stopId: string | number) { function getRecent(): string[] { const rawRecentStops = localStorage.getItem(`recentStops_vigo`); if (rawRecentStops) { - return (JSON.parse(rawRecentStops) as (number|string)[]).map(normalizeId); + return (JSON.parse(rawRecentStops) as (number | string)[]).map(normalizeId); } return []; } @@ -215,7 +222,9 @@ function getRecent(): string[] { function getFavouriteIds(): string[] { const rawFavouriteStops = localStorage.getItem(`favouriteStops_vigo`); if (rawFavouriteStops) { - return (JSON.parse(rawFavouriteStops) as (number|string)[]).map(normalizeId); + return (JSON.parse(rawFavouriteStops) as (number | string)[]).map( + normalizeId + ); } return []; } @@ -225,13 +234,13 @@ async function loadStopsFromNetwork(): Promise<Stop[]> { const response = await fetch(REGION_DATA.stopsEndpoint); const rawStops = (await response.json()) as any[]; return rawStops.map((raw) => { - const id = normalizeId(raw.stopId); - return { - ...raw, - stopId: id, - type: raw.type || (id.startsWith('renfe:') ? 'train' : 'bus'), - favourite: false - } as Stop; + const id = normalizeId(raw.stopId); + return { + ...raw, + stopId: id, + type: raw.type || (id.startsWith("renfe:") ? "train" : "bus"), + favourite: false, + } as Stop; }); } diff --git a/src/frontend/app/root.css b/src/frontend/app/root.css index 72eecff..367fa29 100644 --- a/src/frontend/app/root.css +++ b/src/frontend/app/root.css @@ -45,7 +45,9 @@ --error-message-color: #7f8c8d; color-scheme: light; - font-family: ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji"; + font-family: + ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", + "Segoe UI Symbol", "Noto Color Emoji"; } [data-theme="dark"] { diff --git a/src/frontend/app/root.tsx b/src/frontend/app/root.tsx index 7bf07a0..9c105f0 100644 --- a/src/frontend/app/root.tsx +++ b/src/frontend/app/root.tsx @@ -3,7 +3,7 @@ import { Links, Meta, Scripts, - ScrollRestoration + ScrollRestoration, } from "react-router"; import "@fontsource-variable/roboto"; diff --git a/src/frontend/app/routes/about.tsx b/src/frontend/app/routes/about.tsx index 1354e8f..5158330 100644 --- a/src/frontend/app/routes/about.tsx +++ b/src/frontend/app/routes/about.tsx @@ -1,6 +1,6 @@ import { useTranslation } from "react-i18next"; import { usePageTitle } from "~/contexts/PageTitleContext"; -import '../tailwind-full.css'; +import "../tailwind-full.css"; export default function About() { const { t } = useTranslation(); @@ -37,7 +37,9 @@ export default function About() { <strong className="text-[--text-color] min-w-fit"> {t("about.data_realtime")}: </strong> - <span className="opacity-80">{t("about.data_realtime_source")}</span> + <span className="opacity-80"> + {t("about.data_realtime_source")} + </span> </li> <li className="flex flex-col sm:flex-row sm:items-start gap-1"> <strong className="text-[--text-color] min-w-fit"> @@ -89,9 +91,7 @@ export default function About() { </div> <div className="mt-6 p-4 bg-blue-50 dark:bg-blue-950/30 border border-blue-200 dark:border-blue-800 rounded-lg"> - <p className="text-sm leading-relaxed"> - {t("about.thanks_council")} - </p> + <p className="text-sm leading-relaxed">{t("about.thanks_council")}</p> </div> </section> @@ -119,8 +119,17 @@ export default function About() { rel="nofollow noreferrer noopener" target="_blank" > - <svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true"> - <path fillRule="evenodd" d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z" clipRule="evenodd" /> + <svg + className="w-5 h-5" + fill="currentColor" + viewBox="0 0 24 24" + aria-hidden="true" + > + <path + fillRule="evenodd" + d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z" + clipRule="evenodd" + /> </svg> GitHub </a> diff --git a/src/frontend/app/routes/home.tsx b/src/frontend/app/routes/home.tsx index f97fdf7..7c13da6 100644 --- a/src/frontend/app/routes/home.tsx +++ b/src/frontend/app/routes/home.tsx @@ -217,7 +217,9 @@ export default function StopList() { if (isNumericSearch) { // Direct match for stop codes const stopId = searchQuery.trim(); - const exactMatch = data.filter((stop) => stop.stopId === stopId || stop.stopId.endsWith(`:${stopId}`)); + const exactMatch = data.filter( + (stop) => stop.stopId === stopId || stop.stopId.endsWith(`:${stopId}`) + ); if (exactMatch.length > 0) { items = exactMatch; } else { @@ -281,7 +283,9 @@ export default function StopList() { {/* Favourites Gallery */} {!loading && ( <StopGallery - stops={favouriteStops.sort((a, b) => a.stopId.localeCompare(b.stopId))} + stops={favouriteStops.sort((a, b) => + a.stopId.localeCompare(b.stopId) + )} title={t("stoplist.favourites")} emptyMessage={t("stoplist.no_favourites")} /> @@ -301,9 +305,24 @@ export default function StopList() { <div className="w-full px-4 flex flex-col gap-2"> <div className="flex items-center gap-2"> {userLocation && ( - <svg className="w-5 h-5 text-blue-600 dark:text-blue-400" fill="none" viewBox="0 0 24 24" stroke="currentColor"> - <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z" /> - <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 11a3 3 0 11-6 0 3 3 0 016 0z" /> + <svg + className="w-5 h-5 text-blue-600 dark:text-blue-400" + fill="none" + viewBox="0 0 24 24" + stroke="currentColor" + > + <path + strokeLinecap="round" + strokeLinejoin="round" + strokeWidth={2} + d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z" + /> + <path + strokeLinecap="round" + strokeLinejoin="round" + strokeWidth={2} + d="M15 11a3 3 0 11-6 0 3 3 0 016 0z" + /> </svg> )} <h2 className="text-lg font-semibold text-gray-900 dark:text-gray-100"> @@ -322,9 +341,10 @@ export default function StopList() { </> )} {!loading && data - ? (userLocation ? sortedAllStops.slice(0, 6) : sortedAllStops).map( - (stop) => <StopItem key={stop.stopId} stop={stop} /> - ) + ? (userLocation + ? sortedAllStops.slice(0, 6) + : sortedAllStops + ).map((stop) => <StopItem key={stop.stopId} stop={stop} />) : null} </ul> </div> diff --git a/src/frontend/app/routes/lines.tsx b/src/frontend/app/routes/lines.tsx index 658716f..acf8a7f 100644 --- a/src/frontend/app/routes/lines.tsx +++ b/src/frontend/app/routes/lines.tsx @@ -2,36 +2,39 @@ import { useTranslation } from "react-i18next"; import LineIcon from "~/components/LineIcon"; import { usePageTitle } from "~/contexts/PageTitleContext"; import { VIGO_LINES } from "~/data/LinesData"; -import '../tailwind-full.css'; +import "../tailwind-full.css"; export default function LinesPage() { - const { t } = useTranslation(); - usePageTitle(t("navbar.lines", "Líneas")); + const { t } = useTranslation(); + usePageTitle(t("navbar.lines", "Líneas")); - return ( - <div className="container mx-auto px-4 py-6"> - <p className="mb-6 text-gray-700 dark:text-gray-300"> - {t("lines.description", "A continuación se muestra una lista de las líneas de autobús urbano de Vigo con sus respectivas rutas y enlaces a los horarios oficiales.")} - </p> + return ( + <div className="container mx-auto px-4 py-6"> + <p className="mb-6 text-gray-700 dark:text-gray-300"> + {t( + "lines.description", + "A continuación se muestra una lista de las líneas de autobús urbano de Vigo con sus respectivas rutas y enlaces a los horarios oficiales." + )} + </p> - <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"> - {VIGO_LINES.map((line) => ( - <a - key={line.lineNumber} - href={line.scheduleUrl} - target="_blank" - rel="noopener noreferrer" - className="flex items-center gap-3 p-4 bg-white dark:bg-gray-800 rounded-lg shadow hover:shadow-lg transition-shadow border border-gray-200 dark:border-gray-700" - > - <LineIcon line={line.lineNumber} mode="rounded" /> - <div className="flex-1 min-w-0"> - <p className="text-sm md:text-md font-semibold text-gray-900 dark:text-gray-100"> - {line.routeName} - </p> - </div> - </a> - ))} + <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"> + {VIGO_LINES.map((line) => ( + <a + key={line.lineNumber} + href={line.scheduleUrl} + target="_blank" + rel="noopener noreferrer" + className="flex items-center gap-3 p-4 bg-white dark:bg-gray-800 rounded-lg shadow hover:shadow-lg transition-shadow border border-gray-200 dark:border-gray-700" + > + <LineIcon line={line.lineNumber} mode="rounded" /> + <div className="flex-1 min-w-0"> + <p className="text-sm md:text-md font-semibold text-gray-900 dark:text-gray-100"> + {line.routeName} + </p> </div> - </div> - ); + </a> + ))} + </div> + </div> + ); } diff --git a/src/frontend/app/routes/map.tsx b/src/frontend/app/routes/map.tsx index 402bf60..187e9f2 100644 --- a/src/frontend/app/routes/map.tsx +++ b/src/frontend/app/routes/map.tsx @@ -12,7 +12,7 @@ import Map, { Source, type MapLayerMouseEvent, type MapRef, - type StyleSpecification + type StyleSpecification, } from "react-map-gl/maplibre"; import { StopSheet } from "~/components/StopSummarySheet"; import { REGION_DATA } from "~/config/RegionConfig"; @@ -35,7 +35,13 @@ export default function StopMap() { const [stops, setStops] = useState< GeoJsonFeature< Point, - { stopId: string; name: string; lines: string[]; cancelled?: boolean, prefix: string } + { + stopId: string; + name: string; + lines: string[]; + cancelled?: boolean; + prefix: string; + } >[] >([]); const [selectedStop, setSelectedStop] = useState<Stop | null>(null); @@ -51,7 +57,12 @@ export default function StopMap() { const onMapClick = (e: MapLayerMouseEvent) => { const features = e.features; if (!features || features.length === 0) { - console.debug("No features found on map click. Position:", e.lngLat, "Point:", e.point); + console.debug( + "No features found on map click. Position:", + e.lngLat, + "Point:", + e.point + ); return; } const feature = features[0]; @@ -65,7 +76,13 @@ export default function StopMap() { StopDataProvider.getStops().then((data) => { const features: GeoJsonFeature< Point, - { stopId: string; name: string; lines: string[]; cancelled?: boolean, prefix: string } + { + stopId: string; + name: string; + lines: string[]; + cancelled?: boolean; + prefix: string; + } >[] = data.map((s) => ({ type: "Feature", geometry: { @@ -77,7 +94,11 @@ export default function StopMap() { name: s.name.original, lines: s.lines, cancelled: s.cancelled ?? false, - prefix: s.stopId.startsWith("renfe:") ? "stop-renfe" : (s.cancelled ? "stop-vitrasa-cancelled" : "stop-vitrasa"), + prefix: s.stopId.startsWith("renfe:") + ? "stop-renfe" + : s.cancelled + ? "stop-vitrasa-cancelled" + : "stop-vitrasa", }, })); setStops(features); @@ -190,7 +211,11 @@ export default function StopMap() { maxBounds={[REGION_DATA.bounds.sw, REGION_DATA.bounds.ne]} > <NavigationControl position="top-right" /> - <GeolocateControl position="top-right" trackUserLocation={true} positionOptions={{ enableHighAccuracy: false }} /> + <GeolocateControl + position="top-right" + trackUserLocation={true} + positionOptions={{ enableHighAccuracy: false }} + /> <Source id="stops-source" @@ -204,10 +229,7 @@ export default function StopMap() { minzoom={11} source="stops-source" layout={{ - "icon-image": [ - "get", - "prefix" - ], + "icon-image": ["get", "prefix"], "icon-size": [ "interpolate", ["linear"], @@ -242,22 +264,20 @@ export default function StopMap() { "case", ["==", ["get", "prefix"], "stop-renfe"], "#870164", - "#e72b37" + "#e72b37", ], "text-halo-color": "#FFF", "text-halo-width": 1, }} /> - { - selectedStop && ( - <StopSheet - isOpen={isSheetOpen} - onClose={() => setIsSheetOpen(false)} - stop={selectedStop} - /> - ) - } - </Map > + {selectedStop && ( + <StopSheet + isOpen={isSheetOpen} + onClose={() => setIsSheetOpen(false)} + stop={selectedStop} + /> + )} + </Map> ); } diff --git a/src/frontend/app/routes/settings.tsx b/src/frontend/app/routes/settings.tsx index 9b4625f..56df777 100644 --- a/src/frontend/app/routes/settings.tsx +++ b/src/frontend/app/routes/settings.tsx @@ -2,22 +2,29 @@ import { Computer, Moon, Sun } from "lucide-react"; import { useTranslation } from "react-i18next"; import { usePageTitle } from "~/contexts/PageTitleContext"; import { useApp, type Theme } from "../AppContext"; -import '../tailwind-full.css'; +import "../tailwind-full.css"; export default function Settings() { const { t, i18n } = useTranslation(); usePageTitle(t("navbar.settings", "Ajustes")); - const { - theme, - setTheme, - mapPositionMode, - setMapPositionMode - } = useApp(); + const { theme, setTheme, mapPositionMode, setMapPositionMode } = useApp(); const THEMES = [ - { value: "light" as Theme, label: t("about.theme_light", "Claro"), icon: Sun }, - { value: "dark" as Theme, label: t("about.theme_dark", "Oscuro"), icon: Moon }, - { value: "system" as Theme, label: t("about.theme_system", "Sistema"), icon: Computer }, + { + value: "light" as Theme, + label: t("about.theme_light", "Claro"), + icon: Sun, + }, + { + value: "dark" as Theme, + label: t("about.theme_dark", "Oscuro"), + icon: Moon, + }, + { + value: "system" as Theme, + label: t("about.theme_system", "Sistema"), + icon: Computer, + }, ]; return ( @@ -37,9 +44,10 @@ export default function Settings() { rounded-lg border-2 transition-all duration-200 hover:bg-gray-50 dark:hover:bg-gray-800 focus:outline-none focus:ring focus:ring-blue-500 dark:focus:ring-offset-gray-900 - ${value === theme - ? "border-blue-600 bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-400 font-semibold" - : "border-gray-300 dark:border-gray-700 text-gray-700 dark:text-gray-300" + ${ + value === theme + ? "border-blue-600 bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-400 font-semibold" + : "border-gray-300 dark:border-gray-700 text-gray-700 dark:text-gray-300" } `} > diff --git a/src/frontend/app/routes/stops-$id.tsx b/src/frontend/app/routes/stops-$id.tsx index 553b8e7..6d06215 100644 --- a/src/frontend/app/routes/stops-$id.tsx +++ b/src/frontend/app/routes/stops-$id.tsx @@ -198,9 +198,7 @@ export default function Estimates() { loadData(); StopDataProvider.pushRecent(stopId); - setFavourited( - StopDataProvider.isFavourite(stopId) - ); + setFavourited(StopDataProvider.isFavourite(stopId)); setDataLoading(false); }, [stopId, loadData]); @@ -246,34 +244,48 @@ export default function Estimates() { <div className="flex items-center justify-between py-2"> <div className="flex items-center gap-8"> <Star - className={`cursor-pointer transition-colors ${favourited - ? "fill-[var(--star-color)] text-[var(--star-color)]" - : "text-slate-500" - }`} + className={`cursor-pointer transition-colors ${ + favourited + ? "fill-[var(--star-color)] text-[var(--star-color)]" + : "text-slate-500" + }`} onClick={toggleFavourite} /> - <CircleHelp className="text-slate-500 cursor-pointer" onClick={() => setIsHelpModalOpen(true)} /> + <CircleHelp + className="text-slate-500 cursor-pointer" + onClick={() => setIsHelpModalOpen(true)} + /> </div> <div className="consolidated-circulation-caption"> - {t("estimates.caption", "Estimaciones de llegadas a las {{time}}", { - time: dataDate?.toLocaleTimeString(), - })} + {t( + "estimates.caption", + "Estimaciones de llegadas a las {{time}}", + { + time: dataDate?.toLocaleTimeString(), + } + )} </div> <div> {isReducedView ? ( - <EyeClosed className="text-slate-500" onClick={() => setIsReducedView(false)} /> + <EyeClosed + className="text-slate-500" + onClick={() => setIsReducedView(false)} + /> ) : ( - <Eye className="text-slate-500" onClick={() => setIsReducedView(true)} /> + <Eye + className="text-slate-500" + onClick={() => setIsReducedView(true)} + /> )} </div> </div> <ConsolidatedCirculationList data={data} reduced={isReducedView} - driver={stopData?.stopId.split(':')[0]} + driver={stopData?.stopId.split(":")[0]} onCirculationClick={(estimate, idx) => { setSelectedCirculationId(getCirculationId(estimate)); setIsMapModalOpen(true); @@ -295,8 +307,8 @@ export default function Estimates() { previousTripShapeId: c.previousTripShapeId, schedule: c.schedule ? { - shapeId: c.schedule.shapeId, - } + shapeId: c.schedule.shapeId, + } : undefined, }))} isOpen={isMapModalOpen} diff --git a/src/frontend/vite.config.ts b/src/frontend/vite.config.ts index b827847..042177d 100644 --- a/src/frontend/vite.config.ts +++ b/src/frontend/vite.config.ts @@ -11,11 +11,7 @@ export default defineConfig({ define: { __COMMIT_HASH__: JSON.stringify(commitHash), }, - plugins: [ - reactRouter(), - tsconfigPaths(), - tailwindcss() - ], + plugins: [reactRouter(), tsconfigPaths(), tailwindcss()], server: { proxy: { "^/api": { |
