This commit is contained in:
Andras Schmelczer 2026-04-04 17:44:44 +01:00
parent b94cf17d75
commit 0c6d207967
41 changed files with 1809 additions and 1204 deletions

View file

@ -1,4 +1,5 @@
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';
@ -15,9 +16,8 @@ import { IconButton } from '../ui/IconButton';
import { TravelTimeInfoPopup } from '../ui/TravelTimeInfoPopup';
import {
TRANSPORT_MODES,
MODE_LABELS,
MODE_DESCRIPTIONS,
MODE_ICONS,
useTranslatedModes,
type TransportMode,
type TravelTimeEntry,
} from '../../hooks/useTravelTime';
@ -34,7 +34,6 @@ interface FeatureBrowserProps {
travelTimeEntries: TravelTimeEntry[];
onAddTravelTimeEntry: (mode: TransportMode) => void;
isLicensed: boolean;
onUpgradeClick?: () => void;
}
export default function FeatureBrowser({
@ -49,8 +48,9 @@ export default function FeatureBrowser({
travelTimeEntries: _travelTimeEntries,
onAddTravelTimeEntry,
isLicensed,
onUpgradeClick,
}: FeatureBrowserProps) {
const { t } = useTranslation();
const modes = useTranslatedModes();
const [search, setSearch] = useState('');
const [infoFeature, setInfoFeature] = useState<FeatureMeta | null>(null);
const [travelInfoMode, setTravelInfoMode] = useState<TransportMode | null>(null);
@ -102,9 +102,13 @@ export default function FeatureBrowser({
return (
<>
<div className="shrink-0 p-2 border-b border-warm-200 dark:border-navy-700">
<SearchInput value={search} onChange={setSearch} placeholder="Search features..." />
<SearchInput
value={search}
onChange={setSearch}
placeholder={t('filters.searchFeatures')}
/>
</div>
<div className="md:min-h-0 md:flex-1 md:overflow-y-auto flex flex-col">
<div>
{mergedGrouped.map((group) => {
const isExpanded = isSearching || isGroupExpanded(group.name);
return (
@ -158,24 +162,24 @@ export default function FeatureBrowser({
<ModeIcon className="w-4 h-4 text-teal-600 dark:text-teal-400 shrink-0" />
<div className="min-w-0">
<span className="text-sm font-medium text-navy-950 dark:text-warm-100">
{MODE_LABELS[mode]}
{modes.label(mode)}
</span>
<span className="text-xs text-warm-400 dark:text-warm-500 block">
{MODE_DESCRIPTIONS[mode]}
{modes.desc(mode)}
</span>
</div>
</div>
<div className="flex items-center gap-0.5 shrink-0">
<IconButton
onClick={() => setTravelInfoMode(mode)}
title="Feature info"
title={t('filters.featureInfo')}
size="md"
>
<InfoIcon className="w-5 h-5 md:w-3.5 md:h-3.5" />
</IconButton>
<button
onClick={() => onAddTravelTimeEntry(mode)}
title={`Add ${MODE_LABELS[mode]} travel time`}
title={t('travel.addTravelTime', { mode: modes.label(mode) })}
className="p-1 rounded-md text-teal-600 dark:text-teal-400 bg-teal-50 dark:bg-teal-900/30 hover:bg-teal-100 dark:hover:bg-teal-800/40"
>
<PlusIcon className="w-5 h-5 md:w-5 md:h-5" strokeWidth={2.5} />
@ -192,45 +196,15 @@ export default function FeatureBrowser({
{mergedGrouped.length === 0 ? (
<EmptyState
icon={<FilterIcon className="w-8 h-8 text-warm-300 dark:text-warm-600" />}
title={search ? 'No matching features' : 'All features are active'}
description={
search ? 'Try a different search term' : 'Remove a filter to see available features'
}
title={search ? t('filters.noMatchingFeatures') : t('filters.allFeaturesActive')}
description={search ? t('filters.tryDifferentSearch') : t('filters.removeFilterHint')}
className="px-3 py-4"
/>
) : isLicensed ? (
<p className="flex-1 flex items-center justify-center px-4 py-6 text-xs text-center text-warm-400 dark:text-warm-500">
Choose the filters that matter to you. The map updates as you go.
{t('filters.chooseFilters')}
</p>
) : (
<div className="mt-auto flex flex-col items-center px-5 pt-6 pb-0">
<p className="text-sm text-warm-600 dark:text-warm-400 text-center leading-relaxed mb-1">
See crime, schools, noise, broadband, and 50+ more filters across all of England.
</p>
<p className="text-xs text-warm-400 dark:text-warm-500 text-center mb-4">
One-time payment, lifetime access.
</p>
<button
onClick={onUpgradeClick}
className="px-5 py-2.5 rounded-lg bg-gradient-to-r from-teal-500 to-teal-600 hover:from-teal-600 hover:to-teal-700 text-white font-medium text-sm shadow-sm hover:shadow-md"
>
Upgrade to full map
</button>
<svg
viewBox="0 120 1600 230"
className="w-full mt-4 block shrink-0"
preserveAspectRatio="xMidYMax meet"
>
<path
d="M0,350 C400,150 1200,150 1600,350 Z"
className="fill-green-500 dark:fill-green-600"
/>
<path d="M100,350 C450,180 1150,180 1500,350 Z" fill="#000" opacity="0.08" />
<path d="M250,350 C550,220 1050,220 1350,350 Z" fill="#000" opacity="0.06" />
<image href="/house.png" x="735" y="110" width="130" height="120" />
</svg>
</div>
)}
) : null}
</div>
{infoFeature && (
<FeatureInfoPopup