Rewrite to Node.js
Phases 1 and 2.
This commit is contained in:
+517
@@ -0,0 +1,517 @@
|
||||
# Ö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** |
|
||||
Reference in New Issue
Block a user