import { useState, useMemo, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; import { useCollapsibleGroups } from '../../hooks/useCollapsibleGroups'; import { useTravelModes } from '../../hooks/useTravelModes'; import { SearchInput } from '../ui/SearchInput'; import { FilterIcon } from '../ui/icons'; import { CollapsibleGroupHeader } from '../ui/CollapsibleGroupHeader'; import { EmptyState } from '../ui/EmptyState'; import type { FeatureMeta } from '../../types'; import { groupFeaturesByCategory } from '../../lib/features'; import { FeatureInfoPopup } from '../ui/FeatureInfoPopup'; import { FeatureActions } from '../ui/FeatureIcons'; import { FeatureLabel } from '../ui/FeatureLabel'; import { PlusIcon, InfoIcon } from '../ui/icons'; import { IconButton } from '../ui/IconButton'; import { TravelTimeInfoPopup } from '../ui/TravelTimeInfoPopup'; import { TRANSPORT_MODES, MODE_ICONS, useTranslatedModes, type TransportMode, type TravelTimeEntry, } from '../../hooks/useTravelTime'; interface FeatureBrowserProps { availableFeatures: FeatureMeta[]; allFeatures: FeatureMeta[]; pinnedFeature: string | null; onAddFilter: (name: string) => void; onTogglePin: (name: string) => void; onNavigateToSource?: (slug: string, featureName: string) => void; openInfoFeature?: string | null; onClearOpenInfoFeature?: () => void; travelTimeEntries: TravelTimeEntry[]; onAddTravelTimeEntry: (mode: TransportMode) => void; } export default function FeatureBrowser({ availableFeatures, allFeatures, pinnedFeature, onAddFilter, onTogglePin, onNavigateToSource, openInfoFeature, onClearOpenInfoFeature, travelTimeEntries: _travelTimeEntries, onAddTravelTimeEntry, }: FeatureBrowserProps) { const { t } = useTranslation(); const modes = useTranslatedModes(); const [search, setSearch] = useState(''); const [infoFeature, setInfoFeature] = useState(null); const [travelInfoMode, setTravelInfoMode] = useState(null); const [isGroupExpanded, toggleGroup] = useCollapsibleGroups(true); const availableTravelModes = useTravelModes(); useEffect(() => { if (openInfoFeature) { const feat = allFeatures.find((f) => f.name === openInfoFeature); if (feat) setInfoFeature(feat); onClearOpenInfoFeature?.(); } }, [openInfoFeature, allFeatures, onClearOpenInfoFeature]); const filtered = useMemo(() => { if (!search) return availableFeatures; const lower = search.toLowerCase(); return availableFeatures.filter((f) => [f.name, f.description, f.detail, f.group] .filter((value): value is string => Boolean(value)) .some((value) => value.toLowerCase().includes(lower)) ); }, [availableFeatures, search]); const grouped = useMemo(() => groupFeaturesByCategory(filtered), [filtered]); // When searching, expand all groups so results are visible const isSearching = search.length > 0; // Only show modes that have precomputed travel time data const visibleModes = useMemo( () => (availableTravelModes ? TRANSPORT_MODES.filter((m) => availableTravelModes.has(m)) : []), [availableTravelModes] ); const showTravelModes = visibleModes.length > 0 && (!search || 'travel time journey commute car bicycle walking transit transport station tube train'.includes( search.toLowerCase() )); // Ensure "Transport" group exists first when travel modes should be shown. const mergedGrouped = useMemo(() => { if (!showTravelModes) return grouped; if (grouped.some((g) => g.name === 'Transport')) return grouped; return [{ name: 'Transport', features: [] }, ...grouped]; }, [grouped, showTravelModes]); return ( <>
{!search && (

{t('filters.chooseFilters')}

)}
{mergedGrouped.map((group) => { const isExpanded = isSearching || isGroupExpanded(group.name); return (
toggleGroup(group.name)} className="px-3 py-2.5 text-sm font-bold text-navy-950 bg-warm-200 dark:bg-navy-900 dark:text-warm-100 sticky top-0 z-10 hover:bg-warm-200 dark:hover:bg-warm-800" > {group.features.length + (group.name === 'Transport' && showTravelModes ? visibleModes.length : 0)} {isExpanded && ( <> {group.name === 'Transport' && showTravelModes && visibleModes.map((mode) => { const ModeIcon = MODE_ICONS[mode]; return (
onAddTravelTimeEntry(mode)} >
{modes.label(mode)} {modes.desc(mode)}
setTravelInfoMode(mode)} title={t('filters.aboutData')} size="md" >
); })} {group.features.map((f) => { const isPinned = pinnedFeature === f.name; return (
); })} )}
); })} {mergedGrouped.length === 0 ? ( } title={search ? t('filters.noMatchingFeatures') : t('filters.allFeaturesActive')} description={search ? t('filters.tryDifferentSearch') : t('filters.removeFilterHint')} className="px-3 py-4" /> ) : null}
{infoFeature && ( setInfoFeature(null)} onNavigateToSource={onNavigateToSource} /> )} {travelInfoMode && ( setTravelInfoMode(null)} /> )} ); }