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