import './styles/global.css'; import type { LatLngExpression } from 'leaflet'; import { useEffect, useMemo, useState } from 'react'; import { LoginForm } from './components/auth/LoginForm'; import { NetworkMap } from './components/map/NetworkMap'; import { useNetworkSnapshot } from './hooks/useNetworkSnapshot'; import { useAuth } from './state/AuthContext'; import type { Station } from './types/domain'; import { buildTrackAdjacency, computeRoute } from './utils/route'; function App(): JSX.Element { const { token, user, status: authStatus, logout } = useAuth(); const isAuthenticated = authStatus === 'authenticated' && token !== null; const { data, status, error } = useNetworkSnapshot(isAuthenticated ? token : null); const [focusedStationId, setFocusedStationId] = useState(null); const [routeSelection, setRouteSelection] = useState<{ startId: string | null; endId: string | null; }>({ startId: null, endId: null }); useEffect(() => { if (status !== 'success' || !data?.stations.length) { setFocusedStationId(null); setRouteSelection({ startId: null, endId: null }); return; } if (!focusedStationId || !hasStation(data.stations, focusedStationId)) { setFocusedStationId(data.stations[0].id); } }, [status, data, focusedStationId]); useEffect(() => { if (status !== 'success' || !data) { return; } setRouteSelection((current) => { const startExists = current.startId ? hasStation(data.stations, current.startId) : false; const endExists = current.endId ? hasStation(data.stations, current.endId) : false; return { startId: startExists ? current.startId : null, endId: endExists ? current.endId : null, }; }); }, [status, data]); const stationById = useMemo(() => { if (!data) { return new Map(); } const lookup = new Map(); for (const station of data.stations) { lookup.set(station.id, station); } return lookup; }, [data]); const trackAdjacency = useMemo( () => buildTrackAdjacency(data ? data.tracks : []), [data] ); const routeComputation = useMemo( () => computeRoute({ startId: routeSelection.startId, endId: routeSelection.endId, stationById, adjacency: trackAdjacency, }), [routeSelection, stationById, trackAdjacency] ); const routeSegments = useMemo(() => { return routeComputation.segments.map((segment) => segment.map((pair) => [pair[0], pair[1]] as LatLngExpression) ); }, [routeComputation.segments]); const focusedStation = useMemo(() => { if (!data || !focusedStationId) { return null; } return stationById.get(focusedStationId) ?? null; }, [data, focusedStationId, stationById]); const handleStationSelection = (stationId: string) => { setFocusedStationId(stationId); setRouteSelection((current) => { if (!current.startId || (current.startId && current.endId)) { return { startId: stationId, endId: null }; } if (current.startId === stationId) { return { startId: stationId, endId: null }; } return { startId: current.startId, endId: stationId }; }); }; const clearRouteSelection = () => { setRouteSelection({ startId: null, endId: null }); }; return (

Rail Game

Build and manage a railway network using real-world map data.

{isAuthenticated && (
Signed in as {user?.fullName ?? user?.username}
)}
{!isAuthenticated ? (
) : (

Network Snapshot

{status === 'loading' &&

Loading network data…

} {status === 'error' &&

{error}

} {status === 'success' && data && (

Route Selection

Click a station to set the origin, then click another station to preview the rail corridor between them.

Origin
{routeSelection.startId ? (stationById.get(routeSelection.startId)?.name ?? 'Unknown station') : 'Choose a station'}
Destination
{routeSelection.endId ? (stationById.get(routeSelection.endId)?.name ?? 'Unknown station') : 'Choose a station'}
Estimated Length
{routeComputation.totalLength !== null ? `${(routeComputation.totalLength / 1000).toFixed(2)} km` : 'N/A'}
{routeComputation.error && (

{routeComputation.error}

)} {!routeComputation.error && routeComputation.stations && (
Path:
    {routeComputation.stations.map((station) => (
  1. {station.name}
  2. ))}
)}

Stations

    {data.stations.map((station) => (
  • ))}

Trains

    {data.trains.map((train) => (
  • {train.designation} · {train.capacity} capacity ·{' '} {train.maxSpeedKph} km/h
  • ))}

Tracks

    {data.tracks.map((track) => (
  • {track.startStationId} → {track.endStationId} ·{' '} {track.lengthMeters > 0 ? `${(track.lengthMeters / 1000).toFixed(1)} km` : 'N/A'}
  • ))}
{focusedStation && (

Focused Station

Name
{focusedStation.name}
Coordinates
{focusedStation.latitude.toFixed(5)},{' '} {focusedStation.longitude.toFixed(5)}
{focusedStation.code && (
Code
{focusedStation.code}
)} {typeof focusedStation.elevationM === 'number' && (
Elevation
{focusedStation.elevationM.toFixed(1)} m
)} {focusedStation.osmId && (
OSM ID
{focusedStation.osmId}
)}
Status
{(focusedStation.isActive ?? true) ? 'Active' : 'Inactive'}
)}
)}
)}
); } export default App; function hasStation(stations: Station[], id: string): boolean { return stations.some((station) => station.id === id); }