3b87b8c4e5
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.
535 lines
20 KiB
Markdown
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** |
|