aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAriel Costas Guerrero <94913521+arielcostas@users.noreply.github.com>2025-03-03 21:13:28 +0100
committerAriel Costas Guerrero <94913521+arielcostas@users.noreply.github.com>2025-03-03 21:13:28 +0100
commite4a737f43e45f02e80c06346cea73756f83854f3 (patch)
treea13c4de97367ecbd1fec9df72d28789b52a32554
parent4192cc1b5d1e2951963457516de0f9fe668c3b9a (diff)
Implement map page
-rw-r--r--package-lock.json139
-rw-r--r--package.json10
-rw-r--r--src/Layout.css5
-rw-r--r--src/pages/Estimates.tsx16
-rw-r--r--src/pages/Map.tsx101
-rw-r--r--src/styles/Pages.css116
6 files changed, 250 insertions, 137 deletions
diff --git a/package-lock.json b/package-lock.json
index 3ce48eb..81fe11b 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -11,15 +11,17 @@
"@fontsource-variable/outfit": "^5.2.5",
"fuse.js": "^7.1.0",
"lucide-react": "^0.477.0",
- "react": "^18.3.1",
- "react-dom": "^18.3.1",
+ "react": "^19.0.0",
+ "react-dom": "^19.0.0",
+ "react-leaflet": "^5.0.0",
+ "react-leaflet-markercluster": "^5.0.0-rc.0",
"react-router": "^7.2.0"
},
"devDependencies": {
"@eslint/js": "^9.9.0",
"@types/node": "^22.13.8",
- "@types/react": "^18.3.18",
- "@types/react-dom": "^18.3.5",
+ "@types/react": "^19.0.0",
+ "@types/react-dom": "^19.0.0",
"@vitejs/plugin-react-swc": "^3.8.0",
"eslint": "^9.21.0",
"eslint-plugin-react-hooks": "^5.2.0",
@@ -712,6 +714,17 @@
"node": ">= 8"
}
},
+ "node_modules/@react-leaflet/core": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/@react-leaflet/core/-/core-3.0.0.tgz",
+ "integrity": "sha512-3EWmekh4Nz+pGcr+xjf0KNyYfC3U2JjnkWsh0zcqaexYqmmB5ZhH37kz41JXGmKzpaMZCnPofBBm64i+YrEvGQ==",
+ "license": "Hippocratic-2.1",
+ "peerDependencies": {
+ "leaflet": "^1.9.0",
+ "react": "^19.0.0",
+ "react-dom": "^19.0.0"
+ }
+ },
"node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.34.9",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.34.9.tgz",
@@ -1224,32 +1237,24 @@
"undici-types": "~6.20.0"
}
},
- "node_modules/@types/prop-types": {
- "version": "15.7.14",
- "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz",
- "integrity": "sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==",
- "dev": true,
- "license": "MIT"
- },
"node_modules/@types/react": {
- "version": "18.3.18",
- "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.18.tgz",
- "integrity": "sha512-t4yC+vtgnkYjNSKlFx1jkAhH8LgTo2N/7Qvi83kdEaUtMDiwpbLAktKDaAMlRcJ5eSxZkH74eEGt1ky31d7kfQ==",
+ "version": "19.0.10",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.0.10.tgz",
+ "integrity": "sha512-JuRQ9KXLEjaUNjTWpzuR231Z2WpIwczOkBEIvbHNCzQefFIT0L8IqE6NV6ULLyC1SI/i234JnDoMkfg+RjQj2g==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@types/prop-types": "*",
"csstype": "^3.0.2"
}
},
"node_modules/@types/react-dom": {
- "version": "18.3.5",
- "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.5.tgz",
- "integrity": "sha512-P4t6saawp+b/dFrUr2cvkVsfvPguwsxtH6dNIYRllMsefqFzkZk5UIjzyDOv5g1dXIPdG4Sp1yCR4Z6RCUsG/Q==",
+ "version": "19.0.4",
+ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.0.4.tgz",
+ "integrity": "sha512-4fSQ8vWFkg+TGhePfUzVmat3eC14TXYSsiiDSLI0dVLsrm9gZFABjPy/Qu6TKgl1tq1Bu1yDsuQgY3A3DOjCcg==",
"dev": true,
"license": "MIT",
"peerDependencies": {
- "@types/react": "^18.0.0"
+ "@types/react": "^19.0.0"
}
},
"node_modules/@typescript-eslint/eslint-plugin": {
@@ -2175,12 +2180,6 @@
"dev": true,
"license": "ISC"
},
- "node_modules/js-tokens": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
- "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
- "license": "MIT"
- },
"node_modules/js-yaml": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
@@ -2225,6 +2224,21 @@
"json-buffer": "3.0.1"
}
},
+ "node_modules/leaflet": {
+ "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"
+ },
+ "node_modules/leaflet.markercluster": {
+ "version": "1.5.3",
+ "resolved": "https://registry.npmjs.org/leaflet.markercluster/-/leaflet.markercluster-1.5.3.tgz",
+ "integrity": "sha512-vPTw/Bndq7eQHjLBVlWpnGeLa3t+3zGiuM7fJwCkiMFq+nmRuG3RI3f7f4N4TDX7T4NpbAXpR2+NTRSEGfCSeA==",
+ "license": "MIT",
+ "peerDependencies": {
+ "leaflet": "^1.3.1"
+ }
+ },
"node_modules/levn": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
@@ -2262,18 +2276,6 @@
"dev": true,
"license": "MIT"
},
- "node_modules/loose-envify": {
- "version": "1.4.0",
- "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
- "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
- "license": "MIT",
- "dependencies": {
- "js-tokens": "^3.0.0 || ^4.0.0"
- },
- "bin": {
- "loose-envify": "cli.js"
- }
- },
"node_modules/lucide-react": {
"version": "0.477.0",
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.477.0.tgz",
@@ -2527,28 +2529,56 @@
"license": "MIT"
},
"node_modules/react": {
- "version": "18.3.1",
- "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
- "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
+ "version": "19.0.0",
+ "resolved": "https://registry.npmjs.org/react/-/react-19.0.0.tgz",
+ "integrity": "sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==",
"license": "MIT",
- "dependencies": {
- "loose-envify": "^1.1.0"
- },
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/react-dom": {
- "version": "18.3.1",
- "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
- "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
+ "version": "19.0.0",
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.0.0.tgz",
+ "integrity": "sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ==",
+ "license": "MIT",
+ "dependencies": {
+ "scheduler": "^0.25.0"
+ },
+ "peerDependencies": {
+ "react": "^19.0.0"
+ }
+ },
+ "node_modules/react-leaflet": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/react-leaflet/-/react-leaflet-5.0.0.tgz",
+ "integrity": "sha512-CWbTpr5vcHw5bt9i4zSlPEVQdTVcML390TjeDG0cK59z1ylexpqC6M1PJFjV8jD7CF+ACBFsLIDs6DRMoLEofw==",
+ "license": "Hippocratic-2.1",
+ "dependencies": {
+ "@react-leaflet/core": "^3.0.0"
+ },
+ "peerDependencies": {
+ "leaflet": "^1.9.0",
+ "react": "^19.0.0",
+ "react-dom": "^19.0.0"
+ }
+ },
+ "node_modules/react-leaflet-markercluster": {
+ "version": "5.0.0-rc.0",
+ "resolved": "https://registry.npmjs.org/react-leaflet-markercluster/-/react-leaflet-markercluster-5.0.0-rc.0.tgz",
+ "integrity": "sha512-jWa4bPD5LfLV3Lid1RWgl+yKUuQtnqeYtJzzLb/fiRjvX+rtwzY8pMoUFuygqyxNrWxMTQlWKBHxkpI7Sxvu4Q==",
"license": "MIT",
"dependencies": {
- "loose-envify": "^1.1.0",
- "scheduler": "^0.23.2"
+ "@react-leaflet/core": "^3.0.0",
+ "leaflet": "^1.9.4",
+ "leaflet.markercluster": "^1.5.3",
+ "react-leaflet": "^5.0.0"
},
"peerDependencies": {
- "react": "^18.3.1"
+ "leaflet": "^1.9.4",
+ "leaflet.markercluster": "^1.5.3",
+ "react": "^19.0.0",
+ "react-leaflet": "^5.0.0"
}
},
"node_modules/react-router": {
@@ -2664,13 +2694,10 @@
}
},
"node_modules/scheduler": {
- "version": "0.23.2",
- "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
- "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
- "license": "MIT",
- "dependencies": {
- "loose-envify": "^1.1.0"
- }
+ "version": "0.25.0",
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.25.0.tgz",
+ "integrity": "sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA==",
+ "license": "MIT"
},
"node_modules/semver": {
"version": "7.7.1",
diff --git a/package.json b/package.json
index 6d2a61b..37765a7 100644
--- a/package.json
+++ b/package.json
@@ -16,15 +16,17 @@
"@fontsource-variable/outfit": "^5.2.5",
"fuse.js": "^7.1.0",
"lucide-react": "^0.477.0",
- "react": "^18.3.1",
- "react-dom": "^18.3.1",
+ "react": "^19.0.0",
+ "react-dom": "^19.0.0",
+ "react-leaflet": "^5.0.0",
+ "react-leaflet-markercluster": "^5.0.0-rc.0",
"react-router": "^7.2.0"
},
"devDependencies": {
"@eslint/js": "^9.9.0",
"@types/node": "^22.13.8",
- "@types/react": "^18.3.18",
- "@types/react-dom": "^18.3.5",
+ "@types/react": "^19.0.0",
+ "@types/react-dom": "^19.0.0",
"@vitejs/plugin-react-swc": "^3.8.0",
"eslint": "^9.21.0",
"eslint-plugin-react-hooks": "^5.2.0",
diff --git a/src/Layout.css b/src/Layout.css
index ab4eecd..7af97b8 100644
--- a/src/Layout.css
+++ b/src/Layout.css
@@ -9,8 +9,7 @@
.main-content {
flex: 1;
overflow: auto;
- padding: 16px;
- padding-bottom: 70px; /* Extra padding to ensure content isn't hidden behind navbar */
+ padding-bottom: 60px; /* Extra padding to ensure content isn't hidden behind navbar */
}
.nav-bar {
@@ -18,6 +17,8 @@
bottom: 0;
left: 0;
right: 0;
+ z-index: 5;
+
background-color: var(--background-color);
display: flex;
justify-content: space-around;
diff --git a/src/pages/Estimates.tsx b/src/pages/Estimates.tsx
index d3b4ced..3f002be 100644
--- a/src/pages/Estimates.tsx
+++ b/src/pages/Estimates.tsx
@@ -23,13 +23,17 @@ interface StopDetails {
export function Estimates(): JSX.Element {
const sdp = new StopDataProvider();
const [data, setData] = useState<StopDetails | null>(null);
+ const [dataDate, setDataDate] = useState<Date | null>(null);
const [favourited, setFavourited] = useState(false);
const params = useParams();
const loadData = () => {
fetch(`/api/GetStopEstimates?id=${params.stopId}`)
.then(r => r.json())
- .then((body: StopDetails) => setData(body));
+ .then((body: StopDetails) => {
+ setData(body);
+ setDataDate(new Date());
+ });
};
useEffect(() => {
@@ -80,17 +84,9 @@ export function Estimates(): JSX.Element {
</h1>
</div>
- <div className="button-group">
- <Link to="/stops" className="button">
- 🔙 Volver al listado de paradas
- </Link>
-
- <button className="button" onClick={loadData}>⬇️ Recargar</button>
- </div>
-
<div className="table-responsive">
<table className="table">
- <caption>Estimaciones de llegadas</caption>
+ <caption>Estimaciones de llegadas a las {dataDate?.toLocaleTimeString()}</caption>
<thead>
<tr>
diff --git a/src/pages/Map.tsx b/src/pages/Map.tsx
index dbf5b9f..4d89ae0 100644
--- a/src/pages/Map.tsx
+++ b/src/pages/Map.tsx
@@ -1,76 +1,47 @@
-import { useEffect, useMemo, useState } from "react";
-import { Link, useNavigate } from "react-router";
-import { Stop, StopDataProvider } from "../data/StopDataProvider";
-import LineIcon from "../components/LineIcon";
+import { StopDataProvider, Stop } from "../data/StopDataProvider";
+
+import 'leaflet/dist/leaflet.css'
+import 'react-leaflet-markercluster/styles'
+
+import { useEffect, useState } from 'react';
+import LineIcon from '../components/LineIcon';
+import { Link } from 'react-router';
+import { MapContainer } from "react-leaflet/MapContainer";
+import { TileLayer } from "react-leaflet/TileLayer";
+import { Marker } from "react-leaflet/Marker";
+import { Popup } from "react-leaflet/Popup";
+import MarkerClusterGroup from "react-leaflet-markercluster";
const sdp = new StopDataProvider();
export function StopMap() {
- const [data, setData] = useState<Stop[] | null>(null)
- const navigate = useNavigate();
+ const [stops, setStops] = useState<Stop[]>([]);
+ const position = [42.229188855975046, -8.72246955783102]
useEffect(() => {
- sdp.getStops().then((stops: Stop[]) => setData(stops))
+ sdp.getStops().then((stops) => { setStops(stops); });
}, []);
- const handleStopSearch = async (event: React.FormEvent) => {
- event.preventDefault()
-
- const stopId = (event.target as HTMLFormElement).stopId.value
- const searchNumber = parseInt(stopId)
- if (data?.find(stop => stop.stopId === searchNumber)) {
- navigate(`/estimates/${searchNumber}`)
- } else {
- alert("Parada no encontrada")
- }
- }
-
- if (data === null) return <h1 className="page-title">Loading...</h1>
-
return (
- <div className="page-container">
- <h1 className="page-title">Map View</h1>
-
- <div className="map-container">
- {/* Map placeholder - in a real implementation, this would be a map component */}
- <div style={{
- height: '100%',
- backgroundColor: '#f0f0f0',
- display: 'flex',
- justifyContent: 'center',
- alignItems: 'center',
- borderRadius: '8px'
- }}>
- <p>Map will be displayed here</p>
- </div>
- </div>
-
- <form className="search-form" onSubmit={handleStopSearch}>
- <div className="form-group">
- <label className="form-label" htmlFor="stopId">
- Find Stop by ID
- </label>
- <input className="form-input" type="number" placeholder="Stop ID" id="stopId" />
- </div>
-
- <button className="form-button" type="submit">Search</button>
- </form>
+ <MapContainer center={position} zoom={14} scrollWheelZoom={true} style={{ height: '100%' }}>
+ <TileLayer
+ attribution='&copy; <a href="http://osm.org/copyright">OpenStreetMap</a>, &copy; <a href="https://carto.com/attributions">CARTO</a>'
+ url="https://d.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}@2x.png"
+ />
+ <MarkerClusterGroup>
+ {stops.map((stop) => (
+ <Marker key={stop.stopId} position={[stop.latitude, stop.longitude]}>
+ <Popup>
+ <Link to={`/estimates/${stop.stopId}`}>{stop.name}</Link>
+ <br />
+ {stop.lines.map((line) => (
+ <LineIcon key={line} line={line} />
+ ))}
+ </Popup>
+ </Marker>
+ ))}
+ </MarkerClusterGroup>
- <div className="list-container">
- <h2 className="page-subtitle">Nearby Stops</h2>
- <ul className="list">
- {data?.slice(0, 5).map((stop: Stop) => (
- <li className="list-item" key={stop.stopId}>
- <Link className="list-item-link" to={`/estimates/${stop.stopId}`}>
- ({stop.stopId}) {stop.name}
- <div className="line-icons">
- {stop.lines?.map(line => <LineIcon key={line} line={line} />)}
- </div>
- </Link>
- </li>
- ))}
- </ul>
- </div>
- </div>
- )
+ </MapContainer>
+ );
}
diff --git a/src/styles/Pages.css b/src/styles/Pages.css
index 9bae8a3..a8ff842 100644
--- a/src/styles/Pages.css
+++ b/src/styles/Pages.css
@@ -130,6 +130,11 @@ body {
color: var(--text-color);
}
+.distance-info {
+ font-size: 0.9rem;
+ color: var(--subtitle-color);
+}
+
/* Message styles */
.message {
padding: 1rem;
@@ -160,6 +165,117 @@ body {
height: calc(100vh - 140px);
margin: -16px;
margin-bottom: 1rem;
+ position: relative;
+}
+
+#map {
+ position: absolute;
+ top: 0;
+ bottom: 60px; /* Adjust this value based on your navbar height */
+ left: 0;
+ right: 0;
+ height: calc(100vh - 60px); /* Adjust this value based on your navbar height */
+ overflow: hidden;
+ z-index: 0;
+}
+
+.main-content {
+ position: relative;
+ height: calc(100vh - 60px); /* Adjust this value based on your navbar height */
+ overflow: hidden;
+}
+
+.nav-bar {
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ height: 60px; /* Adjust this value based on your navbar height */
+ display: flex;
+ justify-content: space-around;
+ align-items: center;
+ background-color: #fff;
+ border-top: 1px solid #ccc;
+ z-index: 1;
+}
+
+/* Fullscreen map styles */
+.fullscreen-container {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100vw;
+ height: 100vh;
+ padding: 0;
+ margin: 0;
+ max-width: none;
+ overflow: hidden;
+}
+
+.fullscreen-map {
+ width: 100%;
+ height: 100%;
+}
+
+.fullscreen-loading {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ height: 100vh;
+ width: 100vw;
+ font-size: 1.8rem;
+ font-weight: 600;
+ color: var(--text-color);
+}
+
+/* Map marker and popup styles */
+.stop-marker {
+ box-shadow: 0 0 5px rgba(0, 0, 0, 0.3);
+ transition: all 0.2s ease-in-out;
+}
+
+.stop-marker:hover {
+ transform: scale(1.2);
+}
+
+.maplibregl-popup {
+ max-width: 250px;
+}
+
+.maplibregl-popup-content {
+ padding: 12px;
+ border-radius: 8px;
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
+}
+
+.popup-line-icons {
+ display: flex;
+ flex-wrap: wrap;
+ margin: 6px 0;
+ gap: 5px;
+}
+
+.popup-line {
+ display: inline-block;
+ background-color: var(--button-background-color);
+ color: white;
+ padding: 2px 6px;
+ margin-right: 4px;
+ border-radius: 4px;
+ font-size: 0.8rem;
+ font-weight: 500;
+}
+
+.popup-link {
+ display: block;
+ margin-top: 8px;
+ color: var(--button-background-color);
+ text-decoration: none;
+ font-weight: 500;
+}
+
+.popup-link:hover {
+ text-decoration: underline;
}
/* Estimates page specific styles */