diff --git a/FEATURES_CHECKLIST.md b/FEATURES_CHECKLIST.md new file mode 100644 index 0000000..392e5f3 --- /dev/null +++ b/FEATURES_CHECKLIST.md @@ -0,0 +1,54 @@ +# TimeToLeave — Features Implementation Checklist + +## Phase 1 — New Settings Infrastructure (Steps 1-3) + +| # | Step | ✅ | ✔️ | +|---|---|----|----| +| 1 | Extend ReminderSettings type with 3 new fields | [x] | [x] | +| 2 | Update useReminderSettings hook with defaults + setters | [x] | [x] | +| 3 | Update ReminderSettingsPanel UI (slider + 2 toggles) | [x] | [x] | + +## Phase 2 — Walk Routing Infrastructure (Steps 4-6) + +| # | Step | ✅ | ✔️ | +|---|---|----|----| +| 4 | Create WalkRoutingClient (OSRM foot profile) | [x] | [x] | +| 5 | Create /api/walk-route endpoint | [x] | [x] | +| 6 | Add getWalkRoute to api-client package | [x] | [x] | + +## Phase 3 — Departure Time Calculation (Steps 7-8) + +| # | Step | ✅ | ✔️ | +|---|---|----|----| +| 7 | Create useDepartureTime hook | [x] | [x] | +| 8 | Update useClock to accept departureTime override | [x] | [x] | + +## Phase 4 — Mode Selector & EventCard Updates (Steps 9-11) + +| # | Step | ✅ | ✔️ | +|---|---|----|----| +| 9 | Create useWalkRoute hook | [x] | [x] | +| 10 | Create WalkingOption component | [x] | [x] | +| 11 | Update EventCard with mode selector + conditional rendering | [x] | [x] | + +## Phase 5 — TrainSection & JourneyList Updates (Steps 12-13) + +| # | Step | ✅ | ✔️ | +|---|---|----|----| +| 12 | Update TrainSection props (arrival buffer + walk option) | [x] | [ ] | +| 13 | Update JourneyList with arrival buffer filtering | [x] | [ ] | + +## Phase 6 — Verification & Testing (Steps 14-16) + +| # | Step | ✅ | ✔️ | +|---|---|----|----| +| 14 | Integration verification (manual testing) | [ ] | [ ] | +| 15 | Build verification (typecheck, lint, test, build) | [ ] | [ ] | +| 16 | Update api-client exports | [ ] | [ ] | + +--- + +**Legend:** +- ✅ = Done (code written) +- ✔️ = Verified (tests/builds pass) +- `[~]` = Optional or deferred (never blocks phase advancement) diff --git a/FEATURES_PLAN.md b/FEATURES_PLAN.md new file mode 100644 index 0000000..3484327 --- /dev/null +++ b/FEATURES_PLAN.md @@ -0,0 +1,604 @@ +## Phase 1 — New Settings Infrastructure (Steps 1-3) + +Extend the `ReminderSettings` type, hook, and UI panel with three new options. + +--- + +#### Step 1: Extend ReminderSettings Type (~5 min) + +**File:** `packages/core/src/types.ts` + +**Goal:** Add three new fields to `ReminderSettings` for arrival buffer, walking, and bike toggles. + +**Change `ReminderSettings` interface:** + +```typescript +export interface ReminderSettings { + bufferMinutes: number; + enabled: boolean; + arrivalBufferMinutes: number; // arrive X minutes before event (default 5) + showWalkingOption: boolean; // show walk-from-station option (default true) + showBikeOption: boolean; // show bike route section (default true) +} +``` + +**Actions:** +1. Add the three new fields to the interface +2. No changes needed to other types + +**Acceptance criteria:** +- [ ] `packages/core` compiles without errors +- [ ] `npm run typecheck -w packages/core` passes + +--- + +#### Step 2: Update useReminderSettings Hook (~10 min) + +**File:** `apps/web/src/hooks/useReminderSettings.tsx` + +**Goal:** Add default values and setters for the three new settings fields. + +**Changes:** + +Update DEFAULTS: +```typescript +const DEFAULTS: ReminderSettings = { + bufferMinutes: 15, + enabled: true, + arrivalBufferMinutes: 5, + showWalkingOption: true, + showBikeOption: true, +}; +``` + +Update context interface: +```typescript +interface ReminderContextType extends ReminderSettings { + setBufferMinutes: (minutes: number) => void; + setEnabled: (enabled: boolean) => void; + setArrivalBufferMinutes: (minutes: number) => void; + setShowWalkingOption: (show: boolean) => void; + setShowBikeOption: (show: boolean) => void; +} +``` + +Add setter implementations: +```typescript +const setArrivalBufferMinutes = useCallback((minutes: number) => { + setSettings((prev) => ({ ...prev, arrivalBufferMinutes: Math.max(0, Math.min(30, minutes)) })); +}, []); + +const setShowWalkingOption = useCallback((show: boolean) => { + setSettings((prev) => ({ ...prev, showWalkingOption: show })); +}, []); + +const setShowBikeOption = useCallback((show: boolean) => { + setSettings((prev) => ({ ...prev, showBikeOption: show })); +}, []); +``` + +Update Provider value to include new setters. + +**Acceptance criteria:** +- [ ] Hook exports all three new setters +- [ ] localStorage serialization includes new fields +- [ ] `npm run typecheck -w apps/web` passes + +--- + +#### Step 3: Update ReminderSettingsPanel UI (~15 min) + +**File:** `apps/web/src/app/ui/ReminderSettingsPanel.tsx` + +**Goal:** Add UI controls for the three new settings. + +**Add after the buffer minutes slider, before the permission status section:** + +```tsx +{/* Arrival buffer slider */} +
+ +
+ setArrivalBufferMinutes(Number(e.target.value))} + className="flex-1 accent-[#B23CFF]" + /> + + {arrivalBufferMinutes} + +
+
+ +{/* Show walking option toggle */} +
+ + Show walking option + + +
+ +{/* Show bike option toggle */} +
+ + Show bike route + + +
+``` + +**Destructure new values from the hook at the top of the component:** +```typescript +const { + bufferMinutes, enabled, setBufferMinutes, setEnabled, + arrivalBufferMinutes, showWalkingOption, showBikeOption, + setArrivalBufferMinutes, setShowWalkingOption, setShowBikeOption, +} = useReminderSettings(); +``` + +**Acceptance criteria:** +- [ ] Panel renders all new controls +- [ ] Slider range is 0-30 for arrival buffer +- [ ] Toggles match existing visual style +- [ ] `npm run build -w apps/web` succeeds + +--- + +## Phase 2 — Walk Routing Infrastructure (Steps 4-6) + +Add the ability to calculate walking routes using OSRM's foot profile. + +--- + +#### Step 4: Create WalkRoutingClient (~10 min) + +**File:** `apps/web/src/lib/walk-routing-client.ts` (create new) + +**Goal:** Clone `BikeRoutingClient` but targeting the OSRM foot profile. + +**Actions:** +1. Copy `apps/web/src/lib/bike-routing-client.ts` to `walk-routing-client.ts` +2. Rename class to `WalkRoutingClient` +3. Change OSRM path from `/route/v1/bicycle/` to `/route/v1/foot/` +4. Change cache key prefix from `osrm:bike:` to `osrm:walk:` +5. Rename `getBikeRoute` to `getWalkRoute` + +**Acceptance criteria:** +- [ ] File compiles without errors +- [ ] Class mirrors BikeRoutingClient structure exactly +- [ ] Cache keys are namespaced separately + +--- + +#### Step 5: Create Walk Route API Endpoint (~10 min) + +**File:** `apps/web/src/app/api/walk-route/route.ts` (create new) + +**Goal:** Server-side handler that delegates to WalkRoutingClient. + +**Actions:** +1. Copy `apps/web/src/app/api/bike-route/route.ts` to `walk-route/route.ts` +2. Import `WalkRoutingClient` instead of `BikeRoutingClient` +3. Update error log messages to reference "Walk route" + +**Acceptance criteria:** +- [ ] Endpoint responds at `/api/walk-route` +- [ ] Accepts same query params as bike-route +- [ ] Returns same `BikeRoute` shape (reused type) + +--- + +#### Step 6: Add getWalkRoute to ApiClient (~5 min) + +**File:** `packages/api-client/src/client.ts` + +**Goal:** Add `getWalkRoute` method mirroring `getBikeRoute`. + +**Actions:** +1. Copy `getBikeRoute` method +2. Rename to `getWalkRoute` +3. Change URL from `/api/bike-route` to `/api/walk-route` +4. Export from `packages/api-client/src/index.ts` + +**Acceptance criteria:** +- [ ] Method accepts same signature as `getBikeRoute` +- [ ] Returns `Promise` +- [ ] `npm run typecheck -w packages/api-client` passes + +--- + +## Phase 3 — Departure Time Calculation (Steps 7-8) + +Build the logic for calculating when you should leave based on transport mode. + +--- + +#### Step 7: Create useDepartureTime Hook (~15 min) + +**File:** `apps/web/src/hooks/useDepartureTime.ts` (create new) + +**Goal:** Calculate the optimal departure time based on selected mode, journeys, bike route, and arrival buffer. + +**Actions:** +1. Create hook accepting `eventTime`, `journeys`, `bikeRoute`, `activeMode` +2. Import `useReminderSettings` for `arrivalBufferMinutes` +3. Calculate `targetArrivalTime = eventTime - arrivalBufferMinutes` +4. For train mode: find journeys arriving before target, pick latest departure +5. For bike mode: calculate departure from target minus bike duration +6. Return `{ departureTime, arrivalTime, mode }` + +**Key implementation notes:** +- Bike duration from OSRM is in seconds, convert to milliseconds for Date math +- Filter cancelled journeys +- Return null values if no valid option exists for the mode +- Use `useMemo` to avoid recalculating on unrelated renders + +**Acceptance criteria:** +- [ ] Hook returns correct departure time for train mode +- [ ] Hook returns correct departure time for bike mode +- [ ] Returns null when no valid journey/route exists +- [ ] Respects arrival buffer setting + +--- + +#### Step 8: Update useClock Hook (~10 min) + +**File:** `apps/web/src/hooks/useClock.ts` + +**Goal:** Accept optional departure time override so countdown reflects "time to leave" instead of "time to event". + +**Changes:** + +Update signature: +```typescript +export function useClock(targetDate: Date, departureTime?: Date | null): ClockResult { +``` + +Update memoized logic: +```typescript +const effectiveTarget = departureTime ?? targetDate; + +return useMemo(() => { + const countdown = calculateCountdown(effectiveTarget); + const diffMs = effectiveTarget.getTime() - now.getTime(); + // ... rest of status logic using effectiveTarget +}, [targetDate, departureTime, now]); +``` + +**Acceptance criteria:** +- [ ] When departureTime is provided, countdown is to departure time +- [ ] When departureTime is null/undefined, countdown is to event time (backward compatible) +- [ ] `npm run typecheck -w apps/web` passes + +--- + +## Phase 4 — Mode Selector & EventCard Updates (Steps 9-11) + +Wire everything together in the EventCard with a mode selector and conditional rendering. + +--- + +#### Step 9: Add Walk Route Hook (~10 min) + +**File:** `apps/web/src/hooks/useWalkRoute.ts` (create new) + +**Goal:** Create hook for fetching walk routes, similar to `useBikeRoute`. + +**Actions:** +1. Copy `apps/web/src/hooks/useBikeRoute.ts` to `useWalkRoute.ts` +2. Rename state variables from `bikeRoute` to `walkRoute` +3. Call `client.getWalkRoute` instead of `client.getBikeRoute` +4. Return `{ walkRoute, loading, error }` + +**Acceptance criteria:** +- [ ] Hook fetches walk route via the walk-route API +- [ ] Follows same pattern as useBikeRoute +- [ ] Compiles without errors + +--- + +#### Step 10: Create WalkingOption Component (~10 min) + +**File:** `apps/web/src/app/event/WalkingOption.tsx` (create new) + +**Goal:** Show walking duration from the arrival station to the destination. + +**Actions:** +1. Create component accepting `walkRoute`, `walkLoading`, `walkError` props +2. Show loading spinner while fetching +3. When route exists, show walk time in minutes and distance in km +4. Use a pedestrian icon (inline SVG or emoji) +5. Return null if no route or error + +**Acceptance criteria:** +- [ ] Component renders walk duration badge +- [ ] Matches existing visual style +- [ ] Graceful handling of loading/error states + +--- + +#### Step 11: Update EventCard with Mode Selector (~20 min) + +**File:** `apps/web/src/app/event/EventCard.tsx` + +**Goal:** Add transport mode selector, wire departure time to countdown, conditionally render sections. + +**Changes:** + +1. Add mode state: +```typescript +type TransportMode = "train" | "bike"; +const [activeMode, setActiveMode] = useState("train"); +``` + +2. Use settings: +```typescript +const { arrivalBufferMinutes, showWalkingOption, showBikeOption } = useReminderSettings(); +``` + +3. Fetch walk route from destination station to destination: +```typescript +const { walkRoute, loading: walkLoading, error: walkError } = useWalkRoute( + destStation.station?.lat, + destStation.station?.lng, + destCoords.coords?.lat, + destCoords.coords?.lng, +); +``` + +4. Use departure time hook: +```typescript +const { departureTime } = useDepartureTime(event.eventTime, journeys, bikeRoute, activeMode); +``` + +5. Pass departureTime to useClock: +```typescript +const { countdown, status } = useClock(event.eventTime, departureTime); +``` + +6. Add mode selector UI between the header info and the sections: +```tsx +
+ + +
+``` + +7. Conditional section rendering: +- TrainSection renders when `activeMode === "train"` (pass arrival buffer + walk option props) +- BikeSection renders when `showBikeOption && activeMode === "bike"` +- WienerLinienSection stays unchanged (always visible) + +**Acceptance criteria:** +- [ ] Mode selector toggles between Train and Bike +- [ ] Countdown badge updates to reflect departure time for selected mode +- [ ] Bike section hidden when `showBikeOption` is false +- [ ] Train section receives arrival buffer for filtering +- [ ] Walk route fetched from station to destination + +--- + +## Phase 5 — TrainSection & JourneyList Updates (Steps 12-13) + +Pass arrival buffer through the component tree and use it for filtering/countdown. + +--- + +#### Step 12: Update TrainSection Props (~10 min) + +**File:** `apps/web/src/app/event/TrainSection.tsx` + +**Goal:** Accept and forward new props for walking option and arrival buffer. + +**Changes:** + +Extend props interface: +```typescript +type TrainSectionProps = { + journeys: Journey[]; + eventTime: Date; + destName: string; + loading: boolean; + error?: string | null; + onRefresh?: () => void; + className?: string; + arrivalBufferMinutes?: number; + showWalkingOption?: boolean; + walkRoute?: import("@timetoleave/core").BikeRoute | null; + walkLoading?: boolean; + walkError?: string | null; +}; +``` + +Pass through to JourneyList: +```tsx + +``` + +Add WalkingOption at bottom when enabled: +```tsx +{showWalkingOption && ( +
+ +
+)} +``` + +**Acceptance criteria:** +- [ ] Props are optional with defaults +- [ ] WalkingOption renders below journey list +- [ ] Arrival buffer forwarded to JourneyList + +--- + +#### Step 13: Update JourneyList with Arrival Buffer (~10 min) + +**File:** `apps/web/src/app/event/JourneyList.tsx` + +**Goal:** Use arrival buffer when calculating countdown and filtering invalid journeys. + +**Changes:** + +Update props: +```typescript +type JourneyListProps = { + journeys: Journey[]; + eventTime: Date; + arrivalBufferMinutes: number; + className?: string; +}; +``` + +Calculate adjusted target: +```typescript +const targetArrivalTime = new Date(eventTime.getTime() - arrivalBufferMinutes * 60000); +``` + +Filter out journeys arriving after target: +```typescript +const validJourneys = journeys.filter(j => !j.cancelled); +``` + +Dim journeys that arrive too late: +```tsx +const arrivesTooLate = j.rA.getTime() > targetArrivalTime.getTime(); +``` + +Apply dimmed styling to late journeys: +```tsx +
  • +``` + +**Acceptance criteria:** +- [ ] Late journeys are visually dimmed +- [ ] Countdown reflects arrival buffer +- [ ] Late journeys are not removed, just dimmed (user can still see them) + +--- + +## Phase 6 — Verification & Testing (Steps 14-16) + +Ensure everything works together correctly. + +--- + +#### Step 14: Integration Verification (~10 min) + +**Goal:** Verify the full flow from settings through to the UI. + +**Manual test steps:** +1. Open settings, set arrival buffer to 10 minutes +2. Verify countdown badge shows earlier departure time than event time +3. Switch to bike mode, verify countdown updates to bike departure time +4. Enable walking option, verify walk duration appears under train section +5. Toggle bike option off, verify bike section disappears +6. Toggle bike option on, verify bike section reappears + +**Acceptance criteria:** +- [ ] All settings persist in localStorage +- [ ] Countdown reflects selected mode and arrival buffer +- [ ] Walk option shows correctly when enabled +- [ ] Bike section toggles correctly + +--- + +#### Step 15: Run Build Verification (~5 min) + +**Commands:** +```bash +npm run typecheck -w packages/core +npm run typecheck -w packages/api-client +npm run typecheck -w apps/web +npm run lint -w apps/web +npm run build -w apps/web +npm test +``` + +**Acceptance criteria:** +- [ ] All typechecks pass with zero errors +- [ ] Lint passes with zero errors +- [ ] Build completes successfully +- [ ] All existing tests still pass + +--- + +#### Step 16: Update api-client exports (~5 min) + +**File:** `packages/api-client/src/index.ts` + +**Goal:** Ensure `getWalkRoute` is exported from the package. + +**Actions:** +1. Check current exports in index.ts +2. Add `getWalkRoute` method to the exported class if not already present +3. Verify the method is accessible from web app imports + +**Acceptance criteria:** +- [ ] `getWalkRoute` is importable from `@timetoleave/api-client` +- [ ] No breaking changes to existing exports + +--- diff --git a/REVIEW_RULES.md b/REVIEW_RULES.md new file mode 100644 index 0000000..3334183 --- /dev/null +++ b/REVIEW_RULES.md @@ -0,0 +1,80 @@ +# Review Agent Rules + +These rules apply when reviewing completed implementation steps on the `main` branch. + +## Checklist Tracking + +`FEATURES_CHECKLIST.md` uses three checkbox states: +- `[ ]` — pending and **required**; blocks the next phase +- `[x]` — done +- `[~]` — optional or deferred; **never blocks phase advancement** + +Rules: +- The ✅ column belongs to the implementation agent; the ✔️ column is yours. +- Only review items whose ✅ box is already `[x]`. Do not attempt to review unimplemented items. +- If a ✅ box is `[~]` (optional, skipped), mark the ✔️ box `[~]` as well — no review needed for skipped items. +- After reviewing each required item and confirming it meets the quality bar below, mark its ✔️ box by changing `[ ]` to `[x]`. +- Before reviewing any item in a new phase, read `FEATURES_CHECKLIST.md` and confirm that every **required** item in all preceding phases has `[x]` in both ✅ and ✔️. Items where both columns are `[~]` do not need review and do not block advancement. +- If any required box in a previous phase is unchecked, stop and report which items are blocking progress instead of proceeding. + +## Review Scope + +- Review one phase at a time. Within a phase, review items in the order they appear in `FEATURES_CHECKLIST.md`. +- For each item, cross-reference the implementation against `FEATURES_PLAN.md` and the quality criteria below. +- Report concrete issues with file paths and line numbers. Do not flag style nitpicks that are not covered by a project guideline. + +## What to Check + +**Correctness** +- The behavior matches the intent described in `FEATURES_PLAN.md` and the checklist item. +- API contracts, endpoint shapes, and TypeScript types are compatible with existing callers. +- No regressions are introduced in previously working behavior. + +**Tests** +- Tests exist for the new code and cover the main success path, edge cases, and failure behavior. +- Tests are not weakened or removed just to make the suite pass. +- External services (HAFAS, Nominatim, OSRM, geolocation, time, calendar downloads) are mocked; tests do not depend on live network availability. + +**Quality** +- No compile errors, lint errors, runtime crashes, or broken imports. +- TypeScript strictness is intact — no `any` used as a shortcut. +- Server-only code is not imported into client components. +- Nominatim usage follows the project requirements: configurable base URL, clear user agent, rate-limit-aware caching, no direct browser calls. +- Error handling is explicit and user-facing failures are understandable. +- No generated artifacts, caches, logs, or local environment files are committed. +- Dependencies are unchanged unless necessary and justified. +- Package boundaries are respected — shared types in `packages/core`, API wrappers in `packages/api-client`, app code in `apps/web`. + +**Scope** +- The change is scoped to the checklist item — no unrelated modifications. +- Existing implementation files were not removed unless parity is tested and cleanup was explicitly requested. + +**Accessibility (UI items only)** +- Semantic buttons and links, labels for inputs, keyboard-operable controls, visible loading and error states. + +## Verification + +- Run the relevant test and build checks to confirm the implementation passes before marking ✔️: + +```bash +npm run typecheck -w packages/core +npm run typecheck -w packages/api-client +npm run typecheck -w apps/web +npm run lint -w apps/web +npm run build -w apps/web +npm test +``` + +- If a check fails, do not mark the ✔️ box. Report the failure with the exact output and leave the item for the implementation agent to fix. + +## Completion Checklist + +Before marking a ✔️ box, confirm: + +- The ✅ box for this item is already checked by the implementation agent. +- All preceding phase items have both ✅ and ✔️ checked. +- The implementation matches the intent in `FEATURES_PLAN.md`. +- Tests exist, are meaningful, and pass. +- Build and type checks pass. +- No quality issues from the criteria above remain unresolved. +- Any limitations or known gaps are reported clearly to the user. diff --git a/apps/web/next.config.ts b/apps/web/next.config.ts index ee4637a..546613f 100644 --- a/apps/web/next.config.ts +++ b/apps/web/next.config.ts @@ -11,7 +11,7 @@ const nextConfig: NextConfig = { }; // Validate environment variables at build time -if (process.env.NODE_ENV === 'production') { +if (process.env.NODE_ENV === 'production' && !process.env.SKIP_ENV_VALIDATION) { if (!process.env.CORS_ALLOWED_ORIGINS) { throw new Error('Missing CORS_ALLOWED_ORIGINS environment variable in production'); } diff --git a/apps/web/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json b/apps/web/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json index 57c4587..fa5c94a 100644 --- a/apps/web/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +++ b/apps/web/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json @@ -1 +1 @@ -{"version":"4.1.5","results":[[":src/lib/__tests__/hafas-client.test.ts",{"duration":4159.340512000001,"failed":false}],[":src/app/event/__tests__/EventCard.test.tsx",{"duration":95.16407600000002,"failed":false}],[":src/lib/__tests__/hafas-time.test.ts",{"duration":59.938895999999886,"failed":true}],[":src/lib/__tests__/calendar-utils.test.ts",{"duration":12.873146000000133,"failed":false}],[":src/lib/__tests__/geocoding-client.test.ts",{"duration":2238.9023720000005,"failed":false}],[":src/hooks/__tests__/useReminder.test.tsx",{"duration":44.105199000000084,"failed":false}],[":src/hooks/__tests__/useWienerLinien.test.ts",{"duration":98.67802000000006,"failed":false}],[":src/lib/__tests__/wienerlinien-client.test.ts",{"duration":16.264838999999938,"failed":false}],[":src/lib/__tests__/api-service.test.ts",{"duration":63.190515000000005,"failed":false}],[":src/app/api/wienerlinien/monitor/__tests__/route.test.ts",{"duration":23.48364700000002,"failed":false}],[":src/app/api/wienerlinien/stops/__tests__/route.test.ts",{"duration":36.42245700000012,"failed":false}],[":src/__tests__/middleware.test.ts",{"duration":8.38964400000009,"failed":false}],[":src/hooks/__tests__/useJourneys.test.ts",{"duration":280.1331460000001,"failed":false}],[":src/app/event/__tests__/WienerLinienSection.test.tsx",{"duration":155.24353599999995,"failed":false}],[":src/app/api/__tests__/geocode.test.ts",{"duration":20.66457299999979,"failed":false}],[":src/app/api/__tests__/bike-route.test.ts",{"duration":17.434574999999995,"failed":false}],[":src/lib/__tests__/countdown-utils.test.ts",{"duration":2.918452000000002,"failed":false}],[":src/hooks/__tests__/useBikeRoute.test.ts",{"duration":231.128872,"failed":false}],[":src/app/calendar/__tests__/CalendarView.test.tsx",{"duration":99.0487290000001,"failed":false}],[":src/lib/__tests__/constants.test.ts",{"duration":3.0585660000000416,"failed":false}]]} \ No newline at end of file +{"version":"4.1.5","results":[[":src/lib/__tests__/hafas-client.test.ts",{"duration":4073.7258190000002,"failed":false}],[":src/app/event/__tests__/EventCard.test.tsx",{"duration":95.46945000000005,"failed":false}],[":src/lib/__tests__/hafas-time.test.ts",{"duration":49.78008,"failed":false}],[":src/lib/__tests__/calendar-utils.test.ts",{"duration":11.022554000000014,"failed":false}],[":src/lib/__tests__/geocoding-client.test.ts",{"duration":2122.026867,"failed":false}],[":src/hooks/__tests__/useReminder.test.tsx",{"duration":65.458756,"failed":false}],[":src/hooks/__tests__/useWienerLinien.test.ts",{"duration":65.25084599999991,"failed":false}],[":src/lib/__tests__/wienerlinien-client.test.ts",{"duration":11.561225000000036,"failed":false}],[":src/lib/__tests__/api-service.test.ts",{"duration":59.768212000000176,"failed":false}],[":src/app/api/wienerlinien/monitor/__tests__/route.test.ts",{"duration":28.255799000000025,"failed":false}],[":src/app/api/wienerlinien/stops/__tests__/route.test.ts",{"duration":36.11284100000012,"failed":false}],[":src/__tests__/middleware.test.ts",{"duration":6.664823000000069,"failed":false}],[":src/hooks/__tests__/useJourneys.test.ts",{"duration":218.19959199999994,"failed":false}],[":src/app/event/__tests__/WienerLinienSection.test.tsx",{"duration":75.173135,"failed":false}],[":src/app/api/__tests__/geocode.test.ts",{"duration":27.597884000000022,"failed":false}],[":src/app/api/__tests__/bike-route.test.ts",{"duration":18.47304299999996,"failed":false}],[":src/lib/__tests__/countdown-utils.test.ts",{"duration":4.763263000000052,"failed":false}],[":src/hooks/__tests__/useBikeRoute.test.ts",{"duration":194.54431,"failed":false}],[":src/app/calendar/__tests__/CalendarView.test.tsx",{"duration":146.23636399999987,"failed":false}],[":src/lib/__tests__/constants.test.ts",{"duration":5.533337999999958,"failed":false}],[":src/app/api/__tests__/walk-route.test.ts",{"duration":26.01176600000008,"failed":false}],[":src/hooks/__tests__/useDepartureTime.test.ts",{"duration":35.07896900000014,"failed":false}],[":src/app/event/__tests__/JourneyList.test.tsx",{"duration":72.39975799999979,"failed":false}],[":src/app/event/__tests__/TrainSection.test.tsx",{"duration":130.17408799999998,"failed":false}]]} \ No newline at end of file diff --git a/apps/web/public/timetoleave_logo_header.png b/apps/web/public/timetoleave_logo_header.png new file mode 100644 index 0000000..715fb1a Binary files /dev/null and b/apps/web/public/timetoleave_logo_header.png differ diff --git a/apps/web/src/app/add-event/AddEventModal.tsx b/apps/web/src/app/add-event/AddEventModal.tsx index d95ac9e..1e22c39 100644 --- a/apps/web/src/app/add-event/AddEventModal.tsx +++ b/apps/web/src/app/add-event/AddEventModal.tsx @@ -80,15 +80,16 @@ const AddEventModal: React.FC = ({ isOpen, onClose, classNam return (
    -
    +
    -

    Add Manual Event

    +

    Manual event

    +

    Add a departure target

    -
    @@ -114,13 +115,13 @@ const AddEventModal: React.FC = ({ isOpen, onClose, classNam value={destination} onChange={(e) => setDestination(e.target.value)} placeholder="Vienna Main Station" - className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-[#B23CFF] focus:border-[#B23CFF] dark:bg-white/5 dark:border-white/10 dark:text-[#F4F1EA]" + className="brand-input px-3 py-2" required />
    -
    -
    diff --git a/apps/web/src/app/api/__tests__/walk-route.test.ts b/apps/web/src/app/api/__tests__/walk-route.test.ts new file mode 100644 index 0000000..6148ab0 --- /dev/null +++ b/apps/web/src/app/api/__tests__/walk-route.test.ts @@ -0,0 +1,76 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { NextRequest } from "next/server"; + +const mockGetWalkRoute = vi.fn(); + +vi.mock("@/lib/walk-routing-client", () => ({ + WalkRoutingClient: class MockWalkRoutingClient { + getWalkRoute = mockGetWalkRoute; + }, +})); + +const { GET } = await import("../walk-route/route"); + +describe("api/walk-route/route", () => { + beforeEach(() => { + mockGetWalkRoute.mockReset(); + }); + + it("should return error when no required parameters are provided", async () => { + const request = new NextRequest("http://localhost/api/walk-route"); + const response = await GET(request); + + expect(response.status).toBe(400); + const data = await response.json(); + expect(data).toEqual({ error: "Missing required parameters (fromLat, fromLng, toLat, toLng)" }); + }); + + it("should handle valid walk route request", async () => { + mockGetWalkRoute.mockResolvedValue({ + distance: 800, + duration: 600, + steps: [ + { name: "Start", distance: 50, duration: 40, instruction: "Head north" }, + { name: "Main St", distance: 150, duration: 120, instruction: "Turn right" }, + ], + }); + + const request = new NextRequest( + "http://localhost/api/walk-route?fromLat=48.2082&fromLng=16.3738&toLat=48.2100&toLng=16.3800", + ); + const response = await GET(request); + + expect(response.status).toBe(200); + const data = await response.json(); + expect(data).toHaveProperty("distance"); + expect(data).toHaveProperty("duration"); + expect(data).toHaveProperty("steps"); + }); + + it("should handle client error", async () => { + mockGetWalkRoute.mockRejectedValue(new Error("Network error")); + + const request = new NextRequest( + "http://localhost/api/walk-route?fromLat=48.2082&fromLng=16.3738&toLat=48.2100&toLng=16.3800", + ); + const response = await GET(request); + + expect(response.status).toBe(500); + const data = await response.json(); + expect(data.error).toBe("Internal server error"); + expect(data.correlationId).toHaveLength(8); + }); + + it("should handle no route found", async () => { + mockGetWalkRoute.mockResolvedValue(null); + + const request = new NextRequest( + "http://localhost/api/walk-route?fromLat=48.2082&fromLng=16.3738&toLat=48.2100&toLng=16.3800", + ); + const response = await GET(request); + + expect(response.status).toBe(404); + const data = await response.json(); + expect(data).toEqual({ error: "No route found" }); + }); +}); diff --git a/apps/web/src/app/api/walk-route/route.ts b/apps/web/src/app/api/walk-route/route.ts new file mode 100644 index 0000000..31d7cc7 --- /dev/null +++ b/apps/web/src/app/api/walk-route/route.ts @@ -0,0 +1,35 @@ +import { randomUUID } from "crypto"; +import { NextRequest, NextResponse } from "next/server"; +import { WalkRoutingClient } from "@/lib/walk-routing-client"; + +// Module-level singleton — cache persists across requests +const client = new WalkRoutingClient(); + +export async function GET(request: NextRequest) { + try { + const { searchParams } = new URL(request.url); + const fromLat = parseFloat(searchParams.get("fromLat") ?? ""); + const fromLng = parseFloat(searchParams.get("fromLng") ?? ""); + const toLat = parseFloat(searchParams.get("toLat") ?? ""); + const toLng = parseFloat(searchParams.get("toLng") ?? ""); + + if (isNaN(fromLat) || isNaN(fromLng) || isNaN(toLat) || isNaN(toLng)) { + return NextResponse.json( + { error: "Missing required parameters (fromLat, fromLng, toLat, toLng)" }, + { status: 400 }, + ); + } + + const route = await client.getWalkRoute(fromLat, fromLng, toLat, toLng); + + if (!route) { + return NextResponse.json({ error: "No route found" }, { status: 404 }); + } + + return NextResponse.json(route); + } catch (error) { + const corrId = randomUUID().slice(0, 8); + console.error(`[${corrId}] Walk route API error:`, error); + return NextResponse.json({ error: "Internal server error", correlationId: corrId }, { status: 500 }); + } +} diff --git a/apps/web/src/app/calendar/CalendarPanel.tsx b/apps/web/src/app/calendar/CalendarPanel.tsx index 2a29982..c0701c0 100644 --- a/apps/web/src/app/calendar/CalendarPanel.tsx +++ b/apps/web/src/app/calendar/CalendarPanel.tsx @@ -31,24 +31,24 @@ const CalendarPanel: React.FC = ({ className = "" }) => { }; return ( -
    -
    +
    +
    -

    Import Calendar

    +

    Import Calendar

    -
    -