This commit is contained in:
Andras Schmelczer 2026-02-15 22:39:49 +00:00
parent 03445188ea
commit 524580eb25
102 changed files with 36625 additions and 1295 deletions

View file

@ -1,4 +1,4 @@
import { useState, useCallback, useRef, useEffect } from 'react';
import { useRef, useEffect, useCallback } from 'react';
import { Slider } from '../ui/Slider';
import { IconButton } from '../ui/IconButton';
import { PlaceSearchInput } from '../ui/PlaceSearchInput';
@ -6,34 +6,31 @@ import { CloseIcon } from '../ui/icons/CloseIcon';
import { MapPinIcon } from '../ui/icons/MapPinIcon';
import { RouteIcon } from '../ui/icons/RouteIcon';
import { formatFilterValue } from '../../lib/format';
import { authHeaders, logNonAbortError } from '../../lib/api';
import { useLocationSearch, type SearchResult } from '../../hooks/useLocationSearch';
import { MODE_LABELS, type TransportMode } from '../../hooks/useTravelTime';
interface TravelTimeCardProps {
mode: TransportMode;
destination: [number, number] | null;
destinationLabel: string;
slug: string;
label: string;
timeRange: [number, number] | null;
dataRange: [number, number] | null;
onSetDestination: (lat: number, lon: number, label: string) => void;
onSetDestination: (slug: string, label: string) => void;
onTimeRangeChange: (range: [number, number]) => void;
onRemove: () => void;
}
export function TravelTimeCard({
mode,
destination,
destinationLabel,
slug,
label,
timeRange,
dataRange,
onSetDestination,
onTimeRangeChange,
onRemove,
}: TravelTimeCardProps) {
const search = useLocationSearch();
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const search = useLocationSearch(mode);
const containerRef = useRef<HTMLDivElement>(null);
// Close dropdown on outside click
@ -45,42 +42,16 @@ export function TravelTimeCard({
};
document.addEventListener('mousedown', handler);
return () => document.removeEventListener('mousedown', handler);
}, [search]);
}, [search.close]);
const selectResult = useCallback(
async (result: SearchResult) => {
(result: SearchResult) => {
if (result.type === 'place') {
onSetDestination(result.lat, result.lon, result.name);
onSetDestination(result.slug, result.name);
search.clear();
setError(null);
return;
}
// Postcode — fetch coordinates
setError(null);
setLoading(true);
search.close();
try {
const res = await fetch(
`/api/postcode/${encodeURIComponent(result.label)}`,
authHeaders(),
);
if (!res.ok) {
setError('Postcode not found');
return;
}
const json: { postcode: string; latitude: number; longitude: number } =
await res.json();
onSetDestination(json.latitude, json.longitude, json.postcode);
search.clear();
} catch (err) {
logNonAbortError('Postcode lookup failed', err);
setError('Lookup failed');
} finally {
setLoading(false);
}
},
[onSetDestination, search],
[onSetDestination, search.clear],
);
const sliderMin = dataRange ? Math.floor(dataRange[0]) : 0;
@ -107,28 +78,23 @@ export function TravelTimeCard({
<PlaceSearchInput
search={search}
onSelect={selectResult}
loading={loading}
placeholder={destination ? 'Change destination...' : 'Search destination...'}
placeholder={slug ? 'Change destination...' : 'Search destination...'}
size="xs"
inputClassName="w-full px-2 py-1 text-xs rounded border border-warm-200 dark:border-warm-600 bg-white dark:bg-warm-800 text-navy-950 dark:text-warm-200 placeholder-warm-400 dark:placeholder-warm-500 outline-none focus:ring-1 focus:ring-teal-400"
onInputChange={() => setError(null)}
/>
{error && (
<p className="text-xs text-red-600 dark:text-red-400 mt-0.5">{error}</p>
)}
{destination && destinationLabel && (
{slug && label && (
<div className="flex items-center gap-1 mt-1">
<MapPinIcon className="w-3 h-3 text-red-500 shrink-0" />
<span className="text-xs text-warm-600 dark:text-warm-300">
{destinationLabel}
{label}
</span>
</div>
)}
</div>
{/* Time range slider — only show when we have data */}
{destination && dataRange && (
{slug && dataRange && (
<div>
<span className="text-[10px] font-medium text-warm-500 dark:text-warm-400 uppercase tracking-wide">
Max time