From 3c97c47f7eea8112e861626ec55a0b6490e87239 Mon Sep 17 00:00:00 2001 From: zwitschi Date: Sat, 11 Oct 2025 22:10:53 +0200 Subject: [PATCH] feat: add track selection and display functionality in the app --- frontend/src/App.tsx | 78 ++++++++++++++++++++++ frontend/src/components/map/NetworkMap.tsx | 48 +++++++++++-- frontend/src/styles/global.css | 45 +++++++++++++ 3 files changed, 164 insertions(+), 7 deletions(-) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index b81158a..5c799c6 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -19,11 +19,13 @@ function App(): JSX.Element { startId: string | null; endId: string | null; }>({ startId: null, endId: null }); + const [selectedTrackId, setSelectedTrackId] = useState(null); useEffect(() => { if (status !== 'success' || !data?.stations.length) { setFocusedStationId(null); setRouteSelection({ startId: null, endId: null }); + setSelectedTrackId(null); return; } @@ -92,8 +94,16 @@ function App(): JSX.Element { return stationById.get(focusedStationId) ?? null; }, [data, focusedStationId, stationById]); + const selectedTrack = useMemo(() => { + if (!data || !selectedTrackId) { + return null; + } + return data.tracks.find((track) => track.id === selectedTrackId) ?? null; + }, [data, selectedTrackId]); + const handleStationSelection = (stationId: string) => { setFocusedStationId(stationId); + setSelectedTrackId(null); setRouteSelection((current) => { if (!current.startId || (current.startId && current.endId)) { return { startId: stationId, endId: null }; @@ -111,6 +121,16 @@ function App(): JSX.Element { setRouteSelection({ startId: null, endId: null }); }; + const handleCreateTrack = () => { + if (!routeSelection.startId || !routeSelection.endId) { + return; + } + // TODO: Implement track creation API call + alert( + `Creating track between ${stationById.get(routeSelection.startId)?.name} and ${stationById.get(routeSelection.endId)?.name}` + ); + }; + return (
@@ -146,7 +166,9 @@ function App(): JSX.Element { startStationId={routeSelection.startId} endStationId={routeSelection.endId} routeSegments={routeSegments} + selectedTrackId={selectedTrackId} onStationClick={handleStationSelection} + onTrackClick={setSelectedTrackId} />
@@ -206,6 +228,19 @@ function App(): JSX.Element {
)} + {routeSelection.startId && + routeSelection.endId && + routeComputation.error && ( +
+ +
+ )}
@@ -318,6 +353,49 @@ function App(): JSX.Element {
)} + {selectedTrack && ( +
+

Selected Track

+
+
+
Start Station
+
+ {stationById.get(selectedTrack.startStationId)?.name ?? + 'Unknown'} +
+
+
+
End Station
+
+ {stationById.get(selectedTrack.endStationId)?.name ?? + 'Unknown'} +
+
+
+
Length
+
+ {selectedTrack.lengthMeters > 0 + ? `${(selectedTrack.lengthMeters / 1000).toFixed(2)} km` + : 'N/A'} +
+
+
+
Max Speed
+
{selectedTrack.maxSpeedKph} km/h
+
+ {selectedTrack.status && ( +
+
Status
+
{selectedTrack.status}
+
+ )} +
+
Bidirectional
+
{selectedTrack.isBidirectional ? 'Yes' : 'No'}
+
+
+
+ )}
)} diff --git a/frontend/src/components/map/NetworkMap.tsx b/frontend/src/components/map/NetworkMap.tsx index 589f6fb..936e8d0 100644 --- a/frontend/src/components/map/NetworkMap.tsx +++ b/frontend/src/components/map/NetworkMap.tsx @@ -19,7 +19,9 @@ interface NetworkMapProps { readonly startStationId?: string | null; readonly endStationId?: string | null; readonly routeSegments?: LatLngExpression[][]; + readonly selectedTrackId?: string | null; readonly onStationClick?: (stationId: string) => void; + readonly onTrackClick?: (trackId: string) => void; } interface StationPosition { @@ -36,7 +38,9 @@ export function NetworkMap({ startStationId, endStationId, routeSegments = [], + selectedTrackId, onStationClick, + onTrackClick, }: NetworkMapProps): JSX.Element { const stationPositions = useMemo(() => { return snapshot.stations.map((station) => ({ @@ -117,13 +121,43 @@ export function NetworkMap({ url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" /> {focusedPosition ? : null} - {trackSegments.map((segment, index) => ( - - ))} + {trackSegments.map((segment, index) => { + const track = snapshot.tracks[index]; + const isSelected = track.id === selectedTrackId; + return ( + { + onTrackClick?.(track.id); + }, + }} + > + + {track.startStationId} → {track.endStationId} +
+ Length:{' '} + {track.lengthMeters > 0 + ? `${(track.lengthMeters / 1000).toFixed(1)} km` + : 'N/A'} +
+ Max Speed: {track.maxSpeedKph} km/h + {track.status && ( + <> +
+ Status: {track.status} + + )} +
+
+ ); + })} {routeSegments.map((segment, index) => ( div { + display: flex; + align-items: baseline; + justify-content: space-between; + gap: 0.75rem; +} + +.selected-track dt { + font-size: 0.8rem; + text-transform: uppercase; + letter-spacing: 0.08em; + color: rgba(226, 232, 240, 0.6); +} + +.selected-track dd { + font-size: 0.95rem; + color: rgba(226, 232, 240, 0.92); +} + @media (min-width: 768px) { .snapshot-layout { gap: 2rem;