aboutsummaryrefslogtreecommitdiff
path: root/src/frontend/app
diff options
context:
space:
mode:
Diffstat (limited to 'src/frontend/app')
-rw-r--r--src/frontend/app/components/StopGallery.tsx5
-rw-r--r--src/frontend/app/components/layout/NavBar.tsx14
-rw-r--r--src/frontend/app/data/LinesData.ts252
-rw-r--r--src/frontend/app/i18n/locales/en-GB.json4
-rw-r--r--src/frontend/app/i18n/locales/es-ES.json4
-rw-r--r--src/frontend/app/i18n/locales/gl-ES.json4
-rw-r--r--src/frontend/app/routes.tsx1
-rw-r--r--src/frontend/app/routes/home.tsx106
-rw-r--r--src/frontend/app/routes/lines.tsx37
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>
+ );
+}