feat: add track selection and display functionality in the app
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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}`}
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user