better transit times
This commit is contained in:
parent
974f005549
commit
205302dbb8
22 changed files with 247 additions and 69 deletions
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
23
frontend/src/components/ui/icons/BicycleIcon.tsx
Normal file
23
frontend/src/components/ui/icons/BicycleIcon.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
22
frontend/src/components/ui/icons/CarIcon.tsx
Normal file
22
frontend/src/components/ui/icons/CarIcon.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
25
frontend/src/components/ui/icons/TransitIcon.tsx
Normal file
25
frontend/src/components/ui/icons/TransitIcon.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
23
frontend/src/components/ui/icons/WalkingIcon.tsx
Normal file
23
frontend/src/components/ui/icons/WalkingIcon.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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(';;');
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue