Update BikeSection and EventCard with arrival buffer and walk options

This commit is contained in:
2026-05-12 13:52:35 +02:00
parent 1851d2ed47
commit 863996f06c
12 changed files with 218 additions and 82 deletions
+4 -4
View File
@@ -35,16 +35,16 @@
| # | Step | ✅ | ✔️ |
|---|---|----|----|
| 12 | Update TrainSection props (arrival buffer + walk option) | [x] | [ ] |
| 13 | Update JourneyList with arrival buffer filtering | [x] | [ ] |
| 12 | Update TrainSection props (arrival buffer + walk option) | [x] | [x] |
| 13 | Update JourneyList with arrival buffer filtering | [x] | [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 | [ ] | [ ] |
| 15 | Build verification (typecheck, lint, test, build) | [x] | [x] |
| 16 | Update api-client exports | [x] | [x] |
---
@@ -1 +1 @@
{"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}]]}
{"version":"4.1.5","results":[[":src/lib/__tests__/hafas-client.test.ts",{"duration":3812.27516,"failed":false}],[":src/app/event/__tests__/EventCard.test.tsx",{"duration":86.17672299999958,"failed":false}],[":src/lib/__tests__/hafas-time.test.ts",{"duration":42.62157499999989,"failed":false}],[":src/lib/__tests__/calendar-utils.test.ts",{"duration":13.673451999999997,"failed":false}],[":src/lib/__tests__/geocoding-client.test.ts",{"duration":2185.81884,"failed":false}],[":src/hooks/__tests__/useReminder.test.tsx",{"duration":45.468795,"failed":false}],[":src/hooks/__tests__/useWienerLinien.test.ts",{"duration":69.34623899999997,"failed":false}],[":src/lib/__tests__/wienerlinien-client.test.ts",{"duration":11.403096000000005,"failed":false}],[":src/lib/__tests__/api-service.test.ts",{"duration":43.37785599999984,"failed":false}],[":src/app/api/wienerlinien/monitor/__tests__/route.test.ts",{"duration":27.334359999999947,"failed":false}],[":src/app/api/wienerlinien/stops/__tests__/route.test.ts",{"duration":45.94808499999999,"failed":false}],[":src/__tests__/middleware.test.ts",{"duration":9.13286000000005,"failed":false}],[":src/hooks/__tests__/useJourneys.test.ts",{"duration":234.617387,"failed":false}],[":src/app/event/__tests__/WienerLinienSection.test.tsx",{"duration":64.99855000000002,"failed":false}],[":src/app/api/__tests__/geocode.test.ts",{"duration":19.906604000000016,"failed":false}],[":src/app/api/__tests__/bike-route.test.ts",{"duration":13.298808000000008,"failed":false}],[":src/lib/__tests__/countdown-utils.test.ts",{"duration":5.044317999999976,"failed":false}],[":src/hooks/__tests__/useBikeRoute.test.ts",{"duration":204.47145499999988,"failed":false}],[":src/app/calendar/__tests__/CalendarView.test.tsx",{"duration":108.45520299999998,"failed":false}],[":src/lib/__tests__/constants.test.ts",{"duration":7.98414200000002,"failed":false}],[":src/app/api/__tests__/walk-route.test.ts",{"duration":20.138449000000037,"failed":false}],[":src/hooks/__tests__/useDepartureTime.test.ts",{"duration":35.399114000000054,"failed":false}],[":src/app/event/__tests__/JourneyList.test.tsx",{"duration":98.73430799999983,"failed":false}],[":src/app/event/__tests__/TrainSection.test.tsx",{"duration":167.87446,"failed":false}],[":src/hooks/__tests__/useWalkRoute.test.ts",{"duration":139.1208529999999,"failed":false}]]}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 554 KiB

+16 -3
View File
@@ -11,10 +11,18 @@ type BikeSectionProps = {
bikeError: string | null | undefined;
onRefresh?: () => void;
className?: string;
forceVisible?: boolean;
};
const BikeSection: React.FC<BikeSectionProps> = ({ bikeRoute, bikeLoading, bikeError, onRefresh, className = "" }) => {
if (!bikeRoute && !bikeLoading && !bikeError) {
const BikeSection: React.FC<BikeSectionProps> = ({
bikeRoute,
bikeLoading,
bikeError,
onRefresh,
className = "",
forceVisible = false,
}) => {
if (!forceVisible && !bikeRoute && !bikeLoading && !bikeError) {
return null;
}
@@ -24,6 +32,7 @@ const BikeSection: React.FC<BikeSectionProps> = ({ bikeRoute, bikeLoading, bikeE
<div className="flex items-center justify-between">
<div>
<h3 className="text-lg font-semibold text-white">Bicycle Route</h3>
<p className="text-sm text-[#F4F1EA]/60">Door-to-door route to the event</p>
</div>
{onRefresh && (
<Button variant="secondary" size="sm" onClick={onRefresh}>
@@ -67,7 +76,11 @@ const BikeSection: React.FC<BikeSectionProps> = ({ bikeRoute, bikeLoading, bikeE
</div>
)}
</div>
) : null}
) : (
<div className="py-4 text-center text-sm text-[#F4F1EA]/58">
Add origin and destination coordinates to calculate a bike route.
</div>
)}
</div>
</div>
);
+72 -29
View File
@@ -60,17 +60,33 @@ export default function EventCard({ event, originStation }: EventCardProps) {
const { showWalkingOption, showBikeOption, arrivalBufferMinutes } = useReminderSettings();
type TransportMode = "train" | "bike";
const [activeMode, setActiveMode] = useState<TransportMode>("train");
const [requestedMode, setRequestedMode] = useState<TransportMode>("train");
const activeMode: TransportMode = !showBikeOption && requestedMode === "bike" ? "train" : requestedMode;
const { departureTime } = useDepartureTime(event.eventTime, journeys, bikeRoute?.duration || null, activeMode);
const { departureTime, arrivalTime, mode: calculatedMode } = useDepartureTime(
event.eventTime,
journeys,
bikeRoute?.duration ?? null,
activeMode,
);
const { countdown, status } = useClock(event.eventTime, departureTime);
const bikeDisabled = !showBikeOption;
const modeOptions: Array<{ id: TransportMode; label: string; meta: string; disabled?: boolean }> = [
{ id: "train", label: "Train", meta: showWalkingOption ? "Rail + final walk" : "Rail only" },
{
id: "bike",
label: "Bike",
meta: bikeDisabled ? "Disabled in settings" : bikeLoading ? "Calculating route" : "Door to door",
disabled: bikeDisabled,
},
];
return (
<div className="brand-panel overflow-hidden rounded-2xl p-4 sm:p-5">
<div className="mb-4 flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
<div>
<p className="mb-1 text-xs font-semibold uppercase tracking-\[0.24em\] text-\[#D946EF\]">Next stop</p>
<p className="mb-1 text-xs font-semibold uppercase tracking-[0.24em] text-[#D946EF]">Next stop</p>
<h3 className="text-2xl font-bold text-white">{event.title}</h3>
</div>
<CountdownBadge countdown={countdown} status={status} />
@@ -78,36 +94,63 @@ export default function EventCard({ event, originStation }: EventCardProps) {
<div className="mb-5 grid gap-3 sm:grid-cols-2">
<div className="brand-panel-soft rounded-xl p-3">
<p className="text-xs font-semibold uppercase tracking-\[0.18em\] text-\[#F4F1EA\]/42">Destination</p>
<p className="mt-1 text-sm font-medium text-\[#F4F1EA\]">{event.destination}</p>
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-[#F4F1EA]/42">Destination</p>
<p className="mt-1 text-sm font-medium text-[#F4F1EA]">{event.destination}</p>
</div>
<div className="brand-panel-soft rounded-xl p-3">
<p className="text-xs font-semibold uppercase tracking-\[0.18em\] text-\[#F4F1EA\]/42">Appointment</p>
<p className="mt-1 text-sm font-medium text-\[#F4F1EA\]">{format(event.eventTime, "EEE dd MMM yyyy HH:mm")}</p>
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-[#F4F1EA]/42">Appointment</p>
<p className="mt-1 text-sm font-medium text-[#F4F1EA]">{format(event.eventTime, "EEE dd MMM yyyy HH:mm")}</p>
</div>
</div>
<div className="mb-4 flex gap-2">
<button
className={`px-4 py-2 rounded text-sm font-medium transition-colors ${
activeMode === "train"
? "bg-\[#B23CFF\] text-white"
: "bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300"
}`}
onClick={() => setActiveMode("train")}
>
Train
</button>
<button
className={`px-4 py-2 rounded text-sm font-medium transition-colors ${
activeMode === "bike"
? "bg-\[#B23CFF\] text-white"
: "bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300"
}`}
onClick={() => setActiveMode("bike")}
>
Bike
</button>
<div className="mb-4 grid gap-3 sm:grid-cols-2">
{modeOptions.map((option) => (
<button
key={option.id}
className={`rounded-xl border p-3 text-left transition-all ${
activeMode === option.id
? "border-[#D946EF]/70 bg-[#B23CFF]/18 shadow-[0_14px_34px_rgba(178,60,255,0.2)]"
: "border-white/10 bg-white/[0.045] hover:border-[#D946EF]/40 hover:bg-white/[0.07]"
} ${option.disabled ? "cursor-not-allowed opacity-45 hover:border-white/10 hover:bg-white/[0.045]" : ""}`}
onClick={() => {
if (!option.disabled) {
setRequestedMode(option.id);
}
}}
disabled={option.disabled}
>
<span className="flex items-center justify-between gap-3">
<span className="font-semibold text-white">{option.label}</span>
{activeMode === option.id && (
<span className="rounded-full bg-[#FF2D8D]/20 px-2 py-0.5 text-xs font-semibold text-pink-100">
Active
</span>
)}
</span>
<span className="mt-1 block text-xs text-[#F4F1EA]/58">{option.meta}</span>
</button>
))}
</div>
<div className="mb-4 grid gap-3 rounded-xl border border-white/10 bg-black/16 p-3 text-sm sm:grid-cols-3">
<div>
<p className="text-xs font-semibold uppercase tracking-[0.16em] text-[#F4F1EA]/42">Leave by</p>
<p className="mt-1 font-semibold text-white">
{departureTime ? format(departureTime, "HH:mm") : "Pending"}
</p>
</div>
<div>
<p className="text-xs font-semibold uppercase tracking-[0.16em] text-[#F4F1EA]/42">Arrive by</p>
<p className="mt-1 font-semibold text-white">
{arrivalTime ? format(arrivalTime, "HH:mm") : format(new Date(event.eventTime.getTime() - arrivalBufferMinutes * 60_000), "HH:mm")}
</p>
</div>
<div>
<p className="text-xs font-semibold uppercase tracking-[0.16em] text-[#F4F1EA]/42">Buffer</p>
<p className="mt-1 font-semibold text-white">
{arrivalBufferMinutes} min {calculatedMode ? `via ${calculatedMode}` : ""}
</p>
</div>
</div>
<div className="space-y-4">
@@ -127,7 +170,7 @@ export default function EventCard({ event, originStation }: EventCardProps) {
)}
{showBikeOption && activeMode === "bike" && (
<BikeSection bikeRoute={bikeRoute} bikeLoading={bikeLoading} bikeError={bikeError} />
<BikeSection bikeRoute={bikeRoute} bikeLoading={bikeLoading} bikeError={bikeError} forceVisible />
)}
{stops.length > 0 && (
+22 -7
View File
@@ -1,7 +1,7 @@
"use client";
import React from "react";
import { Journey } from "@timetoleave/core";
import type { Journey } from "@timetoleave/core";
import { formatTime } from "@timetoleave/core";
import LeaveByBadge from "./LeaveByBadge";
import { calculateCountdown } from "@timetoleave/core";
@@ -17,7 +17,7 @@ const JourneyList: React.FC<JourneyListProps> = ({
journeys,
eventTime,
arrivalBufferMinutes,
className = ""
className = "",
}) => {
const targetArrivalTime = new Date(eventTime.getTime() - arrivalBufferMinutes * 60000);
@@ -29,12 +29,21 @@ const JourneyList: React.FC<JourneyListProps> = ({
<ul className={`space-y-3 p-4 ${className}`}>
{journeys.map((journey) => {
const arrivesTooLate = journey.rA.getTime() > targetArrivalTime.getTime();
const departure = journey.rD ?? journey.sD;
const arrival = journey.rA ?? journey.sA;
return (
<li key={journey.id} className={`rounded-xl border border-white/10 bg-white/[0.04] p-3 ${arrivesTooLate ? "opacity-40 line-through" : ""}`}>
<li
key={journey.id}
className={`rounded-xl border p-3 ${
arrivesTooLate || journey.cancelled
? "border-[#FF2D8D]/18 bg-[#FF2D8D]/7 opacity-40 line-through"
: "border-white/10 bg-white/[0.04]"
}`}
>
<div className="mb-2 flex items-center justify-between gap-3">
<div className="flex-1">
<span className="font-semibold text-white">{formatTime(journey.sD)}</span>
<span className="font-semibold text-white">{formatTime(departure)}</span>
<span className="ml-2 text-sm text-[#F4F1EA]/48">{journey.platform}</span>
{journey.delay > 0 && (
<span className="ml-2 text-sm font-medium text-orange-200">{`+${journey.delay}'`}</span>
@@ -46,11 +55,17 @@ const JourneyList: React.FC<JourneyListProps> = ({
</span>
</div>
<div className="flex-1 text-right">
<LeaveByBadge countdown={calculateCountdown(journey.sD)} />
<LeaveByBadge countdown={calculateCountdown(departure)} />
</div>
</div>
<div className="text-sm text-[#F4F1EA]/64">
{journey.changes > 0 ? <span>Change(s): {journey.changes}</span> : <span>Direct</span>}
<div className="flex flex-wrap items-center gap-x-3 gap-y-1 text-sm text-[#F4F1EA]/64">
<span>{journey.changes > 0 ? `Change(s): ${journey.changes}` : "Direct"}</span>
<span>Arrives {formatTime(arrival)}</span>
{arrivesTooLate && (
<span className="font-medium text-[#FF2D8D]">
misses {arrivalBufferMinutes} min buffer
</span>
)}
</div>
</li>
);
+9 -4
View File
@@ -1,7 +1,7 @@
"use client";
import React from "react";
import { Journey } from "@timetoleave/core";
import type { Journey, WalkRoute } from "@timetoleave/core";
import { formatDateTime } from "@timetoleave/core";
import WalkingOption from "./WalkingOption";
import JourneyList from "./JourneyList";
@@ -18,7 +18,7 @@ type TrainSectionProps = {
className?: string;
arrivalBufferMinutes?: number;
showWalkingOption?: boolean;
walkRoute?: import("@timetoleave/core").WalkRoute | null;
walkRoute?: WalkRoute | null;
walkLoading?: boolean;
walkError?: string | null;
};
@@ -46,6 +46,11 @@ const TrainSection: React.FC<TrainSectionProps> = ({
<p className="text-sm text-[#F4F1EA]/66">
To {destName} <span className="font-mono">{formatDateTime(eventTime)}</span>
</p>
{(arrivalBufferMinutes ?? 0) > 0 && (
<p className="mt-1 text-xs text-[#D946EF]">
Target arrival: {arrivalBufferMinutes} min early
</p>
)}
</div>
{onRefresh && (
<Button variant="secondary" size="sm" onClick={onRefresh}>
@@ -64,8 +69,8 @@ const TrainSection: React.FC<TrainSectionProps> = ({
) : (
<>
<JourneyList journeys={journeys} eventTime={eventTime} arrivalBufferMinutes={arrivalBufferMinutes ?? 0} />
{showWalkingOption && walkRoute && (
<div className="p-4 border-t border-gray-200 dark:border-white/10">
{showWalkingOption && (walkRoute || walkLoading || walkError) && (
<div className="border-t border-white/10 p-4">
<WalkingOption walkRoute={walkRoute} walkLoading={walkLoading} walkError={walkError} />
</div>
)}
+8 -7
View File
@@ -19,11 +19,12 @@ const WalkingOption: React.FC<WalkingOptionProps> = ({ walkRoute, walkLoading, w
}
return (
<div className={`mt-4 overflow-hidden rounded-xl border border-white/10 bg-black/16 ${className}`}>
<div className={`overflow-hidden rounded-xl border border-white/10 bg-black/16 ${className}`}>
<div className="border-b border-white/10 p-4">
<div className="flex items-center justify-between">
<div>
<h3 className="text-lg font-semibold text-white">Walking Option</h3>
<h3 className="text-lg font-semibold text-white">Final Walk</h3>
<p className="text-sm text-[#F4F1EA]/60">From arrival station to destination</p>
</div>
{onRefresh && (
<Button variant="secondary" size="sm" onClick={onRefresh}>
@@ -34,11 +35,11 @@ const WalkingOption: React.FC<WalkingOptionProps> = ({ walkRoute, walkLoading, w
</div>
<div className="p-4">
{walkLoading ? (
<div className="text-center py-4">
<div className="py-4 text-center">
<LoadingSpinner size="md" />
</div>
) : walkError ? (
<div className="text-center py-4 text-[#FF2D8D]">Error: {walkError}</div>
<div className="py-4 text-center text-[#FF2D8D]">Error: {walkError}</div>
) : walkRoute ? (
<div className="space-y-2">
<div className="flex justify-between">
@@ -55,12 +56,12 @@ const WalkingOption: React.FC<WalkingOptionProps> = ({ walkRoute, walkLoading, w
</div>
{walkRoute.steps && walkRoute.steps.length > 0 && (
<div className="mt-4">
<h4 className="mb-2 font-medium text-\[#F4F1EA\]">Steps</h4>
<h4 className="mb-2 font-medium text-[#F4F1EA]">Steps</h4>
<ul className="space-y-2">
{walkRoute.steps.map((step: { name: string; instruction: string }, index: number) => (
<li key={index} className="text-sm">
<span className="font-medium text-\[#D946EF\]">{step.name}</span>
<span className="ml-2 text-\[#F4F1EA\]/70">{step.instruction}</span>
<span className="font-medium text-[#D946EF]">{step.name}</span>
<span className="ml-2 text-[#F4F1EA]/70">{step.instruction}</span>
</li>
))}
</ul>
@@ -90,8 +90,8 @@ describe("useDepartureTime", () => {
expect(result.current.mode).toBe("bike");
// Should account for bike duration + arrival buffer (default 5 min)
// Total travel time: 30 min (bike) + 5 min (buffer) + 30 min (bike back) = 65 min
const expectedDepartureTime = new Date(eventTimeMs - 65 * 60 * 1000);
// Total travel time: 30 min bike route + 5 min buffer = 35 min
const expectedDepartureTime = new Date(eventTimeMs - 35 * 60 * 1000);
expect(result.current.departureTime?.getTime()).toBeCloseTo(
expectedDepartureTime.getTime()
);
@@ -0,0 +1,75 @@
import { renderHook, waitFor } from '@testing-library/react';
import { useWalkRoute } from '../useWalkRoute';
import { vi } from 'vitest';
describe('useWalkRoute integration', () => {
it('should handle missing coordinates gracefully', () => {
const { result } = renderHook(() =>
useWalkRoute(undefined, undefined, undefined, undefined)
);
expect(result.current.loading).toBe(false);
expect(result.current.walkRoute).toBe(null);
expect(result.current.error).toBe(null);
});
it('should handle API errors', async () => {
// Mock a failed API call
const originalFetch = global.fetch;
global.fetch = vi.fn(() =>
Promise.resolve({
ok: false,
status: 500,
statusText: 'Internal Server Error'
}) as unknown as Response);
const { result } = renderHook(() =>
useWalkRoute(48.2082, 16.3738, 48.2092, 16.3748)
);
await waitFor(() => expect(result.current.loading).toBe(false));
expect(result.current.error).toBeDefined();
expect(result.current.walkRoute).toBe(null);
// Restore original fetch
global.fetch = originalFetch;
});
it('should work with valid coordinates', async () => {
// Mock a successful API call
const mockRoute = {
distance: 500,
duration: 300,
steps: [
{
name: 'Start walking',
distance: 500,
duration: 300,
instruction: 'Walk straight ahead'
}
]
};
const originalFetch = global.fetch;
global.fetch = vi.fn(() =>
Promise.resolve({
ok: true,
json: () => Promise.resolve(mockRoute)
}) as unknown as Response);
const { result } = renderHook(() =>
useWalkRoute(48.2082, 16.3738, 48.2092, 16.3748)
);
await waitFor(() => expect(result.current.loading).toBe(true));
expect(result.current.walkRoute).toBe(null);
await waitFor(() => expect(result.current.loading).toBe(false));
expect(result.current.walkRoute).toEqual(mockRoute);
expect(result.current.error).toBe(null);
// Restore original fetch
global.fetch = originalFetch;
});
});
+7 -24
View File
@@ -30,46 +30,29 @@ export function useDepartureTime(
let arrivalTime: Date | null = null;
let mode: "train" | "bike" | null = null;
// Handle train mode
if (activeMode === "train" && validJourneys.length > 0) {
// Find journeys that arrive before or at the target arrival time
const onTimeJourneys = validJourneys.filter(journey =>
journey.sA.getTime() <= targetArrivalTime.getTime()
journey.rA.getTime() <= targetArrivalTime.getTime()
);
if (onTimeJourneys.length > 0) {
// Pick the journey with the latest departure time
const bestJourney = onTimeJourneys.reduce((latest, current) =>
current.sD.getTime() > latest.sD.getTime() ? current : latest
current.rD.getTime() > latest.rD.getTime() ? current : latest
);
departureTime = new Date(bestJourney.sD);
arrivalTime = new Date(bestJourney.sA);
departureTime = new Date(bestJourney.rD);
arrivalTime = new Date(bestJourney.rA);
mode = "train";
}
}
// Handle bike mode
if (activeMode === "bike" && bikeRoute !== null && bikeRoute > 0) {
// Convert bike route duration from seconds to milliseconds
const bikeDurationMs = bikeRoute * 1000;
// Calculate arrival time by subtracting bike duration from event time
const calculatedArrivalTime = new Date(eventTime);
calculatedArrivalTime.setTime(calculatedArrivalTime.getTime() - bikeDurationMs);
// Calculate departure time by subtracting bike duration from calculated arrival time
const calculatedDepartureTime = new Date(calculatedArrivalTime);
calculatedDepartureTime.setTime(calculatedArrivalTime.getTime() - bikeDurationMs);
// Calculate how much earlier we need to account for arrival buffer
const totalBufferMs = arrivalBufferMinutes * 60 * 1000;
const totalTravelTimeMs = bikeDurationMs * 2 + totalBufferMs;
const targetArrivalMs = eventTime.getTime() - totalBufferMs;
departureTime = new Date(eventTime);
departureTime.setTime(eventTime.getTime() - totalTravelTimeMs);
arrivalTime = new Date(calculatedArrivalTime);
departureTime = new Date(targetArrivalMs - bikeDurationMs);
arrivalTime = new Date(targetArrivalMs);
mode = "bike";
}
+2 -1
View File
@@ -1,6 +1,7 @@
import type {
GeocodeResult,
BikeRoute,
WalkRoute,
CalendarEvent,
Journey,
Station,
@@ -44,7 +45,7 @@ export class ApiClient {
fromLng: number,
toLat: number,
toLng: number,
): Promise<BikeRoute> {
): Promise<WalkRoute> {
const url = buildUrl(this.baseUrl, "/api/bike-route", {
fromLat: String(fromLat),
fromLng: String(fromLng),