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

535 lines
20 KiB
Markdown

# Ö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`
```typescript
// ── 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)
7. Define TypeScript types in `src/types/index.ts`
8. Port `lib/hafas-client.ts``HafasClient` class with `searchStation()` and `fetchJourneys()`
9. Port `lib/calendar-utils.ts``extractEvents()` + `cleanLocation()` (reusable in API routes AND tests)
10. Port `lib/countdown-utils.ts`, `lib/formatting.ts`, `lib/constants.ts`, `lib/demo.ts`, `lib/status-utils.ts`, `lib/live-status-utils.ts`
11. Create `lib/geocoding-client.ts` — Nominatim client (NEW)
12. Create `lib/bike-routing-client.ts` — OSRM client (NEW)
### Phase 3 — API Routes (~45 min)
13. `src/app/api/hafas/route.ts` — POST handler, same logic as Express
14. `src/app/api/calendar/route.ts` — GET handler for remote ICS
15. `src/app/api/calendar/parse/route.ts` — POST handler for ICS body
16. `src/app/api/geocode/route.ts` — GET handler for Nominatim (NEW)
17. `src/app/api/bike-route/route.ts` — GET handler for OSRM (NEW)
18. `src/app/api/health/route.ts` — health check
### Phase 4 — Custom Hooks (~2.5 hours)
19. `useServerHealth.ts` — polls `/api/health` every 30s
20. `useClock.ts` — interval that updates `now` every 10s
21. `useGeolocation.ts` — wraps `navigator.geolocation`
22. `useOriginStation.ts` — finds nearest station from geolocation
23. `useJourneys.ts` — the complex `fetchAll` logic, per-event journey fetching
24. `useBikeRoute.ts` — fetches bicycle route for an event (NEW)
25. `useCalendar.ts` — URL/file import with merge logic
26. `useEventsStore.ts` — shared events state via Context (NEW)
### Phase 5 — UI Components (~3.5 hours)
27. `ui/Chip.tsx` — small badge component
28. `ui/Button.tsx` — styled button
29. `ui/LoadingSpinner.tsx` — loading indicator
30. `event/LeaveByBadge.tsx` — countdown badge
31. `event/JourneyList.tsx` — departure rows
32. `event/TrainSection.tsx` — train data in event card
33. `event/BikeSection.tsx` — bicycle data in event card (NEW)
34. `event/EventCard.tsx` — composes train + bike sections
35. `calendar/UrlTab.tsx`
36. `calendar/FileTab.tsx`
37. `calendar/CalendarPanel.tsx`
38. `add-event/AddEventModal.tsx`
39. `layout/Header.tsx`
40. `layout/Navbar.tsx` (NEW)
### Phase 6 — Calendar Page (~1 hour)
41. `calendar/CalendarView.tsx` — month grid component (NEW)
42. `calendar/DayEvents.tsx` — events for a selected day (NEW)
43. `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`.
44. Migrate `server/__tests__/*.test.js``src/app/api/__tests__/*.test.ts`
45. Add unit tests for `src/lib/calendar-utils.ts`, `src/lib/countdown-utils.ts` in `src/lib/__tests__/`
46. Add API tests for `geocode` and `bike-route` in `src/app/api/__tests__/` (NEW)
47. Add component smoke tests with `@testing-library/react` _(optional)_
### Phase 8 — Cleanup (~30 min)
48. Delete old `server/` directory
49. Delete old `oebb-planner-app/` directory
50. Delete `oebb-planner.jsx`
51. Update `README.md` with new architecture and instructions
52. Final integration test
---
## 8. Dependencies
### Runtime (installed)
```json
{
"dependencies": {
"next": "16.2.6",
"react": "19.2.4",
"react-dom": "19.2.4",
"node-ical": "^0.18.0"
}
}
```
### Development (installed)
```json
{
"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)
```json
{
"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
# .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 process**`npm run dev` starts everything
2. **Type safety** — TypeScript catches bugs at compile time
3. **Composability** — 700-line component becomes ~15 focused components
4. **Reusability**`lib/` 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** |