diff options
| -rw-r--r-- | package-lock.json | 139 | ||||
| -rw-r--r-- | package.json | 10 | ||||
| -rw-r--r-- | src/Layout.css | 5 | ||||
| -rw-r--r-- | src/pages/Estimates.tsx | 16 | ||||
| -rw-r--r-- | src/pages/Map.tsx | 101 | ||||
| -rw-r--r-- | src/styles/Pages.css | 116 |
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='© <a href="http://osm.org/copyright">OpenStreetMap</a>, © <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 */ |
