# Ö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=&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** |