From 276e73412abef28c222c52a84334d49f5e414f3c Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Mon, 17 Nov 2025 23:39:08 +0100 Subject: 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 --- src/frontend/app/AppContext.tsx | 8 +- src/frontend/app/components/ErrorDisplay.tsx | 8 +- src/frontend/app/components/GroupedTable.tsx | 4 +- src/frontend/app/components/NavBar.tsx | 2 +- src/frontend/app/components/PullToRefresh.tsx | 8 +- src/frontend/app/components/RegularTable.tsx | 2 +- src/frontend/app/components/SchedulesTable.tsx | 6 +- src/frontend/app/components/StopMapSheet.css | 27 +++- src/frontend/app/components/StopMapSheet.tsx | 151 +++++++++++++---- src/frontend/app/components/StopSheet.tsx | 105 ++++-------- .../Stops/ConsolidatedCirculationCard.tsx | 180 +++++++++++++++++++++ .../Stops/ConsolidatedCirculationList.css | 5 +- .../Stops/ConsolidatedCirculationList.tsx | 174 ++------------------ .../Stops/ConsolidatedCirculationListSkeleton.tsx | 6 +- src/frontend/app/data/LineColors.ts | 11 +- src/frontend/app/data/RegionConfig.ts | 2 +- src/frontend/app/data/StopDataProvider.ts | 14 +- src/frontend/app/maps/styleloader.ts | 6 +- src/frontend/app/routes/estimates-$id.tsx | 10 +- src/frontend/app/routes/home.tsx | 14 +- src/frontend/app/routes/map.tsx | 12 +- src/frontend/app/routes/settings.tsx | 4 +- src/frontend/app/routes/stops-$id.tsx | 8 +- src/frontend/app/routes/timetable-$id.tsx | 20 +-- src/frontend/package-lock.json | 23 +-- .../public/maps/styles/openfreemap-dark.json | 2 +- .../public/maps/styles/openfreemap-light.json | 2 +- src/frontend/public/pwa-worker.js | 4 +- 28 files changed, 456 insertions(+), 362 deletions(-) create mode 100644 src/frontend/app/components/Stops/ConsolidatedCirculationCard.tsx (limited to 'src/frontend') 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(() => { @@ -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 = ({ 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 = ({ acc[estimate.line].push(estimate); return acc; }, - {} as Record, + {} as Record ); const sortedLines = Object.keys(groupedEstimates).sort((a, b) => { @@ -72,7 +72,7 @@ export const GroupedTable: React.FC = ({ )} - )), + )) )} 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 = ({ htmlScroll, bodyScroll, containerScroll, - parentScroll, + parentScroll ); if (maxScroll > 0 || isRefreshing) { @@ -60,7 +60,7 @@ export const PullToRefresh: React.FC = ({ startY.current = e.touches[0].clientY; setIsPulling(true); }, - [isRefreshing], + [isRefreshing] ); const handleTouchMove = useCallback( @@ -78,7 +78,7 @@ export const PullToRefresh: React.FC = ({ htmlScroll, bodyScroll, containerScroll, - parentScroll, + parentScroll ); if (maxScroll > 10) { @@ -116,7 +116,7 @@ export const PullToRefresh: React.FC = ({ 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 = ({ { 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 = ({ 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 = ({ }); }, () => {}, - { 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 = ({ }); }, () => {}, - { enableHighAccuracy: true, maximumAge: 30000, timeout: 10000 }, + { enableHighAccuracy: true, maximumAge: 30000, timeout: 10000 } ); } catch {} return () => { @@ -158,7 +176,7 @@ export const StopMap: React.FC = ({ 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 = ({ 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 = ({ 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 = ({ 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 = ({ {/* Stop marker (center) */} {stop.latitude && stop.longitude && ( - +
- - + + - + @@ -274,7 +328,11 @@ export const StopMap: React.FC = ({ {/* User position marker (if available) */} {userPosition && ( - +
@@ -290,7 +348,12 @@ export const StopMap: React.FC = ({ 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 = ({ const showLabel = zoom >= 13; const labelGap = gaps[idx] ?? baseGap; return ( - +
= ({ 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))", + }} > - + {showLabel && (
= ({ )} {/* Floating controls */}
-
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 => { + stopId: number +): Promise => { 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 = ({ }) => { const { t } = useTranslation(); const { region } = useApp(); - const [data, setData] = useState(null); + const regionConfig = getRegionConfig(region); + const [data, setData] = useState(null); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [lastUpdated, setLastUpdated] = useState(null); @@ -82,7 +87,7 @@ export const StopSheet: React.FC = ({ 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 = ({ } }, [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 ( @@ -158,7 +145,7 @@ export const StopSheet: React.FC = ({ 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 = ({ ) : (
{limitedEstimates.map((estimate, idx) => ( -
-
- -
-
-
- {estimate.route} -
-
-
-
- - {formatTime(estimate.minutes)} -
- {REGIONS[region].showMeters && - estimate.meters >= 0 && ( -
- {formatDistance(estimate.meters)} -
- )} -
-
+ ))}
)}
- - ) : null}
@@ -236,14 +202,11 @@ export const StopSheet: React.FC = ({ - {t( - "map.view_all_estimates", - "Ver todas las estimaciones", - )} + {t("map.view_all_estimates", "Ver todas las estimaciones")}
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 ( +
+
+
+ +
+ +
+ {estimate.route} +
+ +
+
+ + {estimate.realTime + ? `${displayMinutes} ${t("estimates.minutes", "min")}` + : absoluteArrivalTime(displayMinutes)} +
+
+ {estimate.schedule && ( + <> + {parseServiceId(estimate.schedule.serviceId)} ( + {getTripIdDisplay(estimate.schedule.tripId)}) + + )} + + {estimate.schedule && + estimate.realTime && + estimate.realTime.distance >= 0 && <> · } + + {estimate.realTime && estimate.realTime.distance >= 0 && ( + <>{formatDistance(estimate.realTime.distance)} + )} + + {estimate.schedule && + estimate.realTime && + estimate.realTime.distance >= 0 && <> · } + + {delayText} +
+
+
+
+ ); +}; 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 = ({ data, dataDate, @@ -76,65 +18,10 @@ export const ConsolidatedCirculationList: React.FC = ({ }) => { 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 = ({
) : ( <> - {sortedData.map((estimate, idx) => { - const displayMinutes = - estimate.realTime?.minutes ?? estimate.schedule?.minutes ?? 0; - const timeClass = getTimeClass(estimate); - const delayText = getDelayText(estimate); - - return ( -
-
-
- -
- -
- {estimate.route} -
- -
-
- - {estimate.realTime - ? `${displayMinutes} ${t("estimates.minutes", "min")}` - : absoluteArrivalTime(displayMinutes)} -
-
- {estimate.schedule && ( - <> - {parseServiceId(estimate.schedule.serviceId)} ({getTripIdDisplay(estimate.schedule.tripId)}) - - )} - - {estimate.schedule && - estimate.realTime && - estimate.realTime.distance >= 0 && <> · } - - {estimate.realTime && estimate.realTime.distance >= 0 && ( - <>{formatDistance(estimate.realTime.distance)} - )} - - {estimate.schedule && - estimate.realTime && - estimate.realTime.distance >= 0 && <> · } - - {delayText} -
-
-
-
- ); - })} + {sortedData.map((estimate, idx) => ( + + ))} )} 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 = () => {
- +
))} 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 = { 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 { 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 { // New: get single stop by id async function getStopById( region: RegionId, - stopId: number, + stopId: number ): Promise { 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 { 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 => { 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 => { 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) => , + (stop) => ) : null} 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() {

{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." )}

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 => { 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 => { 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() {

{t( "timetable.noDataAvailable", - "No hay datos de horarios disponibles para hoy", + "No hay datos de horarios disponibles para hoy" )}

{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." )}

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(); -- cgit v1.3