temp
This commit is contained in:
33
frontend/package-lock.json
generated
33
frontend/package-lock.json
generated
@@ -8,8 +8,10 @@
|
||||
"name": "frontend",
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"leaflet": "^1.9.4",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0",
|
||||
"react-leaflet": "^5.0.0",
|
||||
"swr": "^2.3.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -1007,6 +1009,17 @@
|
||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||
}
|
||||
},
|
||||
"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/@rolldown/pluginutils": {
|
||||
"version": "1.0.0-beta.47",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.47.tgz",
|
||||
@@ -2253,6 +2266,12 @@
|
||||
"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/levn": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
|
||||
@@ -2526,6 +2545,20 @@
|
||||
"react": "^19.2.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-refresh": {
|
||||
"version": "0.18.0",
|
||||
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz",
|
||||
|
||||
@@ -10,8 +10,10 @@
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"leaflet": "^1.9.4",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0",
|
||||
"react-leaflet": "^5.0.0",
|
||||
"swr": "^2.3.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
font-family: Arial, sans-serif;
|
||||
background-color: white;
|
||||
color: black;
|
||||
}
|
||||
|
||||
/* Form Layout */
|
||||
@@ -119,7 +121,7 @@ button:disabled {
|
||||
padding: 1rem;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8px;
|
||||
background-color: #f9f9f9;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.leg h3 {
|
||||
@@ -180,7 +182,7 @@ button:disabled {
|
||||
|
||||
/* Stops List */
|
||||
.stops-list {
|
||||
background-color: #ecf0f1;
|
||||
background-color: white;
|
||||
border-radius: 4px;
|
||||
padding: 0.5rem;
|
||||
margin: 0.5rem 0;
|
||||
@@ -198,6 +200,15 @@ button:disabled {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.stop-item.clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.stop-item.past {
|
||||
color: #999;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* Time Input */
|
||||
input[type="datetime-local"] {
|
||||
padding: 0.5rem;
|
||||
@@ -216,20 +227,42 @@ select {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* Route Selector */
|
||||
.route-selector {
|
||||
/* Mode Switch */
|
||||
.mode-switch {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
margin-bottom: 2rem;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.route-selector button {
|
||||
padding: 0.5rem 1rem;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.route-selector button.active {
|
||||
.mode-switch button.active {
|
||||
background-color: #535bf2;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* Explorer */
|
||||
.explorer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.trips {
|
||||
width: 100%;
|
||||
max-width: 800px;
|
||||
}
|
||||
|
||||
.trip {
|
||||
margin-bottom: 2rem;
|
||||
padding: 1rem;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8px;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.trip h3 {
|
||||
margin-top: 0;
|
||||
color: #333;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { useState, useMemo } from "react";
|
||||
import { useState, useMemo, useEffect } from "react";
|
||||
import useSWR from "swr";
|
||||
import "./App.css";
|
||||
import { MapContainer, TileLayer, Marker, Popup, Polyline, CircleMarker, useMap } from 'react-leaflet';
|
||||
import 'leaflet/dist/leaflet.css';
|
||||
|
||||
function StopSelector({ stops, onChange, placeholder, inputId }) {
|
||||
const [inputValue, setInputValue] = useState("");
|
||||
@@ -49,12 +51,66 @@ function StopSelector({ stops, onChange, placeholder, inputId }) {
|
||||
|
||||
const fetcher = (url) => fetch(url).then(res => res.ok ? res.json() : Promise.reject(res.statusText));
|
||||
|
||||
const JsonView = ({ data }) => { (
|
||||
<pre className="json-view">{JSON.stringify(data, null, 2)}</pre>
|
||||
); };
|
||||
const JsonView = ({ data }) => {
|
||||
return (
|
||||
<pre className="json-view">{JSON.stringify(data, null, 2)}</pre>
|
||||
);
|
||||
};
|
||||
|
||||
const MapComponent = ({ stops }) => {
|
||||
if (!stops || stops.length === 0) return null;
|
||||
const center = [stops[0].stop_lat, stops[0].stop_lon];
|
||||
const positions = stops.map(stop => [stop.stop_lat, stop.stop_lon]);
|
||||
return (
|
||||
<MapContainer center={center} zoom={10} style={{ height: '400px', width: '100%' }}>
|
||||
<FitBounds stops={stops} />
|
||||
<TileLayer
|
||||
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
|
||||
attribution='© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
|
||||
/>
|
||||
{stops.map((stop, index) => (
|
||||
<CircleMarker key={stop.stop_id} center={[stop.stop_lat, stop.stop_lon]} radius={5} color="blue" fillColor="blue" fillOpacity={0.5}>
|
||||
<Popup>{stop.stop_name}</Popup>
|
||||
</CircleMarker>
|
||||
))}
|
||||
<Polyline positions={positions} color="blue" />
|
||||
</MapContainer>
|
||||
);
|
||||
};
|
||||
|
||||
const FitBounds = ({ stops }) => {
|
||||
const map = useMap();
|
||||
useEffect(() => {
|
||||
if (stops && stops.length > 1) {
|
||||
const bounds = stops.reduce((acc, stop) => {
|
||||
return [
|
||||
[Math.min(acc[0][0], stop.stop_lat), Math.min(acc[0][1], stop.stop_lon)],
|
||||
[Math.max(acc[1][0], stop.stop_lat), Math.max(acc[1][1], stop.stop_lon)]
|
||||
];
|
||||
}, [[stops[0].stop_lat, stops[0].stop_lon], [stops[0].stop_lat, stops[0].stop_lon]]);
|
||||
map.fitBounds(bounds, { padding: [20, 20] });
|
||||
}
|
||||
}, [map, stops]);
|
||||
return null;
|
||||
};
|
||||
|
||||
const asTimeString = (secondsAfterMidnight) => {
|
||||
|
||||
const hours = Math.floor(secondsAfterMidnight / 3600) % 24;
|
||||
const minutes = Math.floor((secondsAfterMidnight % 3600) / 60);
|
||||
return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
const secondsToDateTime = (secondsAfterMidnight) => {
|
||||
const now = new Date();
|
||||
const date = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
||||
date.setSeconds(secondsAfterMidnight);
|
||||
return date.toISOString();
|
||||
}
|
||||
|
||||
function App() {
|
||||
const { data: stops = [], error: stopsError } = useSWR('/api/stops', fetcher);
|
||||
const [mode, setMode] = useState("plan");
|
||||
const [from, setFrom] = useState({
|
||||
id: "740000030",
|
||||
name: "Falun Centralstation",
|
||||
@@ -65,142 +121,220 @@ function App() {
|
||||
});
|
||||
const [time, setTime] = useState(new Date().toISOString());
|
||||
const [num, setNum] = useState(3);
|
||||
const [routeKey, setRouteKey] = useState(null);
|
||||
const { data: routes = [], error: routesError, isLoading } = useSWR(routeKey, fetcher);
|
||||
|
||||
const { data: routes = [], error: routesError, isLoading } = useSWR(from.id && to.id ? `/api/route?from=${from.id}&to=${to.id}&when=${encodeURIComponent(time)}&num=${num}` : null, (url => fetcher(url).then(data => data.map((item, idx) => ({ ...item, key: idx })))), { revalidateOnFocus: false });
|
||||
const [selectedRouteIndex, setSelectedRouteIndex] = useState(0);
|
||||
const [error, setError] = useState("");
|
||||
|
||||
const routeStops = useMemo(() => {
|
||||
if (!routes[selectedRouteIndex]?.legs) return [];
|
||||
const stopIds = new Set();
|
||||
routes[selectedRouteIndex].legs.forEach(leg => {
|
||||
leg.trip?.stops?.forEach(stopId => stopIds.add(stopId));
|
||||
});
|
||||
return Array.from(stopIds).map(id => stops.find(s => s.stop_id === id)).filter(Boolean);
|
||||
}, [routes, selectedRouteIndex, stops]);
|
||||
|
||||
const [exploreStop, setExploreStop] = useState({
|
||||
id: "740000030",
|
||||
name: "Falun Centralstation",
|
||||
});
|
||||
const [exploreTime, setExploreTime] = useState(new Date().toISOString());
|
||||
const { data: trips = [], error: tripsError, isLoading: tripsLoading } = useSWR(mode === "explore" && exploreStop.id ? `/api/trips?from=${exploreStop.id}&when=${encodeURIComponent(exploreTime)}` : null, fetcher, { revalidateOnFocus: false });
|
||||
|
||||
const findRoute = () => {
|
||||
if (!from.id || !to.id) {
|
||||
setError("Please select both from and to stops");
|
||||
return;
|
||||
}
|
||||
setError("");
|
||||
setRouteKey(`/api/route?from=${from.id}&to=${to.id}&when=${encodeURIComponent(time)}&num=${num}`);
|
||||
// setRouteKey(`/api/route?from=${from.id}&to=${to.id}&when=${encodeURIComponent(time)}&num=${num}`);
|
||||
setSelectedRouteIndex(0);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="App">
|
||||
<h1>GTFS Route Planner</h1>
|
||||
<div className="planner">
|
||||
<div className="selects">
|
||||
<label htmlFor="from-stop">
|
||||
From: ({from.id})
|
||||
<StopSelector
|
||||
stops={stops}
|
||||
onChange={setFrom}
|
||||
placeholder="Search for origin stop"
|
||||
inputId="from-stop"
|
||||
/>
|
||||
</label>
|
||||
<label htmlFor="to-stop">
|
||||
To: ({to.id})
|
||||
<StopSelector
|
||||
stops={stops}
|
||||
onChange={setTo}
|
||||
placeholder="Search for destination stop"
|
||||
inputId="to-stop"
|
||||
/>
|
||||
</label>
|
||||
<label htmlFor="time">
|
||||
Time:
|
||||
<input
|
||||
id="time"
|
||||
type="datetime-local"
|
||||
value={time}
|
||||
onChange={(e) => setTime(e.target.value)}
|
||||
/>
|
||||
</label>
|
||||
<label htmlFor="num">
|
||||
Number of routes:
|
||||
<select
|
||||
id="num"
|
||||
value={num}
|
||||
onChange={(e) => setNum(parseInt(e.target.value))}
|
||||
>
|
||||
<option value={1}>1</option>
|
||||
<option value={2}>2</option>
|
||||
<option value={3}>3</option>
|
||||
<option value={5}>5</option>
|
||||
<option value={10}>10</option>
|
||||
</select>
|
||||
</label>
|
||||
<button onClick={findRoute} disabled={isLoading} type="button">
|
||||
{isLoading ? "Finding route..." : "Find Route"}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{(error || stopsError || routesError) && <p className="error">{error || (stopsError ? "Failed to load stops: " + stopsError.message : "Failed to find route: " + routesError)}</p>}
|
||||
{routes.length > 0 && (
|
||||
<div className="routes">
|
||||
<h2 className="route-title">
|
||||
Routes from {from.name} to {to.name}
|
||||
</h2>
|
||||
{/* {routes.length > 1 && (
|
||||
<div className="route-selector">
|
||||
{routes.map((_, index) => (
|
||||
<button
|
||||
key={index}
|
||||
onClick={() => setSelectedRouteIndex(index)}
|
||||
className={selectedRouteIndex === index ? "active" : ""}
|
||||
type="button"
|
||||
>
|
||||
Option {index + 1}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)} */}
|
||||
<JsonView data={routes} />
|
||||
{/* {routes[selectedRouteIndex]?.legs &&
|
||||
routes[selectedRouteIndex].legs.length > 0 && (
|
||||
<div className="route">
|
||||
{routes[selectedRouteIndex].legs.map((leg, index) => {
|
||||
const trip = leg.trip;
|
||||
return (
|
||||
<div key={leg.trip?.trip_id || index} className="leg">
|
||||
<h3 className="leg-title">
|
||||
Leg {index + 1}:{" "}
|
||||
{trip?.trip_short_name ?? trip.route_id} by{" "}
|
||||
{leg.agency?.agency_name}
|
||||
</h3>
|
||||
<p className="leg-stops">
|
||||
From: {leg.from?.stop_name} To: {leg.to?.stop_name}
|
||||
</p>
|
||||
<p className="leg-time">
|
||||
Departure:{" "}
|
||||
{new Date(leg.departure_time).toLocaleString()}
|
||||
</p>
|
||||
<p className="leg-time">
|
||||
Arrival: {new Date(leg.arrival_time).toLocaleString()}
|
||||
</p>
|
||||
<ul className="stops-list">
|
||||
{leg.stops?.map((stopId) => {
|
||||
const stop = stops.find(
|
||||
(s) => s.stop_id === stopId,
|
||||
);
|
||||
return (
|
||||
<li key={stopId} className="stop-item">
|
||||
{stop?.stop_name || stopId}
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
{/* {index < routes[selectedRouteIndex].legs.length - 1 && (
|
||||
<p className="transfer">
|
||||
Transfer at{" "}
|
||||
{stops.find((s) => s.stop_id === leg.to)?.stop_name ||
|
||||
leg.to}
|
||||
</p>
|
||||
)} }
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}*/}
|
||||
</div>
|
||||
)}
|
||||
<div className="mode-switch">
|
||||
<button onClick={() => setMode("plan")} className={mode === "plan" ? "active" : ""} type="button">Plan Route</button>
|
||||
<button onClick={() => setMode("explore")} className={mode === "explore" ? "active" : ""} type="button">Explore Trips</button>
|
||||
</div>
|
||||
{mode === "plan" && (
|
||||
<div className="planner">
|
||||
<div className="selects">
|
||||
<label htmlFor="from-stop">
|
||||
From: ({from.id})
|
||||
<StopSelector
|
||||
stops={stops}
|
||||
onChange={setFrom}
|
||||
placeholder="Search for origin stop"
|
||||
inputId="from-stop"
|
||||
/>
|
||||
</label>
|
||||
<label htmlFor="to-stop">
|
||||
To: ({to.id})
|
||||
<StopSelector
|
||||
stops={stops}
|
||||
onChange={setTo}
|
||||
placeholder="Search for destination stop"
|
||||
inputId="to-stop"
|
||||
/>
|
||||
</label>
|
||||
<label htmlFor="time">
|
||||
Time:
|
||||
<input
|
||||
id="time"
|
||||
type="datetime-local"
|
||||
value={time}
|
||||
onChange={(e) => setTime(e.target.value)}
|
||||
/>
|
||||
</label>
|
||||
<label htmlFor="num">
|
||||
Number of routes:
|
||||
<select
|
||||
id="num"
|
||||
value={num}
|
||||
onChange={(e) => setNum(parseInt(e.target.value))}
|
||||
>
|
||||
<option value={1}>1</option>
|
||||
<option value={2}>2</option>
|
||||
<option value={3}>3</option>
|
||||
<option value={5}>5</option>
|
||||
<option value={10}>10</option>
|
||||
</select>
|
||||
</label>
|
||||
<button onClick={findRoute} disabled={isLoading} type="button">
|
||||
{isLoading ? "Finding route..." : "Find Route"}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{(error || stopsError || routesError) && <p className="error">{error || (stopsError ? "Failed to load stops: " + stopsError.message : "Failed to find route: " + routesError)}</p>}
|
||||
{routes.length > 0 && (
|
||||
<div className="routes">
|
||||
<h2 className="route-title">
|
||||
Routes from {from.name} to {to.name}
|
||||
</h2>
|
||||
{routes.length > 1 && (
|
||||
<div className="route-selector">
|
||||
{routes.map((_, index) => (
|
||||
<button
|
||||
key={index}
|
||||
onClick={() => setSelectedRouteIndex(index)}
|
||||
className={selectedRouteIndex === index ? "active" : ""}
|
||||
type="button"
|
||||
>
|
||||
Option {index + 1}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{routes[selectedRouteIndex]?.legs &&
|
||||
routes[selectedRouteIndex].legs.length > 0 && (
|
||||
<div className="route">
|
||||
{routes[selectedRouteIndex].legs.map((leg, index) => {
|
||||
|
||||
const trip = leg.trip;
|
||||
console.log(leg)
|
||||
return (
|
||||
<div key={leg.trip?.trip_id || index} className="leg">
|
||||
<h3 className="leg-title">
|
||||
Leg {index + 1}:{" "}
|
||||
{trip?.trip_short_name ?? trip.route_id} by{" "}
|
||||
{trip.agency_name}
|
||||
</h3>
|
||||
<p className="leg-stops">
|
||||
From: {leg.start?.stop.stop_name} To: {leg.end?.stop?.stop_name}
|
||||
</p>
|
||||
<p className="leg-time">
|
||||
Departure:{" "}
|
||||
{asTimeString(leg.start.departure_time)}
|
||||
</p>
|
||||
<p className="leg-time">
|
||||
Arrival: {asTimeString(leg.end.arrival_time)}
|
||||
</p>
|
||||
<ul className="stops-list">
|
||||
{trip.stops?.map((stopId, idx) => {
|
||||
const stop = stops.find(
|
||||
(s) => s.stop_id === stopId,
|
||||
);
|
||||
const isStart = index === 0 && idx === 0;
|
||||
const isEnd = index === routes[selectedRouteIndex].legs.length - 1 && idx === trip.stops.length - 1;
|
||||
const isCurrent = isStart || isEnd;
|
||||
return (
|
||||
<li key={stopId} className={`stop-item ${isCurrent ? 'current' : ''}`}>
|
||||
{stop?.stop_name || stopId}
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
{index < routes[selectedRouteIndex].legs.length - 1 && (
|
||||
<p className="transfer">
|
||||
Transfer at{" "}
|
||||
{stops.find((s) => s.stop_id === leg.to)?.stop_name ||
|
||||
leg.to}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<MapComponent stops={routeStops} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{mode === "explore" && (
|
||||
<div className="explorer">
|
||||
<div className="selects">
|
||||
<label htmlFor="explore-stop">
|
||||
Start Stop: ({exploreStop.id})
|
||||
<StopSelector
|
||||
stops={stops}
|
||||
onChange={setExploreStop}
|
||||
placeholder="Search for start stop"
|
||||
inputId="explore-stop"
|
||||
/>
|
||||
</label>
|
||||
<label htmlFor="explore-time">
|
||||
Time:
|
||||
<input
|
||||
id="explore-time"
|
||||
type="datetime-local"
|
||||
value={exploreTime}
|
||||
onChange={(e) => setExploreTime(e.target.value)}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
{(tripsError || stopsError) && <p className="error">{tripsError ? "Failed to load trips: " + tripsError : "Failed to load stops: " + stopsError}</p>}
|
||||
{tripsLoading && <p>Loading trips...</p>}
|
||||
{trips.length > 0 && (
|
||||
<div className="trips">
|
||||
<h2>Trips from {exploreStop.name}</h2>
|
||||
{trips.slice(0, 10).map((trip, index) => {
|
||||
const tripStops = trip.stops.map(s => stops.find(st => st.stop_id === s.stop_id)).filter(Boolean);
|
||||
return (
|
||||
<div key={trip.trip_id} className="trip">
|
||||
<h3>{trip.headsign || trip.short_name} ({trip.route_id}) by {trip.agency_name}</h3>
|
||||
<ul className="stops-list">
|
||||
{trip.stops.map((stop, idx) => (
|
||||
<li key={idx} className={`stop-item clickable ${idx === 0 ? 'current' : ''}`} onClick={() => {
|
||||
setExploreStop({ id: stop.stop_id, name: stop.stop_name });
|
||||
setExploreTime(secondsToDateTime(stop.departure_time));
|
||||
}}>
|
||||
{asTimeString(stop.arrival_time)} - {stop.stop_name}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<MapComponent stops={tripStops} />
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user