feat: add track selection and display functionality in the app

This commit is contained in:
2025-10-11 22:10:53 +02:00
parent c35049cd54
commit 3c97c47f7e
3 changed files with 164 additions and 7 deletions

View File

@@ -19,11 +19,13 @@ function App(): JSX.Element {
startId: string | null;
endId: string | null;
}>({ startId: null, endId: null });
const [selectedTrackId, setSelectedTrackId] = useState<string | null>(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 (
<div className="app-shell">
<header className="app-header">
@@ -146,7 +166,9 @@ function App(): JSX.Element {
startStationId={routeSelection.startId}
endStationId={routeSelection.endId}
routeSegments={routeSegments}
selectedTrackId={selectedTrackId}
onStationClick={handleStationSelection}
onTrackClick={setSelectedTrackId}
/>
</div>
<div className="route-panel">
@@ -206,6 +228,19 @@ function App(): JSX.Element {
</ol>
</div>
)}
{routeSelection.startId &&
routeSelection.endId &&
routeComputation.error && (
<div className="route-panel__actions">
<button
type="button"
className="primary-button"
onClick={handleCreateTrack}
>
Create Track
</button>
</div>
)}
</div>
<div className="grid">
<div>
@@ -318,6 +353,49 @@ function App(): JSX.Element {
</dl>
</div>
)}
{selectedTrack && (
<div className="selected-track">
<h3>Selected Track</h3>
<dl>
<div>
<dt>Start Station</dt>
<dd>
{stationById.get(selectedTrack.startStationId)?.name ??
'Unknown'}
</dd>
</div>
<div>
<dt>End Station</dt>
<dd>
{stationById.get(selectedTrack.endStationId)?.name ??
'Unknown'}
</dd>
</div>
<div>
<dt>Length</dt>
<dd>
{selectedTrack.lengthMeters > 0
? `${(selectedTrack.lengthMeters / 1000).toFixed(2)} km`
: 'N/A'}
</dd>
</div>
<div>
<dt>Max Speed</dt>
<dd>{selectedTrack.maxSpeedKph} km/h</dd>
</div>
{selectedTrack.status && (
<div>
<dt>Status</dt>
<dd>{selectedTrack.status}</dd>
</div>
)}
<div>
<dt>Bidirectional</dt>
<dd>{selectedTrack.isBidirectional ? 'Yes' : 'No'}</dd>
</div>
</dl>
</div>
)}
</div>
)}
</section>

View File

@@ -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<StationPosition[]>(() => {
return snapshot.stations.map((station) => ({
@@ -117,13 +121,43 @@ export function NetworkMap({
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
/>
{focusedPosition ? <StationFocus position={focusedPosition} /> : null}
{trackSegments.map((segment, index) => (
<Polyline
key={`track-${index}`}
positions={segment}
pathOptions={{ color: '#334155', weight: 3, opacity: 0.8 }}
/>
))}
{trackSegments.map((segment, index) => {
const track = snapshot.tracks[index];
const isSelected = track.id === selectedTrackId;
return (
<Polyline
key={`track-${track.id}`}
positions={segment}
pathOptions={{
color: isSelected ? '#3b82f6' : '#334155',
weight: isSelected ? 5 : 3,
opacity: 0.8,
}}
eventHandlers={{
click: () => {
onTrackClick?.(track.id);
},
}}
>
<Tooltip>
{track.startStationId} {track.endStationId}
<br />
Length:{' '}
{track.lengthMeters > 0
? `${(track.lengthMeters / 1000).toFixed(1)} km`
: 'N/A'}
<br />
Max Speed: {track.maxSpeedKph} km/h
{track.status && (
<>
<br />
Status: {track.status}
</>
)}
</Tooltip>
</Polyline>
);
})}
{routeSegments.map((segment, index) => (
<Polyline
key={`route-${index}`}

View File

@@ -266,6 +266,12 @@ body {
margin: 0;
}
.route-panel__actions {
margin-top: 1rem;
display: flex;
gap: 0.75rem;
}
.selected-station {
margin-top: 1rem;
padding: 1rem 1.25rem;
@@ -305,6 +311,45 @@ body {
color: rgba(226, 232, 240, 0.92);
}
.selected-track {
margin-top: 1rem;
padding: 1rem 1.25rem;
border-radius: 12px;
border: 1px solid rgba(59, 130, 246, 0.35);
background: rgba(37, 99, 235, 0.18);
display: grid;
gap: 0.75rem;
}
.selected-track h3 {
color: rgba(226, 232, 240, 0.9);
font-size: 1.1rem;
}
.selected-track dl {
display: grid;
gap: 0.45rem;
}
.selected-track dl > 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;