better transit times

This commit is contained in:
Andras Schmelczer 2026-02-22 11:13:39 +00:00
parent 974f005549
commit 205302dbb8
22 changed files with 247 additions and 69 deletions

View file

@ -9,10 +9,18 @@ import { groupFeaturesByCategory } from '../../lib/features';
import { FeatureInfoPopup } from '../ui/FeatureInfoPopup';
import { FeatureActions } from '../ui/FeatureIcons';
import { FeatureLabel } from '../ui/FeatureLabel';
import { RouteIcon, PlusIcon, EyeIcon } from '../ui/icons';
import { CarIcon, BicycleIcon, WalkingIcon, TransitIcon, PlusIcon, EyeIcon } from '../ui/icons';
import type { ComponentType } from 'react';
import { IconButton } from '../ui/IconButton';
import { TRANSPORT_MODES, MODE_LABELS, travelFieldKey, type TransportMode, type TravelTimeEntry } from '../../hooks/useTravelTime';
const MODE_ICONS: Record<TransportMode, ComponentType<{ className?: string }>> = {
car: CarIcon,
bicycle: BicycleIcon,
walking: WalkingIcon,
transit: TransitIcon,
};
interface FeatureBrowserProps {
availableFeatures: FeatureMeta[];
allFeatures: FeatureMeta[];
@ -77,7 +85,7 @@ export default function FeatureBrowser({
name="Travel Time"
expanded={isSearching || expandedGroups.has('Travel Time')}
onToggle={() => toggleGroup('Travel Time')}
className="px-3 py-1.5 text-xs font-bold text-warm-500 bg-warm-50 dark:bg-navy-950 dark:text-warm-400 sticky top-0 hover:bg-warm-100 dark:hover:bg-warm-800"
className="px-3 py-1.5 text-xs font-bold text-navy-950 bg-warm-200 dark:bg-navy-900 dark:text-warm-100 sticky top-0 hover:bg-warm-200 dark:hover:bg-warm-800"
>
<span className="text-[10px] font-medium text-warm-400 dark:text-warm-500">
{TRANSPORT_MODES.length}
@ -87,13 +95,14 @@ export default function FeatureBrowser({
const activeEntry = travelTimeEntries.find((e) => e.mode === mode && e.slug);
const fieldKey = activeEntry ? travelFieldKey(activeEntry) : null;
const isPinned = fieldKey !== null && pinnedFeature === fieldKey;
const ModeIcon = MODE_ICONS[mode];
return (
<div
key={mode}
className="flex items-start justify-between px-3 py-1.5 hover:bg-teal-50 dark:hover:bg-teal-900/30 cursor-pointer"
>
<div className="flex items-center gap-2 min-w-0" onClick={() => onAddTravelTimeEntry(mode)}>
<RouteIcon className="w-4 h-4 text-teal-600 dark:text-teal-400 shrink-0" />
<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]}
@ -131,7 +140,7 @@ export default function FeatureBrowser({
name={group.name}
expanded={isExpanded}
onToggle={() => toggleGroup(group.name)}
className="px-3 py-1.5 text-xs font-bold text-warm-500 bg-warm-50 dark:bg-navy-950 dark:text-warm-400 sticky top-0 hover:bg-warm-100 dark:hover:bg-warm-800"
className="px-3 py-1.5 text-xs font-bold text-navy-950 bg-warm-200 dark:bg-navy-900 dark:text-warm-100 sticky top-0 hover:bg-warm-200 dark:hover:bg-warm-800"
>
<span className="text-[10px] font-medium text-warm-400 dark:text-warm-500">
{group.features.length}

View file

@ -254,7 +254,7 @@ export default memo(function Filters({
name="Travel Time"
expanded={!collapsedGroups.has('Travel Time')}
onToggle={() => toggleGroup('Travel Time')}
className="px-3 py-1.5 text-xs font-bold text-warm-500 bg-warm-50 dark:bg-navy-950 dark:text-warm-400 sticky top-0 hover:bg-warm-100 dark:hover:bg-warm-800"
className="px-3 py-1.5 text-xs font-bold text-navy-950 bg-warm-200 dark:bg-navy-900 dark:text-warm-100 sticky top-0 hover:bg-warm-200 dark:hover:bg-warm-800"
>
<span className="text-[10px] font-medium text-warm-400 dark:text-warm-500">
{travelTimeEntries.length}
@ -296,7 +296,7 @@ export default memo(function Filters({
name={group.name}
expanded={isExpanded}
onToggle={() => toggleGroup(group.name)}
className="px-3 py-1.5 text-xs font-bold text-warm-500 bg-warm-50 dark:bg-navy-950 dark:text-warm-400 sticky top-0 hover:bg-warm-100 dark:hover:bg-warm-800"
className="px-3 py-1.5 text-xs font-bold text-navy-950 bg-warm-200 dark:bg-navy-900 dark:text-warm-100 sticky top-0 hover:bg-warm-200 dark:hover:bg-warm-800"
>
<span className="text-[10px] font-medium text-warm-400 dark:text-warm-500">
{group.features.length}

View file

@ -5,10 +5,21 @@ import { PlaceSearchInput } from '../ui/PlaceSearchInput';
import { CloseIcon } from '../ui/icons/CloseIcon';
import { EyeIcon } from '../ui/icons/EyeIcon';
import { MapPinIcon } from '../ui/icons/MapPinIcon';
import { RouteIcon } from '../ui/icons/RouteIcon';
import { CarIcon } from '../ui/icons/CarIcon';
import { BicycleIcon } from '../ui/icons/BicycleIcon';
import { WalkingIcon } from '../ui/icons/WalkingIcon';
import { TransitIcon } from '../ui/icons/TransitIcon';
import { formatFilterValue } from '../../lib/format';
import { useLocationSearch, type SearchResult } from '../../hooks/useLocationSearch';
import { MODE_LABELS, type TransportMode } from '../../hooks/useTravelTime';
import type { ComponentType } from 'react';
const MODE_ICONS: Record<TransportMode, ComponentType<{ className?: string }>> = {
car: CarIcon,
bicycle: BicycleIcon,
walking: WalkingIcon,
transit: TransitIcon,
};
interface TravelTimeCardProps {
mode: TransportMode;
@ -63,12 +74,14 @@ export function TravelTimeCard({
const sliderMax = dataRange ? Math.ceil(dataRange[1]) : 120;
const displayRange = timeRange ?? [sliderMin, sliderMax];
const ModeIcon = MODE_ICONS[mode];
return (
<div className={`space-y-2 px-2 py-2 rounded ${isPinned ? 'ring-2 ring-teal-400 bg-teal-50/50 dark:bg-teal-900/20' : ''}`}>
{/* Header */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-1.5">
<RouteIcon className="w-4 h-4 text-teal-600 dark:text-teal-400" />
<ModeIcon className="w-4 h-4 text-teal-600 dark:text-teal-400" />
<span className="text-sm font-medium text-navy-950 dark:text-warm-100">
Travel Time ({MODE_LABELS[mode]})
</span>

View file

@ -0,0 +1,23 @@
interface IconProps {
className?: string;
}
export function BicycleIcon({ className = 'w-4 h-4' }: IconProps) {
return (
<svg
className={className}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
>
<circle cx="6" cy="17" r="3" />
<circle cx="18" cy="17" r="3" />
<path d="M6 17l3-7h4l3 7" />
<path d="M9 10l3 4h3" />
<circle cx="12" cy="7" r="1.5" />
</svg>
);
}

View file

@ -0,0 +1,22 @@
interface IconProps {
className?: string;
}
export function CarIcon({ className = 'w-4 h-4' }: IconProps) {
return (
<svg
className={className}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M5 17h14v-5l-2-6H7L5 12v5z" />
<circle cx="7.5" cy="17" r="2" />
<circle cx="16.5" cy="17" r="2" />
<path d="M5 12h14" />
</svg>
);
}

View file

@ -0,0 +1,25 @@
interface IconProps {
className?: string;
}
export function TransitIcon({ className = 'w-4 h-4' }: IconProps) {
return (
<svg
className={className}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
>
<rect x="6" y="3" width="12" height="14" rx="3" />
<path d="M6 12h12" />
<circle cx="9" cy="15" r="1" />
<circle cx="15" cy="15" r="1" />
<path d="M9 20l-2 2" />
<path d="M15 20l2 2" />
<path d="M9 3V1h6v2" />
</svg>
);
}

View file

@ -0,0 +1,23 @@
interface IconProps {
className?: string;
}
export function WalkingIcon({ className = 'w-4 h-4' }: IconProps) {
return (
<svg
className={className}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
>
<circle cx="12" cy="4.5" r="2" />
<path d="M13.5 9L15 15l-3 4" />
<path d="M10.5 9L9 15l3 4" />
<path d="M10 9h4l2 4" />
<path d="M8 13l2-4" />
</svg>
);
}

View file

@ -7,3 +7,7 @@ export { FilterIcon } from './FilterIcon';
export { LightbulbIcon } from './LightbulbIcon';
export { MenuIcon } from './MenuIcon';
export { RouteIcon } from './RouteIcon';
export { CarIcon } from './CarIcon';
export { BicycleIcon } from './BicycleIcon';
export { WalkingIcon } from './WalkingIcon';
export { TransitIcon } from './TransitIcon';

View file

@ -84,5 +84,5 @@ export function buildFilterString(filters: FeatureFilters, features: FeatureMeta
const maxStr = meta?.absolute && max === meta.max ? 'inf' : String(max);
return `${name}:${min}:${maxStr}`;
})
.join(',');
.join(';;');
}