import { useState, useCallback, useMemo } from 'react'; import type { ComponentType } from 'react'; import { useTranslation } from 'react-i18next'; import { CarIcon, BicycleIcon, WalkingIcon, TransitIcon } from '../components/ui/icons'; import { dedupeTravelTimeEntries } from '../lib/travel-params'; export type TransportMode = 'car' | 'bicycle' | 'walking' | 'transit'; export const TRANSPORT_MODES: TransportMode[] = ['transit', 'car', 'bicycle', 'walking']; export const MODE_LABELS: Record = { car: 'Car', bicycle: 'Bicycle', walking: 'Walking', transit: 'Public Transport', }; export const MODE_DESCRIPTIONS: Record = { car: 'Drive time via the fastest road route', bicycle: 'Cycling time using bike-friendly routes', walking: 'Walking time along pedestrian paths and pavements', transit: 'Journey time by train, tube, and bus', }; export const MODE_ICONS: Record> = { car: CarIcon, bicycle: BicycleIcon, walking: WalkingIcon, transit: TransitIcon, }; /** * Hook returning translated mode labels and descriptions. */ export function useTranslatedModes() { const { t } = useTranslation(); const label = useCallback( (mode: TransportMode): string => ({ car: t('travel.modeCar'), bicycle: t('travel.modeBicycle'), walking: t('travel.modeWalking'), transit: t('travel.modeTransit'), })[mode], [t] ); const desc = useCallback( (mode: TransportMode): string => ({ car: t('travel.modeCarDesc'), bicycle: t('travel.modeBicycleDesc'), walking: t('travel.modeWalkingDesc'), transit: t('travel.modeTransitDesc'), })[mode], [t] ); return { label, desc }; } export interface TravelTimeEntry { mode: TransportMode; slug: string; label: string; timeRange: [number, number] | null; /** Use best-case (5th percentile) travel time instead of median. Transit only. */ useBest: boolean; } /** Field key matching the backend response: tt_{mode}_{slug} */ export function travelFieldKey(entry: TravelTimeEntry): string { return `tt_${entry.mode}_${entry.slug}`; } export interface TravelTimeInitial { entries?: TravelTimeEntry[]; } export function useTravelTime(initial?: TravelTimeInitial) { const [entries, setEntries] = useState(() => dedupeTravelTimeEntries(initial?.entries ?? []) ); const handleAddEntry = useCallback((mode: TransportMode) => { setEntries((prev) => [...prev, { mode, slug: '', label: '', timeRange: null, useBest: false }]); }, []); const handleRemoveEntry = useCallback((index: number) => { setEntries((prev) => prev.filter((_, i) => i !== index)); }, []); const handleSetDestination = useCallback((index: number, slug: string, label: string) => { setEntries((prev) => dedupeTravelTimeEntries( prev.map((entry, i) => i === index ? { ...entry, slug, label, timeRange: slug ? [0, 120] : null } : entry ) ) ); }, []); const handleTimeRangeChange = useCallback((index: number, range: [number, number]) => { setEntries((prev) => dedupeTravelTimeEntries( prev.map((entry, i) => (i === index ? { ...entry, timeRange: range } : entry)) ) ); }, []); const handleToggleBest = useCallback((index: number) => { setEntries((prev) => dedupeTravelTimeEntries( prev.map((entry, i) => (i === index ? { ...entry, useBest: !entry.useBest } : entry)) ) ); }, []); const handleSetEntries = useCallback((newEntries: TravelTimeEntry[]) => { setEntries(dedupeTravelTimeEntries(newEntries)); }, []); /** Entries that have a destination selected (slug is set) */ const activeEntries = useMemo(() => entries.filter((e) => e.slug !== ''), [entries]); return { entries, activeEntries, handleAddEntry, handleRemoveEntry, handleSetDestination, handleSetEntries, handleTimeRangeChange, handleToggleBest, }; }