Files
time_to_leave/REWRITE_PLAN.md
T
fegger 3b87b8c4e5 Implement real API clients and update types
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.
2026-05-09 11:55:20 +02:00

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-scripts is unmaintained
  • No TypeScript
  • Inline styles make theming/maintenance painful
  • No proper state management pattern
  • Duplication between oebb-planner.jsx and oebb-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 bicycle profile
  • 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)

  1. Initialize Next.js 15 with App Router, TypeScript, Tailwind CSS
  2. Set up tsconfig.json with strict mode
  3. Create .env.example with PORT, HAFAS_URL, NOMINATIM_URL, OSRM_URL
  4. Configure next.config.js (rewrites if needed)
  5. Configure vitest.config.ts
  6. Set up postcss.config.mjs

Phase 2 — Types + Library Layer (~2 hours)

  1. Define TypeScript types in src/types/index.ts
  2. Port lib/hafas-client.tsHafasClient class with searchStation() and fetchJourneys()
  3. Port lib/calendar-utils.tsextractEvents() + cleanLocation() (reusable in API routes AND tests)
  4. Port lib/countdown-utils.ts, lib/formatting.ts, lib/constants.ts, lib/demo.ts, lib/status-utils.ts, lib/live-status-utils.ts
  5. Create lib/geocoding-client.ts — Nominatim client (NEW)
  6. Create lib/bike-routing-client.ts — OSRM client (NEW)

Phase 3 — API Routes (~45 min)

  1. src/app/api/hafas/route.ts — POST handler, same logic as Express
  2. src/app/api/calendar/route.ts — GET handler for remote ICS
  3. src/app/api/calendar/parse/route.ts — POST handler for ICS body
  4. src/app/api/geocode/route.ts — GET handler for Nominatim (NEW)
  5. src/app/api/bike-route/route.ts — GET handler for OSRM (NEW)
  6. src/app/api/health/route.ts — health check

Phase 4 — Custom Hooks (~2.5 hours)

  1. useServerHealth.ts — polls /api/health every 30s
  2. useClock.ts — interval that updates now every 10s
  3. useGeolocation.ts — wraps navigator.geolocation
  4. useOriginStation.ts — finds nearest station from geolocation
  5. useJourneys.ts — the complex fetchAll logic, per-event journey fetching
  6. useBikeRoute.ts — fetches bicycle route for an event (NEW)
  7. useCalendar.ts — URL/file import with merge logic
  8. useEventsStore.ts — shared events state via Context (NEW)

Phase 5 — UI Components (~3.5 hours)

  1. ui/Chip.tsx — small badge component
  2. ui/Button.tsx — styled button
  3. ui/LoadingSpinner.tsx — loading indicator
  4. event/LeaveByBadge.tsx — countdown badge
  5. event/JourneyList.tsx — departure rows
  6. event/TrainSection.tsx — train data in event card
  7. event/BikeSection.tsx — bicycle data in event card (NEW)
  8. event/EventCard.tsx — composes train + bike sections
  9. calendar/UrlTab.tsx
  10. calendar/FileTab.tsx
  11. calendar/CalendarPanel.tsx
  12. add-event/AddEventModal.tsx
  13. layout/Header.tsx
  14. layout/Navbar.tsx (NEW)

Phase 6 — Calendar Page (~1 hour)

  1. calendar/CalendarView.tsx — month grid component (NEW)
  2. calendar/DayEvents.tsx — events for a selected day (NEW)
  3. 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 jsdom and create vitest.config.ts. Update the test script in package.json to vitest.

  1. Migrate server/__tests__/*.test.jssrc/app/api/__tests__/*.test.ts
  2. Add unit tests for src/lib/calendar-utils.ts, src/lib/countdown-utils.ts in src/lib/__tests__/
  3. Add API tests for geocode and bike-route in src/app/api/__tests__/ (NEW)
  4. Add component smoke tests with @testing-library/react (optional)

Phase 8 — Cleanup (~30 min)

  1. Delete old server/ directory
  2. Delete old oebb-planner-app/ directory
  3. Delete oebb-planner.jsx
  4. Update README.md with new architecture and instructions
  5. 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 autoprefixer or a separate tailwind.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

  1. Single processnpm run dev starts everything
  2. Type safety — TypeScript catches bugs at compile time
  3. Composability — 700-line component becomes ~15 focused components
  4. Reusabilitylib/ functions testable independently
  5. Deployable — one artifact, works on Vercel / any Node host
  6. Maintainable — hooks encapsulate side effects, components are pure UI
  7. Modern tooling — no more CRA, no more react-scripts eject anxiety
  8. Bicycle routing — complete picture of train vs bike for each event
  9. Calendar view — browse events by date, not just a flat list
  10. 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