b131e3c69a
Phases 1 and 2.
518 lines
19 KiB
Markdown
518 lines
19 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.js
|
|
├── tsconfig.json
|
|
├── tailwind.config.ts
|
|
├── postcss.config.mjs
|
|
├── vitest.config.ts
|
|
├── 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.ts ← HAFAS API client + types
|
|
│ │ ├── calendar.ts ← ICS helpers (extractEvents, cleanLocation)
|
|
│ │ ├── countdown.ts ← leaveBy, countdownInfo
|
|
│ │ ├── formatting.ts ← fmtTime, delayColor
|
|
│ │ ├── demo.ts ← demo journey generator
|
|
│ │ ├── constants.ts ← WALK_MINS, RED, SAMPLE_EVENTS, HAFAS_BASE
|
|
│ │ ├── geocode.ts ← Nominatim client (NEW)
|
|
│ │ └── bike-route.ts ← OSRM client (NEW)
|
|
│ └── types/
|
|
│ └── index.ts ← all TypeScript interfaces
|
|
├── __tests__/
|
|
│ ├── lib/
|
|
│ │ ├── calendar.test.ts
|
|
│ │ ├── countdown.test.ts
|
|
│ │ └── bike-route.test.ts ← NEW
|
|
│ └── api/
|
|
│ ├── 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 | Replaces 700 lines of inline styles; consistent theming |
|
|
| 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.ts` — HAFAS API client functions with proper types
|
|
9. Port `lib/calendar.ts` — `extractEvents` + `cleanLocation` (reusable in API routes AND tests)
|
|
10. Port `lib/countdown.ts`, `lib/formatting.ts`, `lib/constants.ts`, `lib/demo.ts`
|
|
11. Create `lib/geocode.ts` — Nominatim client (NEW)
|
|
12. Create `lib/bike-route.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)
|
|
|
|
44. Migrate `server/__tests__/*.test.js` → `__tests__/api/*.test.ts`
|
|
45. Add unit tests for `lib/calendar.ts`, `lib/countdown.ts`
|
|
46. Add API tests for `geocode` and `bike-route` (NEW)
|
|
47. Add component smoke tests with `@testing-library/react`
|
|
|
|
### 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
|
|
|
|
```json
|
|
{
|
|
"dependencies": {
|
|
"next": "^15.0.0",
|
|
"react": "^19.0.0",
|
|
"react-dom": "^19.0.0",
|
|
"node-ical": "^0.18.0"
|
|
}
|
|
}
|
|
```
|
|
|
|
### Development
|
|
|
|
```json
|
|
{
|
|
"devDependencies": {
|
|
"typescript": "^5.6.0",
|
|
"@types/node": "^22.0.0",
|
|
"@types/react": "^19.0.0",
|
|
"@types/react-dom": "^19.0.0",
|
|
"@types/node-ical": "^0.18.0",
|
|
"tailwindcss": "^3.4.0",
|
|
"postcss": "^8.4.0",
|
|
"autoprefixer": "^10.4.0",
|
|
"vitest": "^2.0.0",
|
|
"@testing-library/react": "^16.0.0",
|
|
"@testing-library/jest-dom": "^6.0.0",
|
|
"jsdom": "^25.0.0"
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 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** |
|