diff options
Diffstat (limited to 'src/frontend/app')
| -rw-r--r-- | src/frontend/app/components/StopGallery.tsx | 5 | ||||
| -rw-r--r-- | src/frontend/app/components/layout/NavBar.tsx | 14 | ||||
| -rw-r--r-- | src/frontend/app/data/LinesData.ts | 252 | ||||
| -rw-r--r-- | src/frontend/app/i18n/locales/en-GB.json | 4 | ||||
| -rw-r--r-- | src/frontend/app/i18n/locales/es-ES.json | 4 | ||||
| -rw-r--r-- | src/frontend/app/i18n/locales/gl-ES.json | 4 | ||||
| -rw-r--r-- | src/frontend/app/routes.tsx | 1 | ||||
| -rw-r--r-- | src/frontend/app/routes/home.tsx | 106 | ||||
| -rw-r--r-- | src/frontend/app/routes/lines.tsx | 37 |
9 files changed, 370 insertions, 57 deletions
diff --git a/src/frontend/app/components/StopGallery.tsx b/src/frontend/app/components/StopGallery.tsx index c1d9780..a45bfca 100644 --- a/src/frontend/app/components/StopGallery.tsx +++ b/src/frontend/app/components/StopGallery.tsx @@ -73,9 +73,8 @@ 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/layout/NavBar.tsx b/src/frontend/app/components/layout/NavBar.tsx index 91c8810..1a32b1e 100644 --- a/src/frontend/app/components/layout/NavBar.tsx +++ b/src/frontend/app/components/layout/NavBar.tsx @@ -1,4 +1,4 @@ -import { Map, MapPin, Settings } from "lucide-react"; +import { Map, MapPin, Route, Settings } from "lucide-react"; import type { LngLatLike } from "maplibre-gl"; import { useTranslation } from "react-i18next"; import { Link, useLocation } from "react-router"; @@ -57,11 +57,16 @@ export default function NavBar({ orientation = "horizontal" }: NavBarProps) { updateMapState(coords, 16); } }, - () => {} + () => { } ); }, }, { + name: t("navbar.lines", "Líneas"), + icon: Route, + path: "/lines", + }, + { name: t("navbar.settings", "Ajustes"), icon: Settings, path: "/settings", @@ -70,9 +75,8 @@ export default function NavBar({ orientation = "horizontal" }: NavBarProps) { 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/data/LinesData.ts b/src/frontend/app/data/LinesData.ts new file mode 100644 index 0000000..13224e6 --- /dev/null +++ b/src/frontend/app/data/LinesData.ts @@ -0,0 +1,252 @@ +export interface LineInfo { + lineNumber: string; + routeName: string; + scheduleUrl: string; +} + +/** + * Sourced from https://vitrasa.es/lineas-y-horarios/todas-las-lineas + * + Array.from(document.querySelectorAll(".line-information")).map(el => { + return { + lineNumber: el.querySelector(".square-info").innerText, + routeName: el.querySelector(".all-lines-descripcion-prh").innerText, + scheduleUrl: `https://vitrasa.es/documents/5893389/6130928/${el.querySelector("input[type=checkbox]").value}.pdf` + } + }); + + */ + + +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" + } +]; diff --git a/src/frontend/app/i18n/locales/en-GB.json b/src/frontend/app/i18n/locales/en-GB.json index e09ebb2..1eaf096 100644 --- a/src/frontend/app/i18n/locales/en-GB.json +++ b/src/frontend/app/i18n/locales/en-GB.json @@ -88,6 +88,10 @@ "navbar": { "stops": "Stops", "map": "Map", + "lines": "Lines", "settings": "Settings" + }, + "lines": { + "description": "Below is a list of Vigo urban bus lines with their respective routes and links to official timetables." } } diff --git a/src/frontend/app/i18n/locales/es-ES.json b/src/frontend/app/i18n/locales/es-ES.json index 34d38f8..3c6646a 100644 --- a/src/frontend/app/i18n/locales/es-ES.json +++ b/src/frontend/app/i18n/locales/es-ES.json @@ -88,6 +88,10 @@ "navbar": { "stops": "Paradas", "map": "Mapa", + "lines": "Líneas", "settings": "Ajustes" + }, + "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." } } diff --git a/src/frontend/app/i18n/locales/gl-ES.json b/src/frontend/app/i18n/locales/gl-ES.json index 1b98730..dd73576 100644 --- a/src/frontend/app/i18n/locales/gl-ES.json +++ b/src/frontend/app/i18n/locales/gl-ES.json @@ -88,6 +88,10 @@ "navbar": { "stops": "Paradas", "map": "Mapa", + "lines": "Liñas", "settings": "Axustes" + }, + "lines": { + "description": "A continuación se mostra unha lista das liñas de autobús urbano de Vigo coas súas respectivas rutas e ligazóns ós horarios oficiais." } } diff --git a/src/frontend/app/routes.tsx b/src/frontend/app/routes.tsx index 4ade748..16d0da7 100644 --- a/src/frontend/app/routes.tsx +++ b/src/frontend/app/routes.tsx @@ -3,6 +3,7 @@ import { type RouteConfig, index, route } from "@react-router/dev/routes"; export default [ index("routes/home.tsx"), route("/map", "routes/map.tsx"), + route("/lines", "routes/lines.tsx"), route("/stops", "routes/stops.tsx"), route("/stops/:id", "routes/stops-$id.tsx"), route("/settings", "routes/settings.tsx"), diff --git a/src/frontend/app/routes/home.tsx b/src/frontend/app/routes/home.tsx index cb640c3..5d56b48 100644 --- a/src/frontend/app/routes/home.tsx +++ b/src/frontend/app/routes/home.tsx @@ -259,7 +259,7 @@ export default function StopList() { </div> {/* Search Results */} - {searchResults && searchResults.length > 0 && ( + {searchResults && searchResults.length > 0 ? ( <div className="w-full px-4 flex flex-col gap-2"> <h2 className="text-lg font-semibold text-gray-900 dark:text-gray-100"> {t("stoplist.search_results", "Resultados de la búsqueda")} @@ -270,58 +270,66 @@ export default function StopList() { ))} </ul> </div> - )} - - {/* Favourites Gallery */} - {!loading && ( - <StopGallery - stops={favouriteStops.sort((a, b) => a.stopId - b.stopId)} - title={t("stoplist.favourites")} - emptyMessage={t("stoplist.no_favourites")} - /> - )} + ) : searchResults !== null ? ( + <div className="w-full px-4 flex flex-col gap-2"> + <p className="text-center text-gray-600 dark:text-gray-400 py-8"> + {t("stoplist.no_results", "No se encontraron resultados")} + </p> + </div> + ) : ( + <> + {/* Favourites Gallery */} + {!loading && ( + <StopGallery + stops={favouriteStops.sort((a, b) => a.stopId - b.stopId)} + title={t("stoplist.favourites")} + emptyMessage={t("stoplist.no_favourites")} + /> + )} - {/* Recent Stops Gallery - only show if no favourites */} - {!loading && favouriteStops.length === 0 && ( - <StopGallery - stops={recentStops.slice(0, 5)} - title={t("stoplist.recents")} - /> - )} + {/* Recent Stops Gallery - only show if no favourites */} + {!loading && favouriteStops.length === 0 && ( + <StopGallery + stops={recentStops.slice(0, 5)} + title={t("stoplist.recents")} + /> + )} - {/*<ServiceAlerts />*/} + {/*<ServiceAlerts />*/} - {/* All Stops / Nearby Stops */} - <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> - )} - <h2 className="text-lg font-semibold text-gray-900 dark:text-gray-100"> - {userLocation - ? t("stoplist.nearby_stops", "Nearby stops") - : t("stoplist.all_stops", "Paradas")} - </h2> - </div> + {/* All Stops / Nearby Stops */} + <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> + )} + <h2 className="text-lg font-semibold text-gray-900 dark:text-gray-100"> + {userLocation + ? t("stoplist.nearby_stops", "Nearby stops") + : t("stoplist.all_stops", "Paradas")} + </h2> + </div> - <ul className="list-none p-0 m-0 flex flex-col gap-2 md:grid md:grid-cols-[repeat(auto-fill,minmax(300px,1fr))] lg:grid-cols-[repeat(auto-fill,minmax(320px,1fr))]"> - {loading && ( - <> - {Array.from({ length: 6 }, (_, index) => ( - <StopItemSkeleton key={`skeleton-${index}`} /> - ))} - </> - )} - {!loading && data - ? (userLocation ? sortedAllStops.slice(0, 6) : sortedAllStops).map( - (stop) => <StopItem key={stop.stopId} stop={stop} /> - ) - : null} - </ul> - </div> + <ul className="list-none p-0 m-0 flex flex-col gap-2 md:grid md:grid-cols-[repeat(auto-fill,minmax(300px,1fr))] lg:grid-cols-[repeat(auto-fill,minmax(320px,1fr))]"> + {loading && ( + <> + {Array.from({ length: 6 }, (_, index) => ( + <StopItemSkeleton key={`skeleton-${index}`} /> + ))} + </> + )} + {!loading && data + ? (userLocation ? sortedAllStops.slice(0, 6) : sortedAllStops).map( + (stop) => <StopItem key={stop.stopId} stop={stop} /> + ) + : null} + </ul> + </div> + </> + )} </div> ); } diff --git a/src/frontend/app/routes/lines.tsx b/src/frontend/app/routes/lines.tsx new file mode 100644 index 0000000..658716f --- /dev/null +++ b/src/frontend/app/routes/lines.tsx @@ -0,0 +1,37 @@ +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'; + +export default function LinesPage() { + 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> + + <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> + </div> + ); +} |
