lmao
This commit is contained in:
parent
03445188ea
commit
524580eb25
102 changed files with 36625 additions and 1295 deletions
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue