aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/frontend/app/i18n/locales/en-GB.json11
-rw-r--r--src/frontend/app/i18n/locales/es-ES.json11
-rw-r--r--src/frontend/app/i18n/locales/gl-ES.json11
-rw-r--r--src/frontend/app/routes/timetable-$id.css75
-rw-r--r--src/frontend/app/routes/timetable-$id.tsx275
-rw-r--r--src/frontend/package-lock.json21
6 files changed, 319 insertions, 85 deletions
diff --git a/src/frontend/app/i18n/locales/en-GB.json b/src/frontend/app/i18n/locales/en-GB.json
index 12c8121..992a311 100644
--- a/src/frontend/app/i18n/locales/en-GB.json
+++ b/src/frontend/app/i18n/locales/en-GB.json
@@ -9,9 +9,9 @@
"data_source_middle": "under license",
"settings": "Settings",
"theme": "Mode:",
- "theme_light": "Light",
- "theme_dark": "Dark",
- "theme_system": "System",
+ "theme_light": "Light",
+ "theme_dark": "Dark",
+ "theme_system": "System",
"table_style": "Table style:",
"table_style_regular": "Show in order",
"table_style_grouped": "Group by line",
@@ -79,7 +79,10 @@
"loadError": "Error loading timetables",
"errorDetail": "Theoretical timetables are updated daily. Please try again later.",
"showPast": "Show all",
- "hidePast": "Hide past"
+ "hidePast": "Hide past",
+ "goToNow": "Go to now",
+ "scrollUp": "Scroll up",
+ "scrollDown": "Scroll down"
},
"map": {
"popup_title": "Stop",
diff --git a/src/frontend/app/i18n/locales/es-ES.json b/src/frontend/app/i18n/locales/es-ES.json
index e929382..1df399d 100644
--- a/src/frontend/app/i18n/locales/es-ES.json
+++ b/src/frontend/app/i18n/locales/es-ES.json
@@ -9,9 +9,9 @@
"data_source_middle": "bajo licencia",
"settings": "Ajustes",
"theme": "Modo:",
- "theme_light": "Claro",
- "theme_dark": "Oscuro",
- "theme_system": "Sistema",
+ "theme_light": "Claro",
+ "theme_dark": "Oscuro",
+ "theme_system": "Sistema",
"table_style": "Estilo de tabla:",
"table_style_regular": "Mostrar por orden",
"table_style_grouped": "Agrupar por línea",
@@ -79,7 +79,10 @@
"loadError": "Error al cargar los horarios",
"errorDetail": "Los horarios teóricos se actualizan diariamente. Inténtalo más tarde.",
"showPast": "Mostrar todos",
- "hidePast": "Ocultar pasados"
+ "hidePast": "Ocultar pasados",
+ "goToNow": "Ir a ahora",
+ "scrollUp": "Subir",
+ "scrollDown": "Bajar"
},
"map": {
"popup_title": "Parada",
diff --git a/src/frontend/app/i18n/locales/gl-ES.json b/src/frontend/app/i18n/locales/gl-ES.json
index ed1ef53..5a04aff 100644
--- a/src/frontend/app/i18n/locales/gl-ES.json
+++ b/src/frontend/app/i18n/locales/gl-ES.json
@@ -9,9 +9,9 @@
"data_source_middle": "baixo licenza",
"settings": "Axustes",
"theme": "Modo:",
- "theme_light": "Claro",
- "theme_dark": "Escuro",
- "theme_system": "Sistema",
+ "theme_light": "Claro",
+ "theme_dark": "Escuro",
+ "theme_system": "Sistema",
"table_style": "Estilo de táboa:",
"table_style_regular": "Mostrar por orde",
"table_style_grouped": "Agrupar por liña",
@@ -79,7 +79,10 @@
"loadError": "Erro ao cargar os horarios",
"errorDetail": "Os horarios teóricos actualízanse diariamente. Inténtao máis tarde.",
"showPast": "Mostrar todos",
- "hidePast": "Ocultar pasados"
+ "hidePast": "Ocultar pasados",
+ "goToNow": "Ir a agora",
+ "scrollUp": "Subir",
+ "scrollDown": "Baixar"
},
"map": {
"popup_title": "Parada",
diff --git a/src/frontend/app/routes/timetable-$id.css b/src/frontend/app/routes/timetable-$id.css
index c834633..b4e231a 100644
--- a/src/frontend/app/routes/timetable-$id.css
+++ b/src/frontend/app/routes/timetable-$id.css
@@ -38,6 +38,10 @@
.timetable-full-content {
margin-top: 1rem;
+ overflow-y: auto;
+ max-height: calc(100vh - 250px);
+ position: relative;
+ padding-bottom: 80px; /* Space for FAB */
}
.error-message {
@@ -149,3 +153,74 @@
padding: 1rem;
}
}
+
+/* Floating Action Button */
+.fab-container {
+ position: fixed;
+ bottom: 80px;
+ right: 20px;
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+ z-index: 1000;
+}
+
+.fab {
+ width: 56px;
+ height: 56px;
+ border-radius: 50%;
+ border: none;
+ background-color: var(--button-background-color, #007bff);
+ color: white;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ cursor: pointer;
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
+ transition: all 0.3s ease;
+ animation: fadeIn 0.3s ease;
+}
+
+.fab:hover {
+ background-color: var(--button-hover-background-color, #0069d9);
+ box-shadow: 0 6px 16px rgba(0, 0, 0, 0.2);
+ transform: scale(1.05);
+}
+
+.fab:active {
+ transform: scale(0.95);
+}
+
+.fab-icon {
+ width: 24px;
+ height: 24px;
+}
+
+@keyframes fadeIn {
+ from {
+ opacity: 0;
+ transform: translateY(10px);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
+
+/* Adjust FAB position on mobile */
+@media (max-width: 768px) {
+ .fab-container {
+ bottom: 70px;
+ right: 16px;
+ }
+
+ .fab {
+ width: 48px;
+ height: 48px;
+ }
+
+ .fab-icon {
+ width: 20px;
+ height: 20px;
+ }
+}
diff --git a/src/frontend/app/routes/timetable-$id.tsx b/src/frontend/app/routes/timetable-$id.tsx
index 5ecebc8..c8f0125 100644
--- a/src/frontend/app/routes/timetable-$id.tsx
+++ b/src/frontend/app/routes/timetable-$id.tsx
@@ -1,7 +1,14 @@
import { useEffect, useState, useRef } from "react";
import { useParams, Link } from "react-router";
import StopDataProvider from "../data/StopDataProvider";
-import { ArrowLeft, Eye, EyeOff } from "lucide-react";
+import {
+ ArrowLeft,
+ Eye,
+ EyeOff,
+ ChevronUp,
+ ChevronDown,
+ Clock,
+} from "lucide-react";
import { type ScheduledTable } from "~/components/SchedulesTable";
import { TimetableSkeleton } from "~/components/TimetableSkeleton";
import { ErrorDisplay } from "~/components/ErrorDisplay";
@@ -12,12 +19,15 @@ import { useApp } from "~/AppContext";
import "./timetable-$id.css";
interface ErrorInfo {
- type: 'network' | 'server' | 'unknown';
+ type: "network" | "server" | "unknown";
status?: number;
message?: string;
}
-const loadTimetableData = async (region: RegionId, stopId: string): Promise<ScheduledTable[]> => {
+const loadTimetableData = async (
+ region: RegionId,
+ stopId: string,
+): Promise<ScheduledTable[]> => {
const regionConfig = getRegionConfig(region);
// Check if timetable is available for this region
@@ -26,14 +36,17 @@ const loadTimetableData = async (region: RegionId, stopId: string): Promise<Sche
}
// Add delay to see skeletons in action (remove in production)
- await new Promise(resolve => setTimeout(resolve, 1000));
+ await new Promise((resolve) => setTimeout(resolve, 1000));
- const today = new Date().toISOString().split('T')[0]; // YYYY-MM-DD format
- const resp = await fetch(`${regionConfig.timetableEndpoint}?date=${today}&stopId=${stopId}`, {
- headers: {
- Accept: "application/json",
+ const today = new Date().toISOString().split("T")[0]; // YYYY-MM-DD format
+ const resp = await fetch(
+ `${regionConfig.timetableEndpoint}?date=${today}&stopId=${stopId}`,
+ {
+ headers: {
+ Accept: "application/json",
+ },
},
- });
+ );
if (!resp.ok) {
throw new Error(`HTTP ${resp.status}: ${resp.statusText}`);
@@ -44,22 +57,26 @@ const loadTimetableData = async (region: RegionId, stopId: string): Promise<Sche
// Utility function to compare times
const timeToMinutes = (time: string): number => {
- const [hours, minutes] = time.split(':').map(Number);
+ const [hours, minutes] = time.split(":").map(Number);
return hours * 60 + minutes;
};
// Filter past entries (keep only a few recent past ones)
-const filterTimetableData = (data: ScheduledTable[], currentTime: string, showPast: boolean = false): ScheduledTable[] => {
+const filterTimetableData = (
+ data: ScheduledTable[],
+ currentTime: string,
+ 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)
+ const sortedData = [...data].sort(
+ (a, b) => timeToMinutes(a.calling_time) - timeToMinutes(b.calling_time),
);
// Find the current position
- const currentIndex = sortedData.findIndex(entry =>
- timeToMinutes(entry.calling_time) >= currentMinutes
+ const currentIndex = sortedData.findIndex(
+ (entry) => timeToMinutes(entry.calling_time) >= currentMinutes,
);
if (currentIndex === -1) {
@@ -74,11 +91,11 @@ const filterTimetableData = (data: ScheduledTable[], currentTime: string, showPa
// 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 parts = serviceId.split("_");
+ if (parts.length === 0) return "";
const lastPart = parts[parts.length - 1];
- if (lastPart.length < 6) return '';
+ if (lastPart.length < 6) return "";
const last6 = lastPart.slice(-6);
const lineCode = last6.slice(0, 3);
@@ -92,20 +109,40 @@ const parseServiceId = (serviceId: string): string => {
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;
- default: displayLine = `L${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;
+ default:
+ displayLine = `L${lineNumber}`;
}
return `${displayLine}-${turnNumber}`;
};
+// Scroll threshold for showing FAB buttons (in pixels)
+const SCROLL_THRESHOLD = 100;
+
export default function Timetable() {
const { t } = useTranslation();
const { region } = useApp();
@@ -116,37 +153,48 @@ export default function Timetable() {
const [loading, setLoading] = useState(true);
const [error, setError] = useState<ErrorInfo | null>(null);
const [showPastEntries, setShowPastEntries] = useState(false);
+ const [showScrollTop, setShowScrollTop] = useState(false);
+ const [showScrollBottom, setShowScrollBottom] = useState(false);
+ const [showGoToNow, setShowGoToNow] = useState(false);
const nextEntryRef = useRef<HTMLDivElement>(null);
+ const containerRef = useRef<HTMLDivElement>(null);
const regionConfig = getRegionConfig(region);
const currentTime = new Date().toTimeString().slice(0, 8); // HH:MM:SS
- const filteredData = filterTimetableData(timetableData, currentTime, showPastEntries);
+ const filteredData = filterTimetableData(
+ timetableData,
+ currentTime,
+ showPastEntries,
+ );
const parseError = (error: any): ErrorInfo => {
if (!navigator.onLine) {
- return { type: 'network', message: 'No internet connection' };
+ return { type: "network", message: "No internet connection" };
}
- if (error.message?.includes('Failed to fetch') || error.message?.includes('NetworkError')) {
- return { type: 'network' };
+ if (
+ error.message?.includes("Failed to fetch") ||
+ error.message?.includes("NetworkError")
+ ) {
+ return { type: "network" };
}
- if (error.message?.includes('HTTP')) {
+ if (error.message?.includes("HTTP")) {
const statusMatch = error.message.match(/HTTP (\d+):/);
const status = statusMatch ? parseInt(statusMatch[1]) : undefined;
- return { type: 'server', status };
+ return { type: "server", status };
}
- return { type: 'unknown', message: error.message };
+ return { type: "unknown", message: error.message };
};
const loadData = async () => {
// Check if timetable is available for this region
if (!regionConfig.timetableEndpoint) {
setError({
- type: 'server',
+ type: "server",
status: 501,
- message: 'Timetable not available for this region'
+ message: "Timetable not available for this region",
});
setLoading(false);
return;
@@ -163,24 +211,25 @@ export default function Timetable() {
// Scroll to next entry after a short delay to allow rendering
setTimeout(() => {
const currentMinutes = timeToMinutes(currentTime);
- const sortedData = [...timetableBody].sort((a, b) =>
- timeToMinutes(a.calling_time) - timeToMinutes(b.calling_time)
+ const sortedData = [...timetableBody].sort(
+ (a, b) =>
+ timeToMinutes(a.calling_time) - timeToMinutes(b.calling_time),
);
- const nextIndex = sortedData.findIndex(entry =>
- timeToMinutes(entry.calling_time) >= currentMinutes
+ const nextIndex = sortedData.findIndex(
+ (entry) => timeToMinutes(entry.calling_time) >= currentMinutes,
);
if (nextIndex !== -1 && nextEntryRef.current) {
nextEntryRef.current.scrollIntoView({
- behavior: 'smooth',
- block: 'center'
+ behavior: "smooth",
+ block: "center",
});
}
}, 500);
}
} catch (err) {
- console.error('Error loading timetable data:', err);
+ console.error("Error loading timetable data:", err);
setError(parseError(err));
setTimetableData([]);
} finally {
@@ -193,6 +242,74 @@ export default function Timetable() {
setCustomName(StopDataProvider.getCustomName(region, stopIdNum));
}, [params.id, region]);
+ // Handle scroll events to update FAB visibility
+ useEffect(() => {
+ const handleScroll = () => {
+ if (
+ !containerRef.current ||
+ loading ||
+ error ||
+ timetableData.length === 0
+ ) {
+ return;
+ }
+
+ const container = containerRef.current;
+ const scrollTop = container.scrollTop;
+ const scrollHeight = container.scrollHeight;
+ const clientHeight = container.clientHeight;
+ const scrollBottom = scrollHeight - scrollTop - clientHeight;
+
+ // Threshold for showing scroll buttons (in pixels)
+ const threshold = 100;
+
+ // Show scroll top button when scrolled down
+ setShowScrollTop(scrollTop > threshold);
+
+ // Show scroll bottom button when not at bottom
+ setShowScrollBottom(scrollBottom > threshold);
+
+ // Check if next entry (current time) is visible
+ if (nextEntryRef.current) {
+ const rect = nextEntryRef.current.getBoundingClientRect();
+ const containerRect = container.getBoundingClientRect();
+ const isNextVisible =
+ rect.top >= containerRect.top && rect.bottom <= containerRect.bottom;
+
+ setShowGoToNow(!isNextVisible);
+ }
+ };
+
+ const container = containerRef.current;
+ if (container) {
+ container.addEventListener("scroll", handleScroll);
+ // Initial check
+ handleScroll();
+
+ return () => {
+ container.removeEventListener("scroll", handleScroll);
+ };
+ }
+ }, [loading, error, timetableData]);
+
+ const scrollToTop = () => {
+ containerRef.current?.scrollTo({ top: 0, behavior: "smooth" });
+ };
+
+ const scrollToBottom = () => {
+ containerRef.current?.scrollTo({
+ top: containerRef.current.scrollHeight,
+ behavior: "smooth",
+ });
+ };
+
+ const scrollToNow = () => {
+ nextEntryRef.current?.scrollIntoView({
+ behavior: "smooth",
+ block: "center",
+ });
+ };
+
if (loading) {
return (
<div className="page-container">
@@ -242,16 +359,24 @@ export default function Timetable() {
</div>
) : timetableData.length === 0 ? (
<div className="error-message">
- <p>{t("timetable.noDataAvailable", "No hay datos de horarios disponibles para hoy")}</p>
+ <p>
+ {t(
+ "timetable.noDataAvailable",
+ "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.")}
+ {t(
+ "timetable.errorDetail",
+ "Los horarios teóricos se actualizan diariamente. Inténtalo más tarde.",
+ )}
</p>
</div>
) : (
- <div className="timetable-full-content">
+ <div className="timetable-full-content" ref={containerRef}>
<div className="timetable-controls">
<button
- className={`past-toggle ${showPastEntries ? 'active' : ''}`}
+ className={`past-toggle ${showPastEntries ? "active" : ""}`}
onClick={() => setShowPastEntries(!showPastEntries)}
>
{showPastEntries ? (
@@ -274,6 +399,42 @@ export default function Timetable() {
currentTime={currentTime}
nextEntryRef={nextEntryRef}
/>
+
+ {/* Floating Action Button */}
+ {(showGoToNow || showScrollTop || showScrollBottom) && (
+ <div className="fab-container">
+ {showGoToNow && !showScrollTop && !showScrollBottom && (
+ <button
+ className="fab fab-now"
+ onClick={scrollToNow}
+ title={t("timetable.goToNow", "Ir a ahora")}
+ aria-label={t("timetable.goToNow", "Ir a ahora")}
+ >
+ <Clock className="fab-icon" />
+ </button>
+ )}
+ {showScrollTop && (
+ <button
+ className="fab fab-up"
+ onClick={scrollToTop}
+ title={t("timetable.scrollUp", "Subir")}
+ aria-label={t("timetable.scrollUp", "Subir")}
+ >
+ <ChevronUp className="fab-icon" />
+ </button>
+ )}
+ {showScrollBottom && !showScrollTop && (
+ <button
+ className="fab fab-down"
+ onClick={scrollToBottom}
+ title={t("timetable.scrollDown", "Bajar")}
+ aria-label={t("timetable.scrollDown", "Bajar")}
+ >
+ <ChevronDown className="fab-icon" />
+ </button>
+ )}
+ </div>
+ )}
</div>
)}
</div>
@@ -301,7 +462,11 @@ const TimetableTableWithScroll: React.FC<{
{data.map((entry, index) => {
const entryMinutes = timeToMinutes(entry.calling_time);
const isPast = entryMinutes < nowMinutes;
- const isNext = !isPast && (index === 0 || timeToMinutes(data[index - 1]?.calling_time || '00:00:00') < nowMinutes);
+ const isNext =
+ !isPast &&
+ (index === 0 ||
+ timeToMinutes(data[index - 1]?.calling_time || "00:00:00") <
+ nowMinutes);
return (
<div
@@ -312,8 +477,8 @@ const TimetableTableWithScroll: React.FC<{
background: isPast
? "var(--surface-past, #f3f3f3)"
: isNext
- ? "var(--surface-next, #e8f5e8)"
- : "var(--surface-future, #fff)"
+ ? "var(--surface-next, #e8f5e8)"
+ : "var(--surface-future, #fff)",
}}
>
<div className="card-header">
@@ -325,7 +490,9 @@ const TimetableTableWithScroll: React.FC<{
{entry.route && entry.route.trim() ? (
<strong>{entry.route}</strong>
) : (
- <strong>{t("timetable.noDestination", "Línea")} {entry.line}</strong>
+ <strong>
+ {t("timetable.noDestination", "Línea")} {entry.line}
+ </strong>
)}
</div>
@@ -341,7 +508,7 @@ const TimetableTableWithScroll: React.FC<{
<div className="card-body">
{!isPast && entry.next_streets.length > 0 && (
<div className="route-streets">
- {entry.next_streets.join(' — ')}
+ {entry.next_streets.join(" — ")}
</div>
)}
</div>
@@ -351,7 +518,9 @@ const TimetableTableWithScroll: React.FC<{
</div>
{data.length === 0 && (
- <p className="no-data">{t("timetable.noData", "No hay datos de horarios disponibles")}</p>
+ <p className="no-data">
+ {t("timetable.noData", "No hay datos de horarios disponibles")}
+ </p>
)}
</div>
);
diff --git a/src/frontend/package-lock.json b/src/frontend/package-lock.json
index e8bdee3..6548e8a 100644
--- a/src/frontend/package-lock.json
+++ b/src/frontend/package-lock.json
@@ -83,7 +83,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",
@@ -1601,7 +1600,6 @@
"integrity": "sha512-sww8oDNqz8SgaXEQ3maqTuMlibCMpmWvLE0s5zyEyOQb1G99clYMcXceQ2HNU2jtXJkp+P5XI1CngpGpngyTnw==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@mjackson/node-fetch-server": "^0.2.0",
"@react-router/express": "7.9.5",
@@ -1983,7 +1981,6 @@
"integrity": "sha512-qzQZRBqkFsYyaSWXuEHc2WR9c0a0CXwiE5FWUvn7ZM+vdy1uZLfCunD38UzhuB7YN/J11ndbDBcTmOdxJo9Q7A==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"undici-types": "~7.16.0"
}
@@ -1994,7 +1991,6 @@
"integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"csstype": "^3.0.2"
}
@@ -2065,7 +2061,6 @@
"integrity": "sha512-6m1I5RmHBGTnUGS113G04DMu3CpSdxCAU/UvtjNWL4Nuf3MW9tQhiJqRlHzChIkhy6kZSAQmc+I1bcGjE3yNKg==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "8.46.3",
"@typescript-eslint/types": "8.46.3",
@@ -2372,7 +2367,6 @@
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"dev": true,
"license": "MIT",
- "peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -2613,7 +2607,6 @@
}
],
"license": "MIT",
- "peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.8.9",
"caniuse-lite": "^1.0.30001746",
@@ -3178,7 +3171,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",
@@ -3386,7 +3378,6 @@
"integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"accepts": "~1.3.8",
"array-flatten": "1.1.1",
@@ -4281,7 +4272,6 @@
"integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==",
"dev": true,
"license": "MIT",
- "peer": true,
"bin": {
"jiti": "lib/jiti-cli.mjs"
}
@@ -4391,8 +4381,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",
@@ -4472,7 +4461,6 @@
"integrity": "sha512-XY2KHMsrVwSxGmudI94BXvP6v++9KxFs6/MEm9yPaF83H1YYDE8MPe2kc+2yAuPV7c1iA6Y5tmBsPcNj4J/ywg==",
"dev": true,
"license": "BSD-3-Clause",
- "peer": true,
"dependencies": {
"@mapbox/geojson-rewind": "^0.5.2",
"@mapbox/jsonlint-lines-primitives": "^2.0.2",
@@ -5097,7 +5085,6 @@
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true,
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=12"
},
@@ -5314,7 +5301,6 @@
"resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz",
"integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==",
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -5324,7 +5310,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"
},
@@ -5456,7 +5441,6 @@
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.9.5.tgz",
"integrity": "sha512-JmxqrnBZ6E9hWmf02jzNn9Jm3UqyeimyiwzD69NjxGySG6lIz/1LVPsoTCwN7NBX2XjCEa1LIX5EMz1j2b6u6A==",
"license": "MIT",
- "peer": true,
"dependencies": {
"cookie": "^1.0.1",
"set-cookie-parser": "^2.6.0"
@@ -6311,7 +6295,6 @@
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"devOptional": true,
"license": "Apache-2.0",
- "peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -6506,7 +6489,6 @@
"integrity": "sha512-ZWyE8YXEXqJrrSLvYgrRP7p62OziLW7xI5HYGWFzOvupfAlrLvURSzv/FyGyy0eidogEM3ujU+kUG1zuHgb6Ug==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"esbuild": "^0.25.0",
"fdir": "^6.5.0",
@@ -6782,7 +6764,6 @@
"integrity": "sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==",
"dev": true,
"license": "MIT",
- "peer": true,
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}