UI improvements

This commit is contained in:
Andras Schmelczer 2026-03-15 09:02:10 +00:00
parent 83dd2ca87e
commit 1569d116a9
14 changed files with 222 additions and 92 deletions

View file

@ -25,7 +25,7 @@ export default memo(function AiFilterInput({ loading, error, notes, onSubmit }:
const hasContent = query.trim().length > 0;
return (
<div className="px-3 py-2">
<div className="px-3 py-2" data-tutorial="ai-filters">
<form onSubmit={handleSubmit} className="flex items-center gap-1.5">
<div className="relative flex-1">
<SparklesIcon className="absolute left-2 top-1/2 -translate-y-1/2 w-3.5 h-3.5 text-teal-500 dark:text-teal-400 pointer-events-none" />

View file

@ -1,5 +1,6 @@
import { useState, useMemo, useEffect } from 'react';
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';
@ -11,7 +12,6 @@ import { FeatureActions } from '../ui/FeatureIcons';
import { FeatureLabel } from '../ui/FeatureLabel';
import { CarIcon, BicycleIcon, WalkingIcon, TransitIcon, PlusIcon } from '../ui/icons';
import type { ComponentType } from 'react';
import { IconButton } from '../ui/IconButton';
import { TRANSPORT_MODES, MODE_LABELS, MODE_DESCRIPTIONS, type TransportMode, type TravelTimeEntry } from '../../hooks/useTravelTime';
const MODE_ICONS: Record<TransportMode, ComponentType<{ className?: string }>> = {
@ -53,6 +53,7 @@ export default function FeatureBrowser({
const [search, setSearch] = useState('');
const [infoFeature, setInfoFeature] = useState<FeatureMeta | null>(null);
const [expandedGroups, toggleGroup] = useCollapsibleGroups();
const availableTravelModes = useTravelModes();
useEffect(() => {
if (openInfoFeature) {
@ -73,9 +74,15 @@ export default function FeatureBrowser({
// When searching, expand all groups so results are visible
const isSearching = search.length > 0;
// All modes are always available (can add multiple entries per mode)
// Only show modes that have precomputed travel time data
const visibleModes = useMemo(
() => (availableTravelModes ? TRANSPORT_MODES.filter((m) => availableTravelModes.has(m)) : []),
[availableTravelModes],
);
const showTravelModes =
!search || 'travel time journey commute car bicycle walking transit'.includes(search.toLowerCase());
visibleModes.length > 0 &&
(!search || 'travel time journey commute car bicycle walking transit'.includes(search.toLowerCase()));
return (
<>
@ -92,15 +99,15 @@ export default function FeatureBrowser({
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"
>
<span className="text-xs font-medium text-warm-400 dark:text-warm-500">
{TRANSPORT_MODES.length}
{visibleModes.length}
</span>
</CollapsibleGroupHeader>
{(isSearching || expandedGroups.has('Travel Time')) && TRANSPORT_MODES.map((mode) => {
{(isSearching || expandedGroups.has('Travel Time')) && visibleModes.map((mode) => {
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"
className="flex items-center 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)}>
<ModeIcon className="w-4 h-4 text-teal-600 dark:text-teal-400 shrink-0" />
@ -114,9 +121,13 @@ export default function FeatureBrowser({
</div>
</div>
<div className="flex items-center gap-0.5 shrink-0">
<IconButton onClick={() => onAddTravelTimeEntry(mode)} title={`Add ${MODE_LABELS[mode]} travel time`} size="md">
<PlusIcon className="w-7 h-7 md:w-3.5 md:h-3.5" />
</IconButton>
<button
onClick={() => onAddTravelTimeEntry(mode)}
title={`Add ${MODE_LABELS[mode]} travel time`}
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-7 h-7 md:w-5 md:h-5" strokeWidth={2.5} />
</button>
</div>
</div>
);
@ -143,7 +154,7 @@ export default function FeatureBrowser({
return (
<div
key={f.name}
className="flex items-start justify-between px-3 py-1.5 hover:bg-teal-50 dark:hover:bg-teal-900/30 dark:text-warm-300"
className="flex items-center justify-between px-3 py-1.5 hover:bg-teal-50 dark:hover:bg-teal-900/30 dark:text-warm-300"
>
<div className="min-w-0 mr-2">
<FeatureLabel feature={f} onShowInfo={setInfoFeature} size="sm" />

View file

@ -212,9 +212,6 @@ export default function JourneyInstructions({ postcode, entries, label }: Journe
const displayLegs = j.legs ? invertLegs(j.legs) : null;
const legSum = j.legs ? j.legs.reduce((sum, l) => sum + l.minutes, 0) : 0;
const totalMin = j.minutes ?? legSum;
const waitingMin = j.minutes != null ? Math.max(0, j.minutes - legSum) : null;
const bestWaitingMin =
j.bestMinutes != null ? Math.max(0, j.bestMinutes - legSum) : null;
return (
<div key={j.slug} className="bg-warm-50 dark:bg-warm-800 rounded-lg p-2.5">
@ -238,22 +235,6 @@ export default function JourneyInstructions({ postcode, entries, label }: Journe
{displayLegs.map((leg, i) => (
<TimelineLeg key={i} leg={leg} isLast={i === displayLegs.length - 1} />
))}
{waitingMin != null && waitingMin > 0 && (
<div className="mt-1.5 pt-1.5 border-t border-warm-200 dark:border-warm-700 flex items-baseline justify-between">
<span className="text-[11px] text-warm-500 dark:text-warm-400">
Waiting & transfers
</span>
<span className="text-[11px] text-warm-600 dark:text-warm-300">
{waitingMin} min
{bestWaitingMin != null && (
<span className="text-warm-400 dark:text-warm-500">
{' '}
(best: {bestWaitingMin === 0 ? '~0' : bestWaitingMin} min)
</span>
)}
</span>
</div>
)}
</div>
) : (
<span className="text-xs text-warm-500 dark:text-warm-400">

View file

@ -33,6 +33,7 @@ import { useLicense } from '../../hooks/useLicense';
import UpgradeModal from '../ui/UpgradeModal';
import { SpinnerIcon } from '../ui/icons/SpinnerIcon';
import { MapPinIcon } from '../ui/icons/MapPinIcon';
import { BookmarkIcon } from '../ui/icons/BookmarkIcon';
export interface ExportState {
onExport: () => void;
@ -101,6 +102,22 @@ export default function MapPage({
const [mobileDrawerOpen, setMobileDrawerOpen] = useState(false);
const [poiPaneOpen, setPoiPaneOpen] = useState(false);
const [showBookmarkToast, setShowBookmarkToast] = useState(false);
const bookmarkToastDismissed = useRef(
localStorage.getItem('bookmark_toast_dismissed') === '1'
);
const handleSavePropertyWithToast = useCallback(
(property: Property) => {
onSaveProperty?.(property);
if (!bookmarkToastDismissed.current) {
setShowBookmarkToast(true);
bookmarkToastDismissed.current = true;
}
},
[onSaveProperty]
);
const {
filters,
activeFeature,
@ -358,6 +375,31 @@ export default function MapPage({
}
}, [screenshotMode, mapData.loading, mapData.data.length, mapData.postcodeData.length, mapData.usePostcodeView]);
const bookmarkToast = showBookmarkToast && (
<div className="fixed bottom-6 left-1/2 -translate-x-1/2 z-[60] flex items-center gap-3 px-4 py-3 rounded-lg bg-navy-900 text-white text-sm shadow-lg animate-fade-in">
<BookmarkIcon className="w-4 h-4 text-teal-400 shrink-0" filled />
<span>Property saved!</span>
<button
onClick={() => {
setShowBookmarkToast(false);
onNavigateTo('saved', 'properties');
}}
className="px-3 py-1 rounded bg-teal-600 hover:bg-teal-500 text-white text-xs font-medium whitespace-nowrap"
>
View saved
</button>
<button
onClick={() => {
setShowBookmarkToast(false);
localStorage.setItem('bookmark_toast_dismissed', '1');
}}
className="text-warm-400 hover:text-warm-200 text-xs whitespace-nowrap"
>
Don&apos;t show again
</button>
</div>
);
if (screenshotMode) {
return (
<div className="h-full w-full">
@ -416,7 +458,7 @@ export default function MapPage({
loading={selection.loadingProperties}
hexagonId={selection.selectedHexagon?.id || null}
onLoadMore={selection.handleLoadMoreProperties}
onSaveProperty={onSaveProperty}
onSaveProperty={onSaveProperty ? handleSavePropertyWithToast : undefined}
onUnsaveProperty={onUnsaveProperty}
isPropertySaved={isPropertySaved}
getSavedPropertyId={getSavedPropertyId}
@ -592,6 +634,8 @@ export default function MapPage({
/>
)}
{bookmarkToast}
{mapData.licenseRequired && (
<UpgradeModal
isLoggedIn={!!user}
@ -730,6 +774,8 @@ export default function MapPage({
</div>
)}
{bookmarkToast}
{mapData.licenseRequired && (
<UpgradeModal
isLoggedIn={!!user}

View file

@ -76,11 +76,15 @@ export function TravelTimeCard({
<span className="text-sm font-medium text-navy-950 dark:text-warm-100">
Travel Time ({MODE_LABELS[mode]})
</span>
<button
onClick={() => setShowInfo(true)}
className="p-1 -m-0.5 rounded text-warm-400 hover:text-warm-700 dark:hover:text-warm-300 hover:bg-warm-100 dark:hover:bg-warm-700 shrink-0"
title="Feature info"
>
<InfoIcon className="w-3.5 h-3.5" />
</button>
</div>
<div className="flex items-center gap-0.5">
<IconButton onClick={() => setShowInfo(true)} title="Feature info">
<InfoIcon className="w-3.5 h-3.5" />
</IconButton>
{slug && (
<IconButton onClick={onTogglePin} active={isPinned} title={isPinned ? 'Stop previewing' : 'Preview on map'}>
<EyeIcon className="w-3.5 h-3.5" filled={isPinned} />