aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorCopilot <198982749+Copilot@users.noreply.github.com>2025-11-17 23:39:08 +0100
committerGitHub <noreply@github.com>2025-11-17 23:39:08 +0100
commit276e73412abef28c222c52a84334d49f5e414f3c (patch)
tree8b7ae07eafa53f9efc5884e4f0696e6077266f48 /src
parent36d982fb3b01fd8181b216b57fba2c42e9404d1f (diff)
Use consolidated data API in map sheet with shared card component (#100)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: arielcostas <94913521+arielcostas@users.noreply.github.com> Co-authored-by: Ariel Costas Guerrero <ariel@costas.dev>
Diffstat (limited to 'src')
-rw-r--r--src/frontend/app/AppContext.tsx8
-rw-r--r--src/frontend/app/components/ErrorDisplay.tsx8
-rw-r--r--src/frontend/app/components/GroupedTable.tsx4
-rw-r--r--src/frontend/app/components/NavBar.tsx2
-rw-r--r--src/frontend/app/components/PullToRefresh.tsx8
-rw-r--r--src/frontend/app/components/RegularTable.tsx2
-rw-r--r--src/frontend/app/components/SchedulesTable.tsx6
-rw-r--r--src/frontend/app/components/StopMapSheet.css27
-rw-r--r--src/frontend/app/components/StopMapSheet.tsx151
-rw-r--r--src/frontend/app/components/StopSheet.tsx105
-rw-r--r--src/frontend/app/components/Stops/ConsolidatedCirculationCard.tsx180
-rw-r--r--src/frontend/app/components/Stops/ConsolidatedCirculationList.css5
-rw-r--r--src/frontend/app/components/Stops/ConsolidatedCirculationList.tsx174
-rw-r--r--src/frontend/app/components/Stops/ConsolidatedCirculationListSkeleton.tsx6
-rw-r--r--src/frontend/app/data/LineColors.ts11
-rw-r--r--src/frontend/app/data/RegionConfig.ts2
-rw-r--r--src/frontend/app/data/StopDataProvider.ts14
-rw-r--r--src/frontend/app/maps/styleloader.ts6
-rw-r--r--src/frontend/app/routes/estimates-$id.tsx10
-rw-r--r--src/frontend/app/routes/home.tsx14
-rw-r--r--src/frontend/app/routes/map.tsx12
-rw-r--r--src/frontend/app/routes/settings.tsx4
-rw-r--r--src/frontend/app/routes/stops-$id.tsx8
-rw-r--r--src/frontend/app/routes/timetable-$id.tsx20
-rw-r--r--src/frontend/package-lock.json23
-rw-r--r--src/frontend/public/maps/styles/openfreemap-dark.json2
-rw-r--r--src/frontend/public/maps/styles/openfreemap-light.json2
-rw-r--r--src/frontend/public/pwa-worker.js4
28 files changed, 456 insertions, 362 deletions
diff --git a/src/frontend/app/AppContext.tsx b/src/frontend/app/AppContext.tsx
index a369293..9c2521f 100644
--- a/src/frontend/app/AppContext.tsx
+++ b/src/frontend/app/AppContext.tsx
@@ -63,7 +63,7 @@ export const AppProvider = ({ children }: { children: ReactNode }) => {
};
const [systemTheme, setSystemTheme] = useState<"light" | "dark">(
- getPreferredScheme,
+ getPreferredScheme
);
const [theme, setTheme] = useState<Theme>(() => {
@@ -141,7 +141,7 @@ export const AppProvider = ({ children }: { children: ReactNode }) => {
const toggleTableStyle = () => {
setTableStyle((prevTableStyle) =>
- prevTableStyle === "regular" ? "grouped" : "regular",
+ prevTableStyle === "regular" ? "grouped" : "regular"
);
};
@@ -155,7 +155,7 @@ export const AppProvider = ({ children }: { children: ReactNode }) => {
() => {
const saved = localStorage.getItem("mapPositionMode");
return saved === "last" ? "last" : "gps";
- },
+ }
);
useEffect(() => {
@@ -263,7 +263,7 @@ export const AppProvider = ({ children }: { children: ReactNode }) => {
(error) => {
console.error("Error getting location:", error);
setLocationPermission(false);
- },
+ }
);
}
}
diff --git a/src/frontend/app/components/ErrorDisplay.tsx b/src/frontend/app/components/ErrorDisplay.tsx
index f63c995..a2f40c8 100644
--- a/src/frontend/app/components/ErrorDisplay.tsx
+++ b/src/frontend/app/components/ErrorDisplay.tsx
@@ -38,25 +38,25 @@ export const ErrorDisplay: React.FC<ErrorDisplayProps> = ({
case "network":
return t(
"errors.network",
- "No hay conexión a internet. Comprueba tu conexión y vuelve a intentarlo.",
+ "No hay conexión a internet. Comprueba tu conexión y vuelve a intentarlo."
);
case "server":
if (error.status === 404) {
return t(
"errors.not_found",
- "No se encontraron datos para esta parada.",
+ "No se encontraron datos para esta parada."
);
}
if (error.status === 500) {
return t(
"errors.server_error",
- "Error del servidor. Inténtalo de nuevo más tarde.",
+ "Error del servidor. Inténtalo de nuevo más tarde."
);
}
if (error.status && error.status >= 400) {
return t(
"errors.client_error",
- "Error en la solicitud. Verifica que la parada existe.",
+ "Error en la solicitud. Verifica que la parada existe."
);
}
return t("errors.server_generic", "Error del servidor ({{status}})", {
diff --git a/src/frontend/app/components/GroupedTable.tsx b/src/frontend/app/components/GroupedTable.tsx
index f116537..3a799a7 100644
--- a/src/frontend/app/components/GroupedTable.tsx
+++ b/src/frontend/app/components/GroupedTable.tsx
@@ -29,7 +29,7 @@ export const GroupedTable: React.FC<GroupedTable> = ({
acc[estimate.line].push(estimate);
return acc;
},
- {} as Record<string, typeof data>,
+ {} as Record<string, typeof data>
);
const sortedLines = Object.keys(groupedEstimates).sort((a, b) => {
@@ -72,7 +72,7 @@ export const GroupedTable: React.FC<GroupedTable> = ({
</td>
)}
</tr>
- )),
+ ))
)}
</tbody>
diff --git a/src/frontend/app/components/NavBar.tsx b/src/frontend/app/components/NavBar.tsx
index f9f1a03..b8c6ad6 100644
--- a/src/frontend/app/components/NavBar.tsx
+++ b/src/frontend/app/components/NavBar.tsx
@@ -51,7 +51,7 @@ export default function NavBar() {
updateMapState(coords, 16);
}
},
- () => {},
+ () => {}
);
},
},
diff --git a/src/frontend/app/components/PullToRefresh.tsx b/src/frontend/app/components/PullToRefresh.tsx
index d5ea51b..b3abe86 100644
--- a/src/frontend/app/components/PullToRefresh.tsx
+++ b/src/frontend/app/components/PullToRefresh.tsx
@@ -48,7 +48,7 @@ export const PullToRefresh: React.FC<PullToRefreshProps> = ({
htmlScroll,
bodyScroll,
containerScroll,
- parentScroll,
+ parentScroll
);
if (maxScroll > 0 || isRefreshing) {
@@ -60,7 +60,7 @@ export const PullToRefresh: React.FC<PullToRefreshProps> = ({
startY.current = e.touches[0].clientY;
setIsPulling(true);
},
- [isRefreshing],
+ [isRefreshing]
);
const handleTouchMove = useCallback(
@@ -78,7 +78,7 @@ export const PullToRefresh: React.FC<PullToRefreshProps> = ({
htmlScroll,
bodyScroll,
containerScroll,
- parentScroll,
+ parentScroll
);
if (maxScroll > 10) {
@@ -116,7 +116,7 @@ export const PullToRefresh: React.FC<PullToRefreshProps> = ({
setIsActive(false);
}
},
- [isPulling, threshold, isActive, y],
+ [isPulling, threshold, isActive, y]
);
const handleTouchEnd = useCallback(async () => {
diff --git a/src/frontend/app/components/RegularTable.tsx b/src/frontend/app/components/RegularTable.tsx
index baa3804..868332f 100644
--- a/src/frontend/app/components/RegularTable.tsx
+++ b/src/frontend/app/components/RegularTable.tsx
@@ -24,7 +24,7 @@ export const RegularTable: React.FC<RegularTableProps> = ({
{
hour: "2-digit",
minute: "2-digit",
- },
+ }
).format(arrival);
};
diff --git a/src/frontend/app/components/SchedulesTable.tsx b/src/frontend/app/components/SchedulesTable.tsx
index 07d3136..60e7ab0 100644
--- a/src/frontend/app/components/SchedulesTable.tsx
+++ b/src/frontend/app/components/SchedulesTable.tsx
@@ -100,17 +100,17 @@ const findNearbyEntries = (
entries: ScheduledTable[],
currentTime: string,
before: number = 4,
- after: number = 4,
+ after: number = 4
): ScheduledTable[] => {
if (!currentTime) return entries.slice(0, before + after);
const currentMinutes = timeToMinutes(currentTime);
const sortedEntries = [...entries].sort(
- (a, b) => timeToMinutes(a.calling_time) - timeToMinutes(b.calling_time),
+ (a, b) => timeToMinutes(a.calling_time) - timeToMinutes(b.calling_time)
);
let currentIndex = sortedEntries.findIndex(
- (entry) => timeToMinutes(entry.calling_time) >= currentMinutes,
+ (entry) => timeToMinutes(entry.calling_time) >= currentMinutes
);
if (currentIndex === -1) {
diff --git a/src/frontend/app/components/StopMapSheet.css b/src/frontend/app/components/StopMapSheet.css
index 5125ff0..4b0f528 100644
--- a/src/frontend/app/components/StopMapSheet.css
+++ b/src/frontend/app/components/StopMapSheet.css
@@ -27,14 +27,18 @@
.center-btn {
appearance: none;
- border: 1px solid rgba(0,0,0,0.15);
- background: color-mix(in oklab, var(--background-color, #fff) 85%, transparent);
+ border: 1px solid rgba(0, 0, 0, 0.15);
+ background: color-mix(
+ in oklab,
+ var(--background-color, #fff) 85%,
+ transparent
+ );
color: var(--text-primary, #111);
padding: 6px;
border-radius: 6px;
font-size: 12px;
line-height: 1;
- box-shadow: 0 1px 2px rgba(0,0,0,0.15);
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.15);
cursor: pointer;
}
@@ -56,7 +60,7 @@
background: #2a6df4;
border: 2px solid #fff;
border-radius: 50%;
- box-shadow: 0 1px 2px rgba(0,0,0,0.3);
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
}
.user-dot__pulse {
@@ -73,7 +77,16 @@
}
@keyframes userPulse {
- 0% { transform: scale(0.6); opacity: 0.8; }
- 70% { transform: scale(1.2); opacity: 0.15; }
- 100% { transform: scale(1.4); opacity: 0; }
+ 0% {
+ transform: scale(0.6);
+ opacity: 0.8;
+ }
+ 70% {
+ transform: scale(1.2);
+ opacity: 0.15;
+ }
+ 100% {
+ transform: scale(1.4);
+ opacity: 0;
+ }
}
diff --git a/src/frontend/app/components/StopMapSheet.tsx b/src/frontend/app/components/StopMapSheet.tsx
index e87e8c8..b3a1666 100644
--- a/src/frontend/app/components/StopMapSheet.tsx
+++ b/src/frontend/app/components/StopMapSheet.tsx
@@ -1,6 +1,10 @@
import maplibregl from "maplibre-gl";
import React, { useEffect, useMemo, useRef, useState } from "react";
-import Map, { AttributionControl, Marker, type MapRef } from "react-map-gl/maplibre";
+import Map, {
+ AttributionControl,
+ Marker,
+ type MapRef,
+} from "react-map-gl/maplibre";
import { useApp } from "~/AppContext";
import { getLineColor } from "~/data/LineColors";
import type { RegionId } from "~/data/RegionConfig";
@@ -53,24 +57,38 @@ export const StopMap: React.FC<StopMapProps> = ({
const lat2 = (b.lat * Math.PI) / 180;
const sinDLat = Math.sin(dLat / 2);
const sinDLon = Math.sin(dLon / 2);
- const h = sinDLat * sinDLat + Math.cos(lat1) * Math.cos(lat2) * sinDLon * sinDLon;
+ const h =
+ sinDLat * sinDLat + Math.cos(lat1) * Math.cos(lat2) * sinDLon * sinDLon;
return 2 * R * Math.asin(Math.min(1, Math.sqrt(h)));
};
const computeFocusPoints = (): Pt[] => {
const buses: Pt[] = [];
for (const c of busPositions) {
- if (c.currentPosition) buses.push({ lat: c.currentPosition.latitude, lon: c.currentPosition.longitude });
+ if (c.currentPosition)
+ buses.push({
+ lat: c.currentPosition.latitude,
+ lon: c.currentPosition.longitude,
+ });
}
- const stopPt = stop.latitude && stop.longitude ? { lat: stop.latitude, lon: stop.longitude } : null;
- const userPt = userPosition ? { lat: userPosition.latitude, lon: userPosition.longitude } : null;
+ const stopPt =
+ stop.latitude && stop.longitude
+ ? { lat: stop.latitude, lon: stop.longitude }
+ : null;
+ const userPt = userPosition
+ ? { lat: userPosition.latitude, lon: userPosition.longitude }
+ : null;
if (buses.length === 0 && !stopPt && !userPt) return [];
// Choose anchor for proximity: stop > user > average of buses
let anchor: Pt | null = stopPt || userPt || null;
if (!anchor && buses.length) {
- let lat = 0, lon = 0;
- for (const b of buses) { lat += b.lat; lon += b.lon; }
+ let lat = 0,
+ lon = 0;
+ for (const b of buses) {
+ lat += b.lat;
+ lon += b.lon;
+ }
anchor = { lat: lat / buses.length, lon: lon / buses.length };
}
@@ -122,7 +140,7 @@ export const StopMap: React.FC<StopMapProps> = ({
});
},
() => {},
- { enableHighAccuracy: true, maximumAge: 15000, timeout: 5000 },
+ { enableHighAccuracy: true, maximumAge: 15000, timeout: 5000 }
);
geoWatchId.current = navigator.geolocation.watchPosition(
(pos) => {
@@ -133,7 +151,7 @@ export const StopMap: React.FC<StopMapProps> = ({
});
},
() => {},
- { enableHighAccuracy: true, maximumAge: 30000, timeout: 10000 },
+ { enableHighAccuracy: true, maximumAge: 30000, timeout: 10000 }
);
} catch {}
return () => {
@@ -158,7 +176,7 @@ export const StopMap: React.FC<StopMapProps> = ({
const busPositions = useMemo(
() => circulations.filter((c) => !!c.currentPosition),
- [circulations],
+ [circulations]
);
// Fit bounds to stop + buses, with ~1km padding each side, with a modest animation
@@ -185,13 +203,17 @@ export const StopMap: React.FC<StopMapProps> = ({
const bounds = new maplibregl.LngLatBounds(sw, ne);
// Determine predominant bus quadrant relative to stop to bias padding.
- const padding: number | { top: number; right: number; bottom: number; left: number } = 24;
+ const padding:
+ | number
+ | { top: number; right: number; bottom: number; left: number } = 24;
// If the diagonal is huge (likely outliers sneaked in), clamp via zoom fallback
try {
if (points.length === 1) {
const only = points[0];
- mapRef.current.getMap().jumpTo({ center: [only.lon, only.lat], zoom: 16 });
+ mapRef.current
+ .getMap()
+ .jumpTo({ center: [only.lon, only.lat], zoom: 16 });
} else {
mapRef.current.fitBounds(bounds, {
padding: padding as any,
@@ -208,7 +230,10 @@ export const StopMap: React.FC<StopMapProps> = ({
const pts = computeFocusPoints();
if (pts.length === 0) return;
- let minLat = pts[0].lat, maxLat = pts[0].lat, minLon = pts[0].lon, maxLon = pts[0].lon;
+ let minLat = pts[0].lat,
+ maxLat = pts[0].lat,
+ minLon = pts[0].lon,
+ maxLon = pts[0].lon;
for (const p of pts) {
if (p.lat < minLat) minLat = p.lat;
if (p.lat > maxLat) maxLat = p.lat;
@@ -220,14 +245,22 @@ export const StopMap: React.FC<StopMapProps> = ({
const ne = [maxLon, maxLat] as [number, number];
const bounds = new maplibregl.LngLatBounds(sw, ne);
- const padding: number | { top: number; right: number; bottom: number; left: number } = 24;
+ const padding:
+ | number
+ | { top: number; right: number; bottom: number; left: number } = 24;
try {
if (pts.length === 1) {
const only = pts[0];
- mapRef.current.getMap().easeTo({ center: [only.lon, only.lat], zoom: 16, duration: 450 });
+ mapRef.current
+ .getMap()
+ .easeTo({ center: [only.lon, only.lat], zoom: 16, duration: 450 });
} else {
- mapRef.current.fitBounds(bounds, { padding: padding as any, duration: 500, maxZoom: 17 } as any);
+ mapRef.current.fitBounds(bounds, {
+ padding: padding as any,
+ duration: 500,
+ maxZoom: 17,
+ } as any);
}
} catch {}
};
@@ -256,15 +289,36 @@ export const StopMap: React.FC<StopMapProps> = ({
{/* Stop marker (center) */}
{stop.latitude && stop.longitude && (
- <Marker longitude={stop.longitude} latitude={stop.latitude} anchor="bottom">
+ <Marker
+ longitude={stop.longitude}
+ latitude={stop.latitude}
+ anchor="bottom"
+ >
<div title={`Stop ${stop.stopId}`}>
<svg width="28" height="36" viewBox="0 0 28 36">
<defs>
- <filter id="drop" x="-20%" y="-20%" width="140%" height="140%">
- <feDropShadow dx="0" dy="1" stdDeviation="1" flood-opacity="0.35" />
+ <filter
+ id="drop"
+ x="-20%"
+ y="-20%"
+ width="140%"
+ height="140%"
+ >
+ <feDropShadow
+ dx="0"
+ dy="1"
+ stdDeviation="1"
+ floodOpacity={0.35}
+ />
</filter>
</defs>
- <path d="M14 0C6.82 0 1 5.82 1 13c0 8.5 11 23 13 23s13-14.5 13-23C27 5.82 21.18 0 14 0z" fill="#1976d2" stroke="#fff" strokeWidth="2" filter="url(#drop)" />
+ <path
+ d="M14 0C6.82 0 1 5.82 1 13c0 8.5 11 23 13 23s13-14.5 13-23C27 5.82 21.18 0 14 0z"
+ fill="#1976d2"
+ stroke="#fff"
+ strokeWidth="2"
+ filter="url(#drop)"
+ />
<circle cx="14" cy="13" r="5" fill="#fff" />
<circle cx="14" cy="13" r="3" fill="#1976d2" />
</svg>
@@ -274,7 +328,11 @@ export const StopMap: React.FC<StopMapProps> = ({
{/* User position marker (if available) */}
{userPosition && (
- <Marker longitude={userPosition.longitude} latitude={userPosition.latitude} anchor="center">
+ <Marker
+ longitude={userPosition.longitude}
+ latitude={userPosition.latitude}
+ anchor="center"
+ >
<div className="user-dot" title="Your location">
<div className="user-dot__pulse" />
<div className="user-dot__core" />
@@ -290,7 +348,12 @@ export const StopMap: React.FC<StopMapProps> = ({
const gaps: number[] = new Array(busPositions.length).fill(baseGap);
if (map && zoom >= 14.5 && busPositions.length > 1) {
const pts = busPositions.map((c) =>
- c.currentPosition ? map.project([c.currentPosition.longitude, c.currentPosition.latitude]) : null,
+ c.currentPosition
+ ? map.project([
+ c.currentPosition.longitude,
+ c.currentPosition.latitude,
+ ])
+ : null
);
for (let i = 0; i < pts.length; i++) {
const pi = pts[i];
@@ -314,7 +377,12 @@ export const StopMap: React.FC<StopMapProps> = ({
const showLabel = zoom >= 13;
const labelGap = gaps[idx] ?? baseGap;
return (
- <Marker key={idx} longitude={p.longitude} latitude={p.latitude} anchor="center">
+ <Marker
+ key={idx}
+ longitude={p.longitude}
+ latitude={p.latitude}
+ anchor="center"
+ >
<div
style={{
display: "flex",
@@ -329,9 +397,16 @@ export const StopMap: React.FC<StopMapProps> = ({
width="20"
height="20"
viewBox="0 0 24 24"
- style={{ filter: "drop-shadow(0 1px 2px rgba(0,0,0,0.3))" }}
+ style={{
+ filter: "drop-shadow(0 1px 2px rgba(0,0,0,0.3))",
+ }}
>
- <path d="M12 2 L20 22 L12 18 L4 22 Z" fill={lineColor.background} stroke="#fff" strokeWidth="1.5" />
+ <path
+ d="M12 2 L20 22 L12 18 L4 22 Z"
+ fill={lineColor.background}
+ stroke="#fff"
+ strokeWidth="1.5"
+ />
</svg>
{showLabel && (
<div
@@ -362,11 +437,29 @@ export const StopMap: React.FC<StopMapProps> = ({
)}
{/* Floating controls */}
<div className="map-floating-controls">
- <button type="button" aria-label="Center" className="center-btn" onClick={handleCenter} title="Center view">
+ <button
+ type="button"
+ aria-label="Center"
+ className="center-btn"
+ onClick={handleCenter}
+ title="Center view"
+ >
<svg width="20" height="20" viewBox="0 0 24 24" aria-hidden="true">
- <circle cx="12" cy="12" r="3" fill="currentColor"/>
- <path d="M12 2v3M12 19v3M2 12h3M19 12h3" stroke="currentColor" strokeWidth="2" strokeLinecap="round"/>
- <circle cx="12" cy="12" r="8" fill="none" stroke="currentColor" strokeWidth="1.5"/>
+ <circle cx="12" cy="12" r="3" fill="currentColor" />
+ <path
+ d="M12 2v3M12 19v3M2 12h3M19 12h3"
+ stroke="currentColor"
+ strokeWidth="2"
+ strokeLinecap="round"
+ />
+ <circle
+ cx="12"
+ cy="12"
+ r="8"
+ fill="none"
+ stroke="currentColor"
+ strokeWidth="1.5"
+ />
</svg>
</button>
</div>
diff --git a/src/frontend/app/components/StopSheet.tsx b/src/frontend/app/components/StopSheet.tsx
index 6977d87..2a28d36 100644
--- a/src/frontend/app/components/StopSheet.tsx
+++ b/src/frontend/app/components/StopSheet.tsx
@@ -1,15 +1,16 @@
-import { Clock, RefreshCw } from "lucide-react";
+import { RefreshCw } from "lucide-react";
import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { Sheet } from "react-modal-sheet";
import { Link } from "react-router";
import type { Stop } from "~/data/StopDataProvider";
import { useApp } from "../AppContext";
-import { REGIONS, type RegionId, getRegionConfig } from "../data/RegionConfig";
-import { type Estimate } from "../routes/estimates-$id";
+import { type RegionId, getRegionConfig } from "../data/RegionConfig";
+import { type ConsolidatedCirculation } from "../routes/stops-$id";
import { ErrorDisplay } from "./ErrorDisplay";
import LineIcon from "./LineIcon";
import { StopAlert } from "./StopAlert";
+import { ConsolidatedCirculationCard } from "./Stops/ConsolidatedCirculationCard";
import "./StopSheet.css";
import { StopSheetSkeleton } from "./StopSheetSkeleton";
@@ -25,16 +26,19 @@ interface ErrorInfo {
message?: string;
}
-const loadStopData = async (
+const loadConsolidatedData = async (
region: RegionId,
- stopId: number,
-): Promise<Estimate[]> => {
+ stopId: number
+): Promise<ConsolidatedCirculation[]> => {
const regionConfig = getRegionConfig(region);
- const resp = await fetch(`${regionConfig.estimatesEndpoint}?id=${stopId}`, {
- headers: {
- Accept: "application/json",
- },
- });
+ const resp = await fetch(
+ `${regionConfig.consolidatedCirculationsEndpoint}?stopId=${stopId}`,
+ {
+ headers: {
+ Accept: "application/json",
+ },
+ }
+ );
if (!resp.ok) {
throw new Error(`HTTP ${resp.status}: ${resp.statusText}`);
@@ -50,7 +54,8 @@ export const StopSheet: React.FC<StopSheetProps> = ({
}) => {
const { t } = useTranslation();
const { region } = useApp();
- const [data, setData] = useState<Estimate[] | null>(null);
+ const regionConfig = getRegionConfig(region);
+ const [data, setData] = useState<ConsolidatedCirculation[] | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<ErrorInfo | null>(null);
const [lastUpdated, setLastUpdated] = useState<Date | null>(null);
@@ -82,7 +87,7 @@ export const StopSheet: React.FC<StopSheetProps> = ({
setError(null);
setData(null);
- const stopData = await loadStopData(region, stop.stopId);
+ const stopData = await loadConsolidatedData(region, stop.stopId);
setData(stopData);
setLastUpdated(new Date());
} catch (err) {
@@ -99,33 +104,15 @@ export const StopSheet: React.FC<StopSheetProps> = ({
}
}, [isOpen, stop.stopId, region]);
- const formatTime = (minutes: number) => {
- if (minutes > 15) {
- const now = new Date();
- const arrival = new Date(now.getTime() + minutes * 60000);
- return Intl.DateTimeFormat(
- typeof navigator !== "undefined" ? navigator.language : "en",
- {
- hour: "2-digit",
- minute: "2-digit",
- },
- ).format(arrival);
- } else {
- return `${minutes} ${t("estimates.minutes", "min")}`;
- }
- };
-
- const formatDistance = (meters: number) => {
- if (meters > 1024) {
- return `${(meters / 1000).toFixed(1)} km`;
- } else {
- return `${meters} ${t("estimates.meters", "m")}`;
- }
- };
-
// Show only the next 4 arrivals
- const limitedEstimates =
- data?.sort((a, b) => a.minutes - b.minutes).slice(0, 4) || [];
+ const sortedData = data
+ ? [...data].sort(
+ (a, b) =>
+ (a.realTime?.minutes ?? a.schedule?.minutes ?? 999) -
+ (b.realTime?.minutes ?? b.schedule?.minutes ?? 999)
+ )
+ : [];
+ const limitedEstimates = sortedData.slice(0, 4);
return (
<Sheet isOpen={isOpen} onClose={onClose} detent="content">
@@ -158,7 +145,7 @@ export const StopSheet: React.FC<StopSheetProps> = ({
onRetry={loadData}
title={t(
"errors.estimates_title",
- "Error al cargar estimaciones",
+ "Error al cargar estimaciones"
)}
className="compact"
/>
@@ -176,36 +163,15 @@ export const StopSheet: React.FC<StopSheetProps> = ({
) : (
<div className="stop-sheet-estimates-list">
{limitedEstimates.map((estimate, idx) => (
- <div key={idx} className="stop-sheet-estimate-item">
- <div className="stop-sheet-estimate-line">
- <LineIcon line={estimate.line} region={region} />
- </div>
- <div className="stop-sheet-estimate-details">
- <div className="stop-sheet-estimate-route">
- {estimate.route}
- </div>
- </div>
- <div className="stop-sheet-estimate-arrival">
- <div
- className={`stop-sheet-estimate-time ${estimate.minutes <= 15 ? "is-minutes" : ""}`}
- >
- <Clock />
- {formatTime(estimate.minutes)}
- </div>
- {REGIONS[region].showMeters &&
- estimate.meters >= 0 && (
- <div className="stop-sheet-estimate-distance">
- {formatDistance(estimate.meters)}
- </div>
- )}
- </div>
- </div>
+ <ConsolidatedCirculationCard
+ key={idx}
+ estimate={estimate}
+ regionConfig={regionConfig}
+ />
))}
</div>
)}
</div>
-
-
</>
) : null}
</div>
@@ -236,14 +202,11 @@ export const StopSheet: React.FC<StopSheetProps> = ({
</button>
<Link
- to={`/estimates/${stop.stopId}`}
+ to={`/stops/${stop.stopId}`}
className="stop-sheet-view-all"
onClick={onClose}
>
- {t(
- "map.view_all_estimates",
- "Ver todas las estimaciones",
- )}
+ {t("map.view_all_estimates", "Ver todas las estimaciones")}
</Link>
</div>
</div>
diff --git a/src/frontend/app/components/Stops/ConsolidatedCirculationCard.tsx b/src/frontend/app/components/Stops/ConsolidatedCirculationCard.tsx
new file mode 100644
index 0000000..8c3e922
--- /dev/null
+++ b/src/frontend/app/components/Stops/ConsolidatedCirculationCard.tsx
@@ -0,0 +1,180 @@
+import { Clock } from "lucide-react";
+import { useTranslation } from "react-i18next";
+import LineIcon from "~components/LineIcon";
+import { type RegionConfig } from "~data/RegionConfig";
+import { type ConsolidatedCirculation } from "~routes/stops-$id";
+
+import "./ConsolidatedCirculationList.css";
+
+interface ConsolidatedCirculationCardProps {
+ estimate: ConsolidatedCirculation;
+ regionConfig: RegionConfig;
+}
+
+// Utility function to parse service ID and get the turn number
+const parseServiceId = (serviceId: string): string => {
+ const parts = serviceId.split("_");
+ if (parts.length === 0) return "";
+
+ const lastPart = parts[parts.length - 1];
+ if (lastPart.length < 6) return "";
+
+ const last6 = lastPart.slice(-6);
+ const lineCode = last6.slice(0, 3);
+ const turnCode = last6.slice(-3);
+
+ // Remove leading zeros from turn
+ const turnNumber = parseInt(turnCode, 10).toString();
+
+ // Parse line number with special cases
+ const lineNumber = parseInt(lineCode, 10);
+ let displayLine: string;
+
+ switch (lineNumber) {
+ case 1:
+ displayLine = "C1";
+ break;
+ case 3:
+ displayLine = "C3";
+ break;
+ case 30:
+ displayLine = "N1";
+ break;
+ case 33:
+ displayLine = "N4";
+ break;
+ case 8:
+ displayLine = "A";
+ break;
+ case 101:
+ displayLine = "H";
+ break;
+ case 150:
+ displayLine = "REF";
+ break;
+ case 500:
+ displayLine = "TUR";
+ break;
+ case 201:
+ displayLine = "U1";
+ break;
+ case 202:
+ displayLine = "U2";
+ break;
+ default:
+ displayLine = `L${lineNumber}`;
+ }
+
+ return `${displayLine}-${turnNumber}`;
+};
+
+export const ConsolidatedCirculationCard: React.FC<
+ ConsolidatedCirculationCardProps
+> = ({ estimate, regionConfig }) => {
+ const { t } = useTranslation();
+
+ const absoluteArrivalTime = (minutes: number) => {
+ const now = new Date();
+ const arrival = new Date(now.getTime() + minutes * 60000);
+ return Intl.DateTimeFormat(
+ typeof navigator !== "undefined" ? navigator.language : "en",
+ {
+ hour: "2-digit",
+ minute: "2-digit",
+ }
+ ).format(arrival);
+ };
+
+ const formatDistance = (meters: number) => {
+ if (meters > 1024) {
+ return `${(meters / 1000).toFixed(1)} km`;
+ } else {
+ return `${meters} ${t("estimates.meters", "m")}`;
+ }
+ };
+
+ const getDelayText = (estimate: ConsolidatedCirculation): string | null => {
+ if (!estimate.schedule || !estimate.realTime) {
+ return null;
+ }
+
+ const delay = estimate.realTime.minutes - estimate.schedule.minutes;
+
+ if (delay >= -1 && delay <= 2) {
+ return "OK";
+ } else if (delay > 2) {
+ return "R" + delay;
+ } else {
+ return "A" + Math.abs(delay);
+ }
+ };
+
+ const getTripIdDisplay = (tripId: string): string => {
+ const parts = tripId.split("_");
+ return parts.length > 1 ? parts[1] : tripId;
+ };
+
+ const getTimeClass = (estimate: ConsolidatedCirculation): string => {
+ if (estimate.realTime && estimate.schedule?.running) {
+ return "time-running";
+ }
+
+ if (estimate.realTime && !estimate.schedule) {
+ return "time-running";
+ } else if (estimate.realTime && !estimate.schedule?.running) {
+ return "time-delayed";
+ }
+
+ return "time-scheduled";
+ };
+
+ const displayMinutes =
+ estimate.realTime?.minutes ?? estimate.schedule?.minutes ?? 0;
+ const timeClass = getTimeClass(estimate);
+ const delayText = getDelayText(estimate);
+
+ return (
+ <div className="consolidated-circulation-card">
+ <div className="card-header">
+ <div className="line-info">
+ <LineIcon line={estimate.line} region={regionConfig.id} />
+ </div>
+
+ <div className="route-info">
+ <strong>{estimate.route}</strong>
+ </div>
+
+ <div className="time-info">
+ <div className={`arrival-time ${timeClass}`}>
+ <Clock />
+ {estimate.realTime
+ ? `${displayMinutes} ${t("estimates.minutes", "min")}`
+ : absoluteArrivalTime(displayMinutes)}
+ </div>
+ <div className="distance-info">
+ {estimate.schedule && (
+ <>
+ {parseServiceId(estimate.schedule.serviceId)} (
+ {getTripIdDisplay(estimate.schedule.tripId)})
+ </>
+ )}
+
+ {estimate.schedule &&
+ estimate.realTime &&
+ estimate.realTime.distance >= 0 && <> &middot; </>}
+
+ {estimate.realTime && estimate.realTime.distance >= 0 && (
+ <>{formatDistance(estimate.realTime.distance)}</>
+ )}
+
+ {estimate.schedule &&
+ estimate.realTime &&
+ estimate.realTime.distance >= 0 && <> &middot; </>}
+
+ {delayText}
+ </div>
+ </div>
+ </div>
+ </div>
+ );
+};
diff --git a/src/frontend/app/components/Stops/ConsolidatedCirculationList.css b/src/frontend/app/components/Stops/ConsolidatedCirculationList.css
index 4d6a3a8..939f40d 100644
--- a/src/frontend/app/components/Stops/ConsolidatedCirculationList.css
+++ b/src/frontend/app/components/Stops/ConsolidatedCirculationList.css
@@ -92,7 +92,10 @@
}
[data-theme="dark"] .consolidated-circulation-card .arrival-time.time-scheduled,
-[data-theme="dark"] .consolidated-circulation-card .arrival-time.time-scheduled svg {
+[data-theme="dark"]
+ .consolidated-circulation-card
+ .arrival-time.time-scheduled
+ svg {
color: #8fb4ff; /* lighten for dark backgrounds */
}
diff --git a/src/frontend/app/components/Stops/ConsolidatedCirculationList.tsx b/src/frontend/app/components/Stops/ConsolidatedCirculationList.tsx
index 4ee296d..d95ee03 100644
--- a/src/frontend/app/components/Stops/ConsolidatedCirculationList.tsx
+++ b/src/frontend/app/components/Stops/ConsolidatedCirculationList.tsx
@@ -1,8 +1,7 @@
-import { Clock } from "lucide-react";
import { useTranslation } from "react-i18next";
-import LineIcon from "~components/LineIcon";
import { type RegionConfig } from "~data/RegionConfig";
import { type ConsolidatedCirculation } from "~routes/stops-$id";
+import { ConsolidatedCirculationCard } from "./ConsolidatedCirculationCard";
import "./ConsolidatedCirculationList.css";
@@ -12,63 +11,6 @@ interface RegularTableProps {
regionConfig: RegionConfig;
}
-// Utility function to parse service ID and get the turn number
-const parseServiceId = (serviceId: string): string => {
- const parts = serviceId.split("_");
- if (parts.length === 0) return "";
-
- const lastPart = parts[parts.length - 1];
- if (lastPart.length < 6) return "";
-
- const last6 = lastPart.slice(-6);
- const lineCode = last6.slice(0, 3);
- const turnCode = last6.slice(-3);
-
- // Remove leading zeros from turn
- const turnNumber = parseInt(turnCode, 10).toString();
-
- // Parse line number with special cases
- const lineNumber = parseInt(lineCode, 10);
- let displayLine: string;
-
- switch (lineNumber) {
- case 1:
- displayLine = "C1";
- break;
- case 3:
- displayLine = "C3";
- break;
- case 30:
- displayLine = "N1";
- break;
- case 33:
- displayLine = "N4";
- break;
- case 8:
- displayLine = "A";
- break;
- case 101:
- displayLine = "H";
- break;
- case 150:
- displayLine = "REF";
- break;
- case 500:
- displayLine = "TUR";
- break;
- case 201:
- displayLine = "U1";
- break;
- case 202:
- displayLine = "U2";
- break;
- default:
- displayLine = `L${lineNumber}`;
- }
-
- return `${displayLine}-${turnNumber}`;
-};
-
export const ConsolidatedCirculationList: React.FC<RegularTableProps> = ({
data,
dataDate,
@@ -76,65 +18,10 @@ export const ConsolidatedCirculationList: React.FC<RegularTableProps> = ({
}) => {
const { t } = useTranslation();
- const absoluteArrivalTime = (minutes: number) => {
- const now = new Date();
- const arrival = new Date(now.getTime() + minutes * 60000);
- return Intl.DateTimeFormat(
- typeof navigator !== "undefined" ? navigator.language : "en",
- {
- hour: "2-digit",
- minute: "2-digit",
- },
- ).format(arrival);
- };
-
- const formatDistance = (meters: number) => {
- if (meters > 1024) {
- return `${(meters / 1000).toFixed(1)} km`;
- } else {
- return `${meters} ${t("estimates.meters", "m")}`;
- }
- };
-
- const getDelayText = (estimate: ConsolidatedCirculation): string | null => {
- if (!estimate.schedule || !estimate.realTime) {
- return null;
- }
-
- const delay = estimate.realTime.minutes - estimate.schedule.minutes;
-
- if (delay >= -1 && delay <= 2) {
- return "OK"
- } else if (delay > 2) {
- return "R" + delay;
- } else {
- return "A" + Math.abs(delay);
- }
- };
-
- const getTripIdDisplay = (tripId: string): string => {
- const parts = tripId.split("_");
- return parts.length > 1 ? parts[1] : tripId;
- };
-
- const getTimeClass = (estimate: ConsolidatedCirculation): string => {
- if (estimate.realTime && estimate.schedule?.running) {
- return "time-running";
- }
-
- if (estimate.realTime && !estimate.schedule) {
- return "time-running";
- } else if (estimate.realTime && !estimate.schedule?.running) {
- return "time-delayed";
- }
-
- return "time-scheduled";
- };
-
const sortedData = [...data].sort(
(a, b) =>
(a.realTime?.minutes ?? a.schedule?.minutes ?? 999) -
- (b.realTime?.minutes ?? b.schedule?.minutes ?? 999),
+ (b.realTime?.minutes ?? b.schedule?.minutes ?? 999)
);
return (
@@ -151,56 +38,13 @@ export const ConsolidatedCirculationList: React.FC<RegularTableProps> = ({
</div>
) : (
<>
- {sortedData.map((estimate, idx) => {
- const displayMinutes =
- estimate.realTime?.minutes ?? estimate.schedule?.minutes ?? 0;
- const timeClass = getTimeClass(estimate);
- const delayText = getDelayText(estimate);
-
- return (
- <div key={idx} className="consolidated-circulation-card">
- <div className="card-header">
- <div className="line-info">
- <LineIcon line={estimate.line} region={regionConfig.id} />
- </div>
-
- <div className="route-info">
- <strong>{estimate.route}</strong>
- </div>
-
- <div className="time-info">
- <div className={`arrival-time ${timeClass}`}>
- <Clock />
- {estimate.realTime
- ? `${displayMinutes} ${t("estimates.minutes", "min")}`
- : absoluteArrivalTime(displayMinutes)}
- </div>
- <div className="distance-info">
- {estimate.schedule && (
- <>
- {parseServiceId(estimate.schedule.serviceId)} ({getTripIdDisplay(estimate.schedule.tripId)})
- </>
- )}
-
- {estimate.schedule &&
- estimate.realTime &&
- estimate.realTime.distance >= 0 && <> &middot; </>}
-
- {estimate.realTime && estimate.realTime.distance >= 0 && (
- <>{formatDistance(estimate.realTime.distance)}</>
- )}
-
- {estimate.schedule &&
- estimate.realTime &&
- estimate.realTime.distance >= 0 && <> &middot; </>}
-
- {delayText}
- </div>
- </div>
- </div>
- </div>
- );
- })}
+ {sortedData.map((estimate, idx) => (
+ <ConsolidatedCirculationCard
+ key={idx}
+ estimate={estimate}
+ regionConfig={regionConfig}
+ />
+ ))}
</>
)}
</>
diff --git a/src/frontend/app/components/Stops/ConsolidatedCirculationListSkeleton.tsx b/src/frontend/app/components/Stops/ConsolidatedCirculationListSkeleton.tsx
index 43f02ca..90d92e2 100644
--- a/src/frontend/app/components/Stops/ConsolidatedCirculationListSkeleton.tsx
+++ b/src/frontend/app/components/Stops/ConsolidatedCirculationListSkeleton.tsx
@@ -34,7 +34,11 @@ export const ConsolidatedCirculationListSkeleton: React.FC = () => {
<div className="card-footer">
<Skeleton width="90%" height={14} />
- <Skeleton width="70%" height={14} style={{ marginTop: "4px" }} />
+ <Skeleton
+ width="70%"
+ height={14}
+ style={{ marginTop: "4px" }}
+ />
</div>
</div>
))}
diff --git a/src/frontend/app/data/LineColors.ts b/src/frontend/app/data/LineColors.ts
index f0599a3..85a7c54 100644
--- a/src/frontend/app/data/LineColors.ts
+++ b/src/frontend/app/data/LineColors.ts
@@ -58,15 +58,14 @@ const defaultLineColor: LineColorInfo = {
text: "#ffffff",
};
-export function getLineColor(
- region: RegionId,
- line: string,
-): LineColorInfo {
- let formattedLine = /^[a-zA-Z]/.test(line) ? line : `L${line}`;
+export function getLineColor(region: RegionId, line: string): LineColorInfo {
+ let formattedLine = /^[a-zA-Z]/.test(line) ? line : `L${line}`;
formattedLine = formattedLine.toLowerCase().trim();
if (region === "vigo") {
- return vigoLineColors[formattedLine.toLowerCase().trim()] ?? defaultLineColor;
+ return (
+ vigoLineColors[formattedLine.toLowerCase().trim()] ?? defaultLineColor
+ );
}
return defaultLineColor;
diff --git a/src/frontend/app/data/RegionConfig.ts b/src/frontend/app/data/RegionConfig.ts
index 0c188ef..8acfbbf 100644
--- a/src/frontend/app/data/RegionConfig.ts
+++ b/src/frontend/app/data/RegionConfig.ts
@@ -33,7 +33,7 @@ export const REGIONS: Record<RegionId, RegionConfig> = {
textColour: "#e72b37",
defaultZoom: 14,
showMeters: true,
- }
+ },
};
export const DEFAULT_REGION: RegionId = "vigo";
diff --git a/src/frontend/app/data/StopDataProvider.ts b/src/frontend/app/data/StopDataProvider.ts
index e3936b4..b4e877f 100644
--- a/src/frontend/app/data/StopDataProvider.ts
+++ b/src/frontend/app/data/StopDataProvider.ts
@@ -63,7 +63,7 @@ async function getStops(region: RegionId): Promise<Stop[]> {
const rawFav = localStorage.getItem(`favouriteStops_${region}`);
const favouriteStops = rawFav ? (JSON.parse(rawFav) as number[]) : [];
cachedStopsByRegion[region]!.forEach(
- (stop) => (stop.favourite = favouriteStops.includes(stop.stopId)),
+ (stop) => (stop.favourite = favouriteStops.includes(stop.stopId))
);
return cachedStopsByRegion[region]!;
}
@@ -71,7 +71,7 @@ async function getStops(region: RegionId): Promise<Stop[]> {
// New: get single stop by id
async function getStopById(
region: RegionId,
- stopId: number,
+ stopId: number
): Promise<Stop | undefined> {
await initStops(region);
const stop = stopsMapByRegion[region]?.[stopId];
@@ -99,7 +99,7 @@ function setCustomName(region: RegionId, stopId: number, label: string) {
customNamesByRegion[region][stopId] = label;
localStorage.setItem(
`customStopNames_${region}`,
- JSON.stringify(customNamesByRegion[region]),
+ JSON.stringify(customNamesByRegion[region])
);
}
@@ -108,7 +108,7 @@ function removeCustomName(region: RegionId, stopId: number) {
delete customNamesByRegion[region][stopId];
localStorage.setItem(
`customStopNames_${region}`,
- JSON.stringify(customNamesByRegion[region]),
+ JSON.stringify(customNamesByRegion[region])
);
}
}
@@ -129,7 +129,7 @@ function addFavourite(region: RegionId, stopId: number) {
favouriteStops.push(stopId);
localStorage.setItem(
`favouriteStops_${region}`,
- JSON.stringify(favouriteStops),
+ JSON.stringify(favouriteStops)
);
}
}
@@ -144,7 +144,7 @@ function removeFavourite(region: RegionId, stopId: number) {
const newFavouriteStops = favouriteStops.filter((id) => id !== stopId);
localStorage.setItem(
`favouriteStops_${region}`,
- JSON.stringify(newFavouriteStops),
+ JSON.stringify(newFavouriteStops)
);
}
@@ -175,7 +175,7 @@ function pushRecent(region: RegionId, stopId: number) {
localStorage.setItem(
`recentStops_${region}`,
- JSON.stringify(Array.from(recentStops)),
+ JSON.stringify(Array.from(recentStops))
);
}
diff --git a/src/frontend/app/maps/styleloader.ts b/src/frontend/app/maps/styleloader.ts
index d20fd31..43118a0 100644
--- a/src/frontend/app/maps/styleloader.ts
+++ b/src/frontend/app/maps/styleloader.ts
@@ -3,10 +3,12 @@ import type { Theme } from "~/AppContext";
export async function loadStyle(
styleName: string,
- colorScheme: Theme,
+ colorScheme: Theme
): Promise<StyleSpecification> {
if (colorScheme == "system") {
- const isDarkMode = window.matchMedia("(prefers-color-scheme: dark)").matches;
+ const isDarkMode = window.matchMedia(
+ "(prefers-color-scheme: dark)"
+ ).matches;
colorScheme = isDarkMode ? "dark" : "light";
}
diff --git a/src/frontend/app/routes/estimates-$id.tsx b/src/frontend/app/routes/estimates-$id.tsx
index 4efa797..e4006ef 100644
--- a/src/frontend/app/routes/estimates-$id.tsx
+++ b/src/frontend/app/routes/estimates-$id.tsx
@@ -38,7 +38,7 @@ interface ErrorInfo {
const loadData = async (
region: RegionId,
- stopId: string,
+ stopId: string
): Promise<Estimate[]> => {
const regionConfig = getRegionConfig(region);
const resp = await fetch(`${regionConfig.estimatesEndpoint}?id=${stopId}`, {
@@ -56,7 +56,7 @@ const loadData = async (
const loadTimetableData = async (
region: RegionId,
- stopId: string,
+ stopId: string
): Promise<ScheduledTable[]> => {
const regionConfig = getRegionConfig(region);
@@ -72,7 +72,7 @@ const loadTimetableData = async (
headers: {
Accept: "application/json",
},
- },
+ }
);
if (!resp.ok) {
@@ -201,7 +201,7 @@ export default function Estimates() {
StopDataProvider.pushRecent(region, parseInt(params.id ?? ""));
setFavourited(
- StopDataProvider.isFavourite(region, parseInt(params.id ?? "")),
+ StopDataProvider.isFavourite(region, parseInt(params.id ?? ""))
);
}, [params.id, region, loadEstimatesData, loadTimetableDataAsync]);
@@ -323,7 +323,7 @@ export default function Estimates() {
onRetry={loadEstimatesData}
title={t(
"errors.estimates_title",
- "Error al cargar estimaciones",
+ "Error al cargar estimaciones"
)}
/>
) : data ? (
diff --git a/src/frontend/app/routes/home.tsx b/src/frontend/app/routes/home.tsx
index 88c774b..2909999 100644
--- a/src/frontend/app/routes/home.tsx
+++ b/src/frontend/app/routes/home.tsx
@@ -29,7 +29,7 @@ export default function StopList() {
const randomPlaceholder = useMemo(
() => t("stoplist.search_placeholder"),
- [t],
+ [t]
);
const fuse = useMemo(
@@ -38,7 +38,7 @@ export default function StopList() {
threshold: 0.3,
keys: ["name.original", "name.intersect", "stopId"],
}),
- [data],
+ [data]
);
const requestUserLocation = useCallback(() => {
@@ -59,7 +59,7 @@ export default function StopList() {
{
enableHighAccuracy: false,
maximumAge: 5 * 60 * 1000,
- },
+ }
);
}, []);
@@ -117,7 +117,7 @@ export default function StopList() {
lat1: number,
lon1: number,
lat2: number,
- lon2: number,
+ lon2: number
) => {
const R = 6371000; // meters
const dLat = toRadians(lat2 - lat1);
@@ -145,7 +145,7 @@ export default function StopList() {
userLocation.latitude,
userLocation.longitude,
stop.latitude,
- stop.longitude,
+ stop.longitude
);
return { stop, distance };
@@ -183,7 +183,7 @@ export default function StopList() {
// Update favourite and recent stops with full data
const favStops = stopsWithFavourites.filter((stop) =>
- favouriteStopsIds.includes(stop.stopId),
+ favouriteStopsIds.includes(stop.stopId)
);
setFavouriteStops(favStops);
@@ -312,7 +312,7 @@ export default function StopList() {
)}
{!loading && data
? (userLocation ? sortedAllStops.slice(0, 6) : sortedAllStops).map(
- (stop) => <StopItem key={stop.stopId} stop={stop} />,
+ (stop) => <StopItem key={stop.stopId} stop={stop} />
)
: null}
</ul>
diff --git a/src/frontend/app/routes/map.tsx b/src/frontend/app/routes/map.tsx
index d520e5a..5a8c7a2 100644
--- a/src/frontend/app/routes/map.tsx
+++ b/src/frontend/app/routes/map.tsx
@@ -172,7 +172,17 @@ export default function StopMap() {
`stop-${region}-cancelled`,
`stop-${region}`,
],
- "icon-size": ["interpolate", ["linear"], ["zoom"], 13, 0.4, 14, 0.7, 18, 1.0],
+ "icon-size": [
+ "interpolate",
+ ["linear"],
+ ["zoom"],
+ 13,
+ 0.4,
+ 14,
+ 0.7,
+ 18,
+ 1.0,
+ ],
"icon-allow-overlap": true,
"icon-ignore-placement": true,
}}
diff --git a/src/frontend/app/routes/settings.tsx b/src/frontend/app/routes/settings.tsx
index d9c882d..2134b4c 100644
--- a/src/frontend/app/routes/settings.tsx
+++ b/src/frontend/app/routes/settings.tsx
@@ -127,7 +127,7 @@ export default function Settings() {
e.target.value as
| "regular"
| "grouped"
- | "experimental_consolidated",
+ | "experimental_consolidated"
)
}
>
@@ -198,7 +198,7 @@ export default function Settings() {
<p>
{t(
"about.region_change_message",
- "¿Estás seguro de que quieres cambiar la región? Serás redirigido a la lista de paradas.",
+ "¿Estás seguro de que quieres cambiar la región? Serás redirigido a la lista de paradas."
)}
</p>
<div className="modal-buttons">
diff --git a/src/frontend/app/routes/stops-$id.tsx b/src/frontend/app/routes/stops-$id.tsx
index 6e669ca..ac41250 100644
--- a/src/frontend/app/routes/stops-$id.tsx
+++ b/src/frontend/app/routes/stops-$id.tsx
@@ -42,7 +42,7 @@ interface ErrorInfo {
const loadConsolidatedData = async (
region: RegionId,
- stopId: string,
+ stopId: string
): Promise<ConsolidatedCirculation[]> => {
const regionConfig = getRegionConfig(region);
const resp = await fetch(
@@ -51,7 +51,7 @@ const loadConsolidatedData = async (
headers: {
Accept: "application/json",
},
- },
+ }
);
if (!resp.ok) {
@@ -151,7 +151,7 @@ export default function Estimates() {
StopDataProvider.pushRecent(region, parseInt(params.id ?? ""));
setFavourited(
- StopDataProvider.isFavourite(region, parseInt(params.id ?? "")),
+ StopDataProvider.isFavourite(region, parseInt(params.id ?? ""))
);
}, [params.id, region, loadData]);
@@ -240,7 +240,7 @@ export default function Estimates() {
onRetry={loadData}
title={t(
"errors.estimates_title",
- "Error al cargar estimaciones",
+ "Error al cargar estimaciones"
)}
/>
) : data ? (
diff --git a/src/frontend/app/routes/timetable-$id.tsx b/src/frontend/app/routes/timetable-$id.tsx
index da7a2e7..af5e42a 100644
--- a/src/frontend/app/routes/timetable-$id.tsx
+++ b/src/frontend/app/routes/timetable-$id.tsx
@@ -26,7 +26,7 @@ interface ErrorInfo {
const loadTimetableData = async (
region: RegionId,
- stopId: string,
+ stopId: string
): Promise<ScheduledTable[]> => {
const regionConfig = getRegionConfig(region);
@@ -45,7 +45,7 @@ const loadTimetableData = async (
headers: {
Accept: "application/json",
},
- },
+ }
);
if (!resp.ok) {
@@ -65,18 +65,18 @@ const timeToMinutes = (time: string): number => {
const filterTimetableData = (
data: ScheduledTable[],
currentTime: string,
- showPast: boolean = false,
+ showPast: boolean = false
): ScheduledTable[] => {
if (showPast) return data;
const currentMinutes = timeToMinutes(currentTime);
const sortedData = [...data].sort(
- (a, b) => timeToMinutes(a.calling_time) - timeToMinutes(b.calling_time),
+ (a, b) => timeToMinutes(a.calling_time) - timeToMinutes(b.calling_time)
);
// Find the current position
const currentIndex = sortedData.findIndex(
- (entry) => timeToMinutes(entry.calling_time) >= currentMinutes,
+ (entry) => timeToMinutes(entry.calling_time) >= currentMinutes
);
if (currentIndex === -1) {
@@ -161,7 +161,7 @@ export default function Timetable() {
const filteredData = filterTimetableData(
timetableData,
currentTime,
- showPastEntries,
+ showPastEntries
);
const parseError = (error: any): ErrorInfo => {
@@ -210,11 +210,11 @@ export default function Timetable() {
const currentMinutes = timeToMinutes(currentTime);
const sortedData = [...timetableBody].sort(
(a, b) =>
- timeToMinutes(a.calling_time) - timeToMinutes(b.calling_time),
+ timeToMinutes(a.calling_time) - timeToMinutes(b.calling_time)
);
const nextIndex = sortedData.findIndex(
- (entry) => timeToMinutes(entry.calling_time) >= currentMinutes,
+ (entry) => timeToMinutes(entry.calling_time) >= currentMinutes
);
if (nextIndex !== -1 && nextEntryRef.current) {
@@ -293,13 +293,13 @@ export default function Timetable() {
<p>
{t(
"timetable.noDataAvailable",
- "No hay datos de horarios disponibles para hoy",
+ "No hay datos de horarios disponibles para hoy"
)}
</p>
<p className="error-detail">
{t(
"timetable.errorDetail",
- "Los horarios teóricos se actualizan diariamente. Inténtalo más tarde.",
+ "Los horarios teóricos se actualizan diariamente. Inténtalo más tarde."
)}
</p>
</div>
diff --git a/src/frontend/package-lock.json b/src/frontend/package-lock.json
index 3b8c5a7..d7d9279 100644
--- a/src/frontend/package-lock.json
+++ b/src/frontend/package-lock.json
@@ -9,7 +9,8 @@
"version": "0.0.0",
"dependencies": {
"@fontsource-variable/roboto": "^5.2.8",
- "@rollup/rollup-linux-x64-gnu": "^4.53.2",
+ "@react-router/node": "^7.9.4",
+ "@react-router/serve": "^7.9.4",
"framer-motion": "^12.23.24",
"fuse.js": "^7.1.0",
"i18next-browser-languagedetector": "^8.2.0",
@@ -83,7 +84,6 @@
"integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@babel/code-frame": "^7.27.1",
"@babel/generator": "^7.28.3",
@@ -1611,7 +1611,6 @@
"integrity": "sha512-qIT8hp1RJ0VAHyXpfuwoO31b9evrjPLRhUugqYJ7BZLpyAwhRsJIaQvvj60yZwWBMF2/3LdZu7M39rf0FhL6Iw==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@mjackson/node-fetch-server": "^0.2.0",
"@react-router/express": "7.9.6",
@@ -1993,7 +1992,6 @@
"integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"undici-types": "~7.16.0"
}
@@ -2004,7 +2002,6 @@
"integrity": "sha512-tBFxBp9Nfyy5rsmefN+WXc1JeW/j2BpBHFdLZbEVfs9wn3E3NRFxwV0pJg8M1qQAexFpvz73hJXFofV0ZAu92A==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"csstype": "^3.0.2"
}
@@ -2075,7 +2072,6 @@
"integrity": "sha512-tK3GPFWbirvNgsNKto+UmB/cRtn6TZfyw0D6IKrW55n6Vbs7KJoZtI//kpTKzE/DUmmnAFD8/Ca46s7Obs92/w==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "8.46.4",
"@typescript-eslint/types": "8.46.4",
@@ -2382,7 +2378,6 @@
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"dev": true,
"license": "MIT",
- "peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -2623,7 +2618,6 @@
}
],
"license": "MIT",
- "peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.8.9",
"caniuse-lite": "^1.0.30001746",
@@ -3188,7 +3182,6 @@
"integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.1",
@@ -4290,7 +4283,6 @@
"integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==",
"dev": true,
"license": "MIT",
- "peer": true,
"bin": {
"jiti": "lib/jiti-cli.mjs"
}
@@ -4400,8 +4392,7 @@
"version": "1.9.4",
"resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz",
"integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==",
- "license": "BSD-2-Clause",
- "peer": true
+ "license": "BSD-2-Clause"
},
"node_modules/leaflet.markercluster": {
"version": "1.5.3",
@@ -4481,7 +4472,6 @@
"integrity": "sha512-2B/H+DpjDO2NzsvNQYVIuKPyijhYJW/Hk3W+6BloAzXhm6nqXC3gVrntPPgP6hRH8f8j23nbNLOtM6OKplHwRQ==",
"dev": true,
"license": "BSD-3-Clause",
- "peer": true,
"dependencies": {
"@mapbox/geojson-rewind": "^0.5.2",
"@mapbox/jsonlint-lines-primitives": "^2.0.2",
@@ -5107,7 +5097,6 @@
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true,
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=12"
},
@@ -5324,7 +5313,6 @@
"resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz",
"integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==",
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -5334,7 +5322,6 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz",
"integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==",
"license": "MIT",
- "peer": true,
"dependencies": {
"scheduler": "^0.27.0"
},
@@ -5466,7 +5453,6 @@
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.9.6.tgz",
"integrity": "sha512-Y1tUp8clYRXpfPITyuifmSoE2vncSME18uVLgaqyxh9H35JWpIfzHo+9y3Fzh5odk/jxPW29IgLgzcdwxGqyNA==",
"license": "MIT",
- "peer": true,
"dependencies": {
"cookie": "^1.0.1",
"set-cookie-parser": "^2.6.0"
@@ -6321,7 +6307,6 @@
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"devOptional": true,
"license": "Apache-2.0",
- "peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -6516,7 +6501,6 @@
"integrity": "sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"esbuild": "^0.25.0",
"fdir": "^6.5.0",
@@ -6792,7 +6776,6 @@
"integrity": "sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==",
"dev": true,
"license": "MIT",
- "peer": true,
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
diff --git a/src/frontend/public/maps/styles/openfreemap-dark.json b/src/frontend/public/maps/styles/openfreemap-dark.json
index 1a46976..2803f1a 100644
--- a/src/frontend/public/maps/styles/openfreemap-dark.json
+++ b/src/frontend/public/maps/styles/openfreemap-dark.json
@@ -3346,7 +3346,7 @@
"interpolate",
["linear"],
["get", "zoom"],
- 0, 11,
+ 0, 1,
14, 1,
16, 0.8,
18, 0.6,
diff --git a/src/frontend/public/maps/styles/openfreemap-light.json b/src/frontend/public/maps/styles/openfreemap-light.json
index 0141ce4..d616fe7 100644
--- a/src/frontend/public/maps/styles/openfreemap-light.json
+++ b/src/frontend/public/maps/styles/openfreemap-light.json
@@ -6995,7 +6995,7 @@
"interpolate",
["linear"],
["get", "zoom"],
- 0, 11,
+ 0, 1,
14, 1,
16, 0.8,
18, 0.6,
diff --git a/src/frontend/public/pwa-worker.js b/src/frontend/public/pwa-worker.js
index 13c1978..eb69b84 100644
--- a/src/frontend/public/pwa-worker.js
+++ b/src/frontend/public/pwa-worker.js
@@ -14,7 +14,7 @@ self.addEventListener("install", (event) => {
caches
.open(STATIC_CACHE_NAME)
.then((cache) => cache.addAll(STATIC_CACHE_ASSETS))
- .then(() => self.skipWaiting()),
+ .then(() => self.skipWaiting())
);
});
@@ -27,7 +27,7 @@ self.addEventListener("activate", (event) => {
if (name !== STATIC_CACHE_NAME) {
return caches.delete(name);
}
- }),
+ })
);
await self.clients.claim();