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;
|
startId: string | null;
|
||||||
endId: string | null;
|
endId: string | null;
|
||||||
}>({ startId: null, endId: null });
|
}>({ startId: null, endId: null });
|
||||||
|
const [selectedTrackId, setSelectedTrackId] = useState<string | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (status !== 'success' || !data?.stations.length) {
|
if (status !== 'success' || !data?.stations.length) {
|
||||||
setFocusedStationId(null);
|
setFocusedStationId(null);
|
||||||
setRouteSelection({ startId: null, endId: null });
|
setRouteSelection({ startId: null, endId: null });
|
||||||
|
setSelectedTrackId(null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -92,8 +94,16 @@ function App(): JSX.Element {
|
|||||||
return stationById.get(focusedStationId) ?? null;
|
return stationById.get(focusedStationId) ?? null;
|
||||||
}, [data, focusedStationId, stationById]);
|
}, [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) => {
|
const handleStationSelection = (stationId: string) => {
|
||||||
setFocusedStationId(stationId);
|
setFocusedStationId(stationId);
|
||||||
|
setSelectedTrackId(null);
|
||||||
setRouteSelection((current) => {
|
setRouteSelection((current) => {
|
||||||
if (!current.startId || (current.startId && current.endId)) {
|
if (!current.startId || (current.startId && current.endId)) {
|
||||||
return { startId: stationId, endId: null };
|
return { startId: stationId, endId: null };
|
||||||
@@ -111,6 +121,16 @@ function App(): JSX.Element {
|
|||||||
setRouteSelection({ startId: null, endId: null });
|
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 (
|
return (
|
||||||
<div className="app-shell">
|
<div className="app-shell">
|
||||||
<header className="app-header">
|
<header className="app-header">
|
||||||
@@ -146,7 +166,9 @@ function App(): JSX.Element {
|
|||||||
startStationId={routeSelection.startId}
|
startStationId={routeSelection.startId}
|
||||||
endStationId={routeSelection.endId}
|
endStationId={routeSelection.endId}
|
||||||
routeSegments={routeSegments}
|
routeSegments={routeSegments}
|
||||||
|
selectedTrackId={selectedTrackId}
|
||||||
onStationClick={handleStationSelection}
|
onStationClick={handleStationSelection}
|
||||||
|
onTrackClick={setSelectedTrackId}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="route-panel">
|
<div className="route-panel">
|
||||||
@@ -206,6 +228,19 @@ function App(): JSX.Element {
|
|||||||
</ol>
|
</ol>
|
||||||
</div>
|
</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>
|
||||||
<div className="grid">
|
<div className="grid">
|
||||||
<div>
|
<div>
|
||||||
@@ -318,6 +353,49 @@ function App(): JSX.Element {
|
|||||||
</dl>
|
</dl>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -19,7 +19,9 @@ interface NetworkMapProps {
|
|||||||
readonly startStationId?: string | null;
|
readonly startStationId?: string | null;
|
||||||
readonly endStationId?: string | null;
|
readonly endStationId?: string | null;
|
||||||
readonly routeSegments?: LatLngExpression[][];
|
readonly routeSegments?: LatLngExpression[][];
|
||||||
|
readonly selectedTrackId?: string | null;
|
||||||
readonly onStationClick?: (stationId: string) => void;
|
readonly onStationClick?: (stationId: string) => void;
|
||||||
|
readonly onTrackClick?: (trackId: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface StationPosition {
|
interface StationPosition {
|
||||||
@@ -36,7 +38,9 @@ export function NetworkMap({
|
|||||||
startStationId,
|
startStationId,
|
||||||
endStationId,
|
endStationId,
|
||||||
routeSegments = [],
|
routeSegments = [],
|
||||||
|
selectedTrackId,
|
||||||
onStationClick,
|
onStationClick,
|
||||||
|
onTrackClick,
|
||||||
}: NetworkMapProps): JSX.Element {
|
}: NetworkMapProps): JSX.Element {
|
||||||
const stationPositions = useMemo<StationPosition[]>(() => {
|
const stationPositions = useMemo<StationPosition[]>(() => {
|
||||||
return snapshot.stations.map((station) => ({
|
return snapshot.stations.map((station) => ({
|
||||||
@@ -117,13 +121,43 @@ export function NetworkMap({
|
|||||||
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
|
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
|
||||||
/>
|
/>
|
||||||
{focusedPosition ? <StationFocus position={focusedPosition} /> : null}
|
{focusedPosition ? <StationFocus position={focusedPosition} /> : null}
|
||||||
{trackSegments.map((segment, index) => (
|
{trackSegments.map((segment, index) => {
|
||||||
|
const track = snapshot.tracks[index];
|
||||||
|
const isSelected = track.id === selectedTrackId;
|
||||||
|
return (
|
||||||
<Polyline
|
<Polyline
|
||||||
key={`track-${index}`}
|
key={`track-${track.id}`}
|
||||||
positions={segment}
|
positions={segment}
|
||||||
pathOptions={{ color: '#334155', weight: 3, opacity: 0.8 }}
|
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) => (
|
{routeSegments.map((segment, index) => (
|
||||||
<Polyline
|
<Polyline
|
||||||
key={`route-${index}`}
|
key={`route-${index}`}
|
||||||
|
|||||||
@@ -266,6 +266,12 @@ body {
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.route-panel__actions {
|
||||||
|
margin-top: 1rem;
|
||||||
|
display: flex;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
.selected-station {
|
.selected-station {
|
||||||
margin-top: 1rem;
|
margin-top: 1rem;
|
||||||
padding: 1rem 1.25rem;
|
padding: 1rem 1.25rem;
|
||||||
@@ -305,6 +311,45 @@ body {
|
|||||||
color: rgba(226, 232, 240, 0.92);
|
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) {
|
@media (min-width: 768px) {
|
||||||
.snapshot-layout {
|
.snapshot-layout {
|
||||||
gap: 2rem;
|
gap: 2rem;
|
||||||
|
|||||||
Reference in New Issue
Block a user