Replace mock data in geocoding, HAFAS, and bike routing clients with actual implementations using fetch and appropriate interfaces. Update typescript definitions to match the new data structures, including Journey, Station, and BikeRoute. Add vitest and related testing dependencies, replacing the placeholder test script with actual tests for the new client logic. Refactor calendar and countdown utilities to use the new types and remove unused files. Implement real API clients and update types Replace mock data in HafasClient, GeocodingClient, and BikeRoutingClient with actual fetch implementations. Update types to match API responses and add Vitest tests.
20 KiB
ÖBB Planner — Next.js Rewrite Plan
Status: Planned Created: 2024 Scope: Full rewrite from CRA + Express to Next.js App Router with new features
1. Current State
| Layer | Technology | Files |
|---|---|---|
| Backend | Express.js + CORS + node-ical | server/index.js (~140 lines) |
| Frontend | Create React App (React 19) | oebb-planner.jsx (~700 lines monolithic component) |
| Tests | Jest + Supertest | server/__tests__/*.test.js |
| Build | Separate npm start for server + react-scripts start for frontend |
Two independent processes |
Problems with current architecture
- Two separate processes to manage
- Monolithic component (~700 lines) — no component breakdown
- CRA is deprecated;
react-scriptsis unmaintained - No TypeScript
- Inline styles make theming/maintenance painful
- No proper state management pattern
- Duplication between
oebb-planner.jsxandoebb-planner-app/src/App.js
2. New Features (Beyond Rewrite)
2.1 Calendar View (/calendar)
A browsable month view that lets you navigate between months, see events as badges on dates, and click into a day to see event + train details.
How it works:
- User imports calendar via the existing ICS flow
- Events are stored in React state / localStorage
- A month grid shows event dots on dates that have events
- Clicking a day shows that day's events with their train info
- Clicking an event scrolls to / expands the full event card
UI wireframe:
┌──────────────────────┬──────────────────────┐
│ ← November 2024 → │ [Day] [Week] [Month] │
├──────────────────────┼──────────────────────┤
│ Su Mo Tu We Th Fr Sa │ Thursday, 14 Nov │
│ 1 2 3 4 5 6 7 │ │
│ 8 9 10 11 12 13 14 │ ● 14:00 Graz Hbf │
│ ● ● │ 🚂 Leave 13:10 │
│ 15 16 17 18 19 20 21 │ 🚲 Leave 11:07 │
│ ● ● │ │
│ ... │ ● 17:00 Linz Hbf │
│ │ 🚂 Leave 15:30 │
│ │ 🚲 Leave 15:45 │
└──────────────────────┴──────────────────────┘
2.2 Bicycle Routing
API choice: OSRM Public Demo Server (router.project-osrm.org)
- Free, no API key required, open source
- Supports
bicycleprofile - Returns distance, duration, turn-by-turn steps
- For production: self-host or switch to OpenRouteService
Geocoding: Nominatim (OpenStreetMap) to convert station/event location names to coordinates for routing.
Data flow:
Origin station (name) → already have coords from geolocation
Destination station (name) → geocode via Nominatim → get coordinates
→ OSRM bicycle route → distance + duration
→ cache in TripData alongside train journeys
2.3 Train vs Bicycle Comparison
Each event card shows both travel modes side by side:
┌───────────────────────────────────────────────────┐
│ 🏔 Meeting in Graz │
│ 📍 Graz Hbf · 14:00 · Thu, 14 Nov │
├───────────────────────────────────────────────────┤
│ 🚂 NEXT BEST TRAIN 🚲 BYCYCLE │
│ RJX 5234 on time 42 km │
│ DEP 13:22 ARR 14:35 2h 15min │
│ PLAT 3 0 emissions │
│ Leave by 13:10 Leave by 11:07 │
└───────────────────────────────────────────────────┘
3. Target Architecture
oebb_planner/
├── package.json
├── next.config.ts
├── tsconfig.json
├── postcss.config.mjs ← Tailwind v4 uses @tailwindcss/postcss; no tailwind.config.ts needed
├── vitest.config.ts ← must exist before Phase 7; deferred from Phase 1
├── src/
│ ├── app/
│ │ ├── layout.tsx ← root layout (fonts, providers, navbar)
│ │ ├── page.tsx ← main planner page (event cards)
│ │ ├── globals.css
│ │ ├── calendar/
│ │ │ └── page.tsx ← browsable calendar view (NEW)
│ │ ├── api/
│ │ │ ├── hafas/
│ │ │ │ └── route.ts ← POST → proxy to ÖBB HAFAS
│ │ │ ├── calendar/
│ │ │ │ ├── route.ts ← GET → fetch remote ICS calendar
│ │ │ │ └── parse/
│ │ │ │ └── route.ts ← POST → parse raw ICS body
│ │ │ ├── geocode/
│ │ │ │ └── route.ts ← GET → Nominatim geocoding (NEW)
│ │ │ ├── bike-route/
│ │ │ │ └── route.ts ← GET → OSRM bicycle routing (NEW)
│ │ │ └── health/
│ │ │ └── route.ts ← GET → liveness check
│ │ └── not-found.tsx
│ ├── components/
│ │ ├── layout/
│ │ │ ├── Header.tsx ← logo, server status, clock
│ │ │ └── Navbar.tsx ← navigation between pages (NEW)
│ │ ├── calendar/
│ │ │ ├── CalendarPanel.tsx ← collapsible import panel
│ │ │ ├── CalendarView.tsx ← month grid component (NEW)
│ │ │ ├── DayEvents.tsx ← events for a selected day (NEW)
│ │ │ ├── UrlTab.tsx
│ │ │ └── FileTab.tsx
│ │ ├── event/
│ │ │ ├── EventCard.tsx ← single event card (with train+bike)
│ │ │ ├── TrainSection.tsx ← train data in event card
│ │ │ ├── BikeSection.tsx ← bicycle data in event card (NEW)
│ │ │ ├── JourneyList.tsx ← all departures table
│ │ │ └── LeaveByBadge.tsx
│ │ ├── add-event/
│ │ │ └── AddEventModal.tsx
│ │ └── ui/
│ │ ├── Chip.tsx
│ │ ├── Button.tsx
│ │ └── LoadingSpinner.tsx
│ ├── hooks/
│ │ ├── useServerHealth.ts ← polls /api/health every 30s
│ │ ├── useClock.ts ← ticking clock (every 10s)
│ │ ├── useGeolocation.ts ← GPS positioning
│ │ ├── useOriginStation.ts ← finds nearest ÖBB station
│ │ ├── useJourneys.ts ← fetches + caches train data
│ │ ├── useBikeRoute.ts ← fetches bicycle route (NEW)
│ │ ├── useCalendar.ts ← calendar import logic
│ │ └── useEventsStore.ts ← shared events state (NEW)
│ ├── lib/
│ │ ├── hafas-client.ts ← HAFAS API client (class HafasClient)
│ │ ├── calendar-utils.ts ← ICS helpers (extractEvents, cleanLocation)
│ │ ├── countdown-utils.ts ← calculateCountdown, leaveBy helpers
│ │ ├── formatting.ts ← formatTime, formatDate, formatDateTime, formatDuration, formatDistance
│ │ ├── demo.ts ← demo journey generator
│ │ ├── constants.ts ← HAFAS_URL, NOMINATIM_URL, OSRM_URL, HAFAS_TIMEOUT_MS, DEFAULT_DAYS, APP_VERSION
│ │ ├── status-utils.ts ← StatusUtils.checkServerStatus() (used by useServerHealth)
│ │ ├── live-status-utils.ts ← LiveStatusUtils.getLiveStatus() (stub; live data TBD)
│ │ ├── geocoding-client.ts ← Nominatim client (NEW)
│ │ ├── bike-routing-client.ts ← OSRM client (NEW)
│ │ └── index.ts ← re-exports all lib modules
│ └── types/
│ └── index.ts ← all TypeScript interfaces
├── src/lib/__tests__/ ← unit tests live alongside lib, not at repo root
│ ├── hafas-client.test.ts
│ ├── geocoding-client.test.ts
│ ├── calendar-utils.test.ts
│ ├── countdown-utils.test.ts
│ └── bike-routing-client.test.ts ← NEW
├── src/app/api/__tests__/ ← API route tests
│ ├── hafas.test.ts
│ ├── calendar.test.ts
│ ├── geocode.test.ts ← NEW
│ ├── bike-route.test.ts ← NEW
│ └── health.test.ts
├── public/
│ ├── favicon.ico
│ └── robots.txt
├── .env.example
└── README.md
4. TypeScript Types
src/types/index.ts
// ── HAFAS / Train ───────────────────────────────────────────
interface Journey {
id: string;
sD: Date; // scheduled departure
rD: Date; // real departure
sA: Date; // scheduled arrival
rA: Date; // real arrival
delay: number; // minutes
platform: string;
changes: number;
trains: string[];
cancelled: boolean;
}
interface Station {
name: string;
extId: string;
}
// ── Events ───────────────────────────────────────────────────
interface Event {
id: string;
title: string;
destination: string;
eventTime: Date;
source: "manual" | "calendar";
}
// ── Trip Data (per event) ────────────────────────────────────
interface TripDataEntry {
journeys: Journey[];
destName: string;
demo: boolean;
loading: boolean;
bikeRoute?: BikeRoute | null; // NEW
bikeLoading?: boolean; // NEW
bikeError?: string | null; // NEW
destCoords?: { lat: number; lng: number }; // NEW (cached geocode)
}
// ── Bicycle Routing (NEW) ────────────────────────────────────
interface BikeRoute {
distance: number; // meters
duration: number; // seconds
steps?: BikeStep[];
}
interface BikeStep {
name: string;
distance: number;
duration: number;
instruction: string;
}
// ── Geocoding (NEW) ──────────────────────────────────────────
interface GeocodeResult {
lat: number;
lng: number;
display_name: string;
}
// ── Calendar Import ──────────────────────────────────────────
interface CalendarEvent {
id: string;
title: string;
destination: string;
eventTime: string; // ISO string from API
source: "calendar";
}
// ── UI Helpers ───────────────────────────────────────────────
interface CountdownInfo {
label: string;
color: string;
urgent: boolean;
}
type ServerStatus = null | true | false;
type LiveStatus = null | true | false;
type LocState = "pending" | "granted" | "denied";
type CalStatus = null | "loading" | "ok" | "error";
5. New API Routes
POST /api/hafas
- Purpose: Proxy to
fahrplan.oebb.at/bin/mgate.exe - Body:
{ svcReqL: [...] }— HAFAS service requests - Returns: HAFAS JSON response
- Timeout: 12s
GET /api/calendar?url=<ics_url>&days=14
- Purpose: Fetch and parse a remote ICS calendar
- Returns:
CalendarEvent[] - Supports:
https://,webcal://
POST /api/calendar/parse
- Purpose: Parse raw ICS content sent in request body
- Body: Raw ICS text
- Returns:
CalendarEvent[]
GET /api/geocode?name=Graz+Hbf&countrycodes=at (NEW)
- Purpose: Proxy to Nominatim for address→coordinates
- Upstream:
nominatim.openstreetmap.org/search?q={name}&format=json&limit=1&countrycodes=at - Returns:
{ lat, lng, display_name } - Caching: In-memory cache with TTL to avoid rate limits
GET /api/bike-route?fromLat=...&fromLng=...&toLat=...&toLng=... (NEW)
- Purpose: Proxy to OSRM for bicycle routing
- Upstream:
router.project-osrm.org/route/v1/bicycle/{lon1},{lat1};{lon2},{lat2}?overview=false - Returns:
{ distance, duration, steps: [...] }
GET /api/health
- Purpose: Liveness check
- Returns:
{ ok: true, ts: ISOString, version: "2.0.0" }
6. Key Technical Decisions
| Decision | Choice | Rationale |
|---|---|---|
| Framework | Next.js 15 App Router | Modern standard; API routes are serverless-compatible |
| Language | TypeScript (strict) | Type safety across frontend and backend |
| Styling | Tailwind CSS v4 | Replaces 700 lines of inline styles; v4 uses CSS-native config, no tailwind.config.ts |
| State | React hooks + Context | No Redux needed for this scale |
| API proxy | Next.js Route Handlers | Same logic as Express, no Express dependency |
| ICS parsing | Keep node-ical |
Already works, well-tested |
| Bike routing | OSRM public demo | Free, no API key, good enough for dev |
| Geocoding | Nominatim | Free, open source, good Austria coverage |
| Testing | Vitest + Testing Library | Faster than Jest for TS, better DX |
| Deployment | Vercel (recommended) or any Node host | Single artifact |
7. Migration Phases
Phase 1 — Scaffold Next.js Project (~30 min)
- Initialize Next.js 15 with App Router, TypeScript, Tailwind CSS
- Set up
tsconfig.jsonwith strict mode - Create
.env.examplewithPORT,HAFAS_URL,NOMINATIM_URL,OSRM_URL - Configure
next.config.js(rewrites if needed) - Configure
vitest.config.ts - Set up
postcss.config.mjs
Phase 2 — Types + Library Layer (~2 hours)
- Define TypeScript types in
src/types/index.ts - Port
lib/hafas-client.ts—HafasClientclass withsearchStation()andfetchJourneys() - Port
lib/calendar-utils.ts—extractEvents()+cleanLocation()(reusable in API routes AND tests) - Port
lib/countdown-utils.ts,lib/formatting.ts,lib/constants.ts,lib/demo.ts,lib/status-utils.ts,lib/live-status-utils.ts - Create
lib/geocoding-client.ts— Nominatim client (NEW) - Create
lib/bike-routing-client.ts— OSRM client (NEW)
Phase 3 — API Routes (~45 min)
src/app/api/hafas/route.ts— POST handler, same logic as Expresssrc/app/api/calendar/route.ts— GET handler for remote ICSsrc/app/api/calendar/parse/route.ts— POST handler for ICS bodysrc/app/api/geocode/route.ts— GET handler for Nominatim (NEW)src/app/api/bike-route/route.ts— GET handler for OSRM (NEW)src/app/api/health/route.ts— health check
Phase 4 — Custom Hooks (~2.5 hours)
useServerHealth.ts— polls/api/healthevery 30suseClock.ts— interval that updatesnowevery 10suseGeolocation.ts— wrapsnavigator.geolocationuseOriginStation.ts— finds nearest station from geolocationuseJourneys.ts— the complexfetchAlllogic, per-event journey fetchinguseBikeRoute.ts— fetches bicycle route for an event (NEW)useCalendar.ts— URL/file import with merge logicuseEventsStore.ts— shared events state via Context (NEW)
Phase 5 — UI Components (~3.5 hours)
ui/Chip.tsx— small badge componentui/Button.tsx— styled buttonui/LoadingSpinner.tsx— loading indicatorevent/LeaveByBadge.tsx— countdown badgeevent/JourneyList.tsx— departure rowsevent/TrainSection.tsx— train data in event cardevent/BikeSection.tsx— bicycle data in event card (NEW)event/EventCard.tsx— composes train + bike sectionscalendar/UrlTab.tsxcalendar/FileTab.tsxcalendar/CalendarPanel.tsxadd-event/AddEventModal.tsxlayout/Header.tsxlayout/Navbar.tsx(NEW)
Phase 6 — Calendar Page (~1 hour)
calendar/CalendarView.tsx— month grid component (NEW)calendar/DayEvents.tsx— events for a selected day (NEW)app/calendar/page.tsx— calendar route (NEW)
Phase 7 — Tests (~2 hours)
Install test dependencies first:
npm install -D vitest @vitejs/plugin-react @testing-library/react @testing-library/jest-dom jsdomand createvitest.config.ts. Update thetestscript inpackage.jsontovitest.
- Migrate
server/__tests__/*.test.js→src/app/api/__tests__/*.test.ts - Add unit tests for
src/lib/calendar-utils.ts,src/lib/countdown-utils.tsinsrc/lib/__tests__/ - Add API tests for
geocodeandbike-routeinsrc/app/api/__tests__/(NEW) - Add component smoke tests with
@testing-library/react(optional)
Phase 8 — Cleanup (~30 min)
- Delete old
server/directory - Delete old
oebb-planner-app/directory - Delete
oebb-planner.jsx - Update
README.mdwith new architecture and instructions - Final integration test
8. Dependencies
Runtime (installed)
{
"dependencies": {
"next": "16.2.6",
"react": "19.2.4",
"react-dom": "19.2.4",
"node-ical": "^0.18.0"
}
}
Development (installed)
{
"devDependencies": {
"typescript": "^5",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"tailwindcss": "^4",
"@tailwindcss/postcss": "^4",
"eslint": "^9",
"eslint-config-next": "16.2.6"
}
}
To be installed before Phase 7 (tests)
{
"devDependencies": {
"vitest": "^2.0.0",
"@vitejs/plugin-react": "^4.0.0",
"@testing-library/react": "^16.0.0",
"@testing-library/jest-dom": "^6.0.0",
"jsdom": "^25.0.0"
}
}
Note: Tailwind v4 no longer requires
autoprefixeror a separatetailwind.config.ts— configuration is done via CSS and@tailwindcss/postcss.
9. Environment Variables
# .env.example
# Server port
PORT=3001
# ÖBB HAFAS API
HAFAS_URL=https://fahrplan.oebb.at/bin/mgate.exe
# Nominatim geocoding (OpenStreetMap)
NOMINATIM_URL=https://nominatim.openstreetmap.org
# OSRM bicycle routing
OSRM_URL=https://router.project-osrm.org
# Nominatim user-agent / referer (required by their ToS)
NOMINATIM_USER_AGENT=OebbPlanner/1.0
10. Benefits of the Rewrite
- Single process —
npm run devstarts everything - Type safety — TypeScript catches bugs at compile time
- Composability — 700-line component becomes ~15 focused components
- Reusability —
lib/functions testable independently - Deployable — one artifact, works on Vercel / any Node host
- Maintainable — hooks encapsulate side effects, components are pure UI
- Modern tooling — no more CRA, no more
react-scripts ejectanxiety - Bicycle routing — complete picture of train vs bike for each event
- Calendar view — browse events by date, not just a flat list
- Train+bike comparison — side-by-side travel times in each event card
11. Estimated Effort
| Phase | Description | Est. Time |
|---|---|---|
| 1 | Scaffold Next.js + Tailwind + TS | 30 min |
| 2 | Types + lib layer (incl. geocode, bike-route) | 2 hours |
| 3 | API routes (incl. geocode, bike-route) | 45 min |
| 4 | Custom hooks (incl. bike-route, events-store) | 2.5 hours |
| 5 | UI Components (incl. bike section, navbar) | 3.5 hours |
| 6 | Calendar page (incl. month view, day events) | 1 hour |
| 7 | Tests (API + lib + component) | 2 hours |
| 8 | Cleanup (delete old dirs, update README) | 30 min |
| Total | ~13-14 hours |