good changes
This commit is contained in:
parent
160283f1a1
commit
c997ea46a5
26 changed files with 991 additions and 288 deletions
|
|
@ -69,6 +69,14 @@ const DATA_SOURCES = [
|
|||
url: 'https://download.geofabrik.de/europe/great-britain-latest.osm.pbf',
|
||||
license: 'Open Data Commons Open Database License (ODbL)',
|
||||
},
|
||||
{
|
||||
id: 'os-open-greenspace',
|
||||
name: 'OS Open Greenspace',
|
||||
origin: 'Ordnance Survey',
|
||||
use: 'Authoritative green space boundaries for Great Britain, including public parks, gardens, playing fields, and play spaces. Polygon centroids are used for park proximity counts and distance-to-nearest-park calculations.',
|
||||
url: 'https://osdatahub.os.uk/downloads/open/OpenGreenspace',
|
||||
license: 'Open Government Licence v3.0',
|
||||
},
|
||||
{
|
||||
id: 'naptan',
|
||||
name: 'NaPTAN (Public Transport Stops)',
|
||||
|
|
@ -101,14 +109,6 @@ const DATA_SOURCES = [
|
|||
url: 'https://www.ofcom.org.uk/phones-and-broadband/coverage-and-speeds/connected-nations-20252/data-downloads-2025',
|
||||
license: 'Open Government Licence v3.0',
|
||||
},
|
||||
{
|
||||
id: 'geosure',
|
||||
name: 'GeoSure Ground Stability',
|
||||
origin: 'Ordnance Survey',
|
||||
use: 'Ground stability hazard ratings on a 5km hex grid covering Great Britain. Six risk categories (collapsible deposits, compressible ground, landslides, running sand, shrink-swell, and soluble rocks) rated Low, Moderate, or Significant. Spatial-joined to postcodes via centroid intersection.',
|
||||
url: 'https://osdatahub.os.uk/downloads/open/GeoSure',
|
||||
license: 'Open Government Licence v3.0',
|
||||
},
|
||||
{
|
||||
id: 'council-tax',
|
||||
name: 'Council Tax Levels 2025-26',
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ const LOADING_MESSAGES = [
|
|||
'Analysing your query...',
|
||||
'Searching for destinations...',
|
||||
'Generating filters...',
|
||||
'Refining results...',
|
||||
];
|
||||
|
||||
/** Cycle through loading messages to show progress. */
|
||||
|
|
@ -28,9 +29,11 @@ function useLoadingMessage(loading: boolean): string {
|
|||
// Advance message every 1.5s
|
||||
timerRef.current = setTimeout(() => setIndex(1), 1500);
|
||||
const t2 = setTimeout(() => setIndex(2), 3500);
|
||||
const t3 = setTimeout(() => setIndex(3), 5500);
|
||||
return () => {
|
||||
clearTimeout(timerRef.current);
|
||||
clearTimeout(t2);
|
||||
clearTimeout(t3);
|
||||
};
|
||||
}, [loading]);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import { useMemo, useState } from 'react';
|
||||
import { useCollapsibleGroups } from '../../hooks/useCollapsibleGroups';
|
||||
import type {
|
||||
FeatureFilters,
|
||||
FeatureMeta,
|
||||
|
|
@ -38,6 +37,8 @@ interface AreaPaneProps {
|
|||
filters: FeatureFilters;
|
||||
onNavigateToSource?: (slug: string, featureName: string) => void;
|
||||
travelTimeEntries?: TravelTimeEntry[];
|
||||
isGroupExpanded: (name: string) => boolean;
|
||||
onToggleGroup: (name: string) => void;
|
||||
}
|
||||
|
||||
export default function AreaPane({
|
||||
|
|
@ -52,11 +53,12 @@ export default function AreaPane({
|
|||
filters,
|
||||
onNavigateToSource,
|
||||
travelTimeEntries,
|
||||
isGroupExpanded,
|
||||
onToggleGroup,
|
||||
}: AreaPaneProps) {
|
||||
const propertyCount = isPostcode && postcodeData ? postcodeData.properties.count : stats?.count;
|
||||
const featureGroups = useMemo(() => groupFeaturesByCategory(globalFeatures), [globalFeatures]);
|
||||
const [infoFeature, setInfoFeature] = useState<FeatureMeta | null>(null);
|
||||
const [collapsedGroups, toggleGroup] = useCollapsibleGroups();
|
||||
|
||||
const numericByName = useMemo(() => {
|
||||
if (!stats) return new Map();
|
||||
|
|
@ -165,17 +167,17 @@ export default function AreaPane({
|
|||
) ?? []
|
||||
);
|
||||
|
||||
const isExpanded = !collapsedGroups.has(group.name);
|
||||
const expanded = isGroupExpanded(group.name);
|
||||
|
||||
return (
|
||||
<div key={group.name}>
|
||||
<CollapsibleGroupHeader
|
||||
name={group.name}
|
||||
expanded={isExpanded}
|
||||
onToggle={() => toggleGroup(group.name)}
|
||||
expanded={expanded}
|
||||
onToggle={() => onToggleGroup(group.name)}
|
||||
className="px-3 py-2.5 text-sm font-bold text-warm-500 bg-warm-50 dark:bg-warm-900 dark:text-warm-400 sticky top-0 z-10 hover:bg-warm-100 dark:hover:bg-warm-800"
|
||||
/>
|
||||
{isExpanded && (
|
||||
{expanded && (
|
||||
<div className="px-3 py-2 space-y-3">
|
||||
{stackedCharts
|
||||
? stackedCharts.map((chart) => {
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ export default function FeatureBrowser({
|
|||
const [search, setSearch] = useState('');
|
||||
const [infoFeature, setInfoFeature] = useState<FeatureMeta | null>(null);
|
||||
const [travelInfoMode, setTravelInfoMode] = useState<TransportMode | null>(null);
|
||||
const [expandedGroups, toggleGroup] = useCollapsibleGroups();
|
||||
const [isGroupExpanded, toggleGroup] = useCollapsibleGroups(true);
|
||||
const availableTravelModes = useTravelModes();
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -106,7 +106,7 @@ export default function FeatureBrowser({
|
|||
</div>
|
||||
<div className="md:min-h-0 md:flex-1 md:overflow-y-auto flex flex-col">
|
||||
{mergedGrouped.map((group) => {
|
||||
const isExpanded = isSearching || expandedGroups.has(group.name);
|
||||
const isExpanded = isSearching || isGroupExpanded(group.name);
|
||||
return (
|
||||
<div key={group.name} className="shrink-0">
|
||||
<CollapsibleGroupHeader
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ import { usePOIData } from '../../hooks/usePOIData';
|
|||
import { useFilters } from '../../hooks/useFilters';
|
||||
import { useHexagonSelection } from '../../hooks/useHexagonSelection';
|
||||
import { usePaneResize } from '../../hooks/usePaneResize';
|
||||
import { useCollapsibleGroups } from '../../hooks/useCollapsibleGroups';
|
||||
import { useAiFilters } from '../../hooks/useAiFilters';
|
||||
import { useUrlSync } from '../../hooks/useUrlSync';
|
||||
import { useTutorial } from '../../hooks/useTutorial';
|
||||
|
|
@ -274,6 +275,7 @@ export default function MapPage({
|
|||
}, []);
|
||||
|
||||
const pois = usePOIData(mapData.bounds, selectedPOICategories);
|
||||
const [isAreaGroupExpanded, toggleAreaGroup] = useCollapsibleGroups(true);
|
||||
|
||||
useUrlSync(
|
||||
mapData.currentView,
|
||||
|
|
@ -528,6 +530,8 @@ export default function MapPage({
|
|||
hexagonLocation={hexagonLocation}
|
||||
filters={filters}
|
||||
travelTimeEntries={travelTime.activeEntries}
|
||||
isGroupExpanded={isAreaGroupExpanded}
|
||||
onToggleGroup={toggleAreaGroup}
|
||||
/>
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ export default function POIPane({
|
|||
onNavigateToSource,
|
||||
}: POIPaneProps) {
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [collapsedGroups, toggleCollapse] = useCollapsibleGroups();
|
||||
const [isGroupExpanded, toggleCollapse] = useCollapsibleGroups();
|
||||
const [showInfo, setShowInfo] = useState(false);
|
||||
|
||||
const allCategories = groups.flatMap((g) => g.categories);
|
||||
|
|
@ -150,7 +150,7 @@ export default function POIPane({
|
|||
const groupSelected = group.categories.filter((c) => selectedCategories.has(c)).length;
|
||||
const allInGroupSelected = groupSelected === group.categories.length;
|
||||
const someInGroupSelected = groupSelected > 0 && !allInGroupSelected;
|
||||
const isCollapsed = collapsedGroups.has(group.name) && !searchTerm;
|
||||
const isCollapsed = !isGroupExpanded(group.name) && !searchTerm;
|
||||
|
||||
return (
|
||||
<div key={group.name}>
|
||||
|
|
|
|||
|
|
@ -169,15 +169,6 @@ export default function Header({
|
|||
Pricing
|
||||
</a>
|
||||
)}
|
||||
{user && (
|
||||
<a
|
||||
href={PAGE_PATHS.saved}
|
||||
className={tabClass('saved')}
|
||||
onClick={(e) => navLink('saved', e)}
|
||||
>
|
||||
Saved
|
||||
</a>
|
||||
)}
|
||||
</nav>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -187,20 +178,6 @@ export default function Header({
|
|||
{/* Desktop-only dashboard actions */}
|
||||
{!isMobile && activePage === 'dashboard' && (
|
||||
<>
|
||||
{onSaveSearch && (
|
||||
<button
|
||||
onClick={onSaveSearch}
|
||||
disabled={savingSearch}
|
||||
className="flex items-center gap-1.5 px-3 py-1.5 rounded bg-navy-800 hover:bg-navy-700 transition-colors text-sm disabled:opacity-50 disabled:cursor-wait"
|
||||
>
|
||||
{savingSearch ? (
|
||||
<SpinnerIcon className="w-4 h-4 animate-spin" />
|
||||
) : (
|
||||
<BookmarkIcon className="w-4 h-4" />
|
||||
)}
|
||||
Save
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={handleShare}
|
||||
disabled={sharing}
|
||||
|
|
@ -232,8 +209,31 @@ export default function Header({
|
|||
<DownloadIcon className="w-4 h-4" />
|
||||
{exporting ? 'Exporting...' : 'Export'}
|
||||
</button>
|
||||
{onSaveSearch && (
|
||||
<button
|
||||
onClick={onSaveSearch}
|
||||
disabled={savingSearch}
|
||||
className="flex items-center gap-1.5 px-3 py-1.5 rounded bg-navy-800 hover:bg-navy-700 transition-colors text-sm disabled:opacity-50 disabled:cursor-wait"
|
||||
>
|
||||
{savingSearch ? (
|
||||
<SpinnerIcon className="w-4 h-4 animate-spin" />
|
||||
) : (
|
||||
<BookmarkIcon className="w-4 h-4" />
|
||||
)}
|
||||
Save
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{!isMobile && user && (
|
||||
<a
|
||||
href={PAGE_PATHS.saved}
|
||||
className={tabClass('saved')}
|
||||
onClick={(e) => navLink('saved', e)}
|
||||
>
|
||||
Saved
|
||||
</a>
|
||||
)}
|
||||
|
||||
{/* Desktop-only auth */}
|
||||
{!isMobile && (
|
||||
|
|
|
|||
|
|
@ -90,28 +90,10 @@ export default function MobileMenu({
|
|||
mobileNavItem('pricing', 'Pricing')}
|
||||
{user && mobileNavItem('invites', 'Invite Friends')}
|
||||
{user && mobileNavItem('account', 'Account')}
|
||||
{user && mobileNavItem('saved', 'Saved')}
|
||||
|
||||
{/* Dashboard actions */}
|
||||
{activePage === 'dashboard' && (
|
||||
<div className="mt-3 pt-3 border-t border-navy-700 flex flex-col gap-1">
|
||||
{onSaveSearch && (
|
||||
<button
|
||||
onClick={() => {
|
||||
onSaveSearch();
|
||||
onClose();
|
||||
}}
|
||||
disabled={savingSearch}
|
||||
className="w-full flex items-center gap-2 px-4 py-3 text-base text-warm-300 hover:bg-navy-800 hover:text-white rounded disabled:opacity-50"
|
||||
>
|
||||
{savingSearch ? (
|
||||
<SpinnerIcon className="w-5 h-5 animate-spin" />
|
||||
) : (
|
||||
<BookmarkIcon className="w-5 h-5" />
|
||||
)}
|
||||
Save
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={() => {
|
||||
onShare();
|
||||
|
|
@ -133,8 +115,27 @@ export default function MobileMenu({
|
|||
<DownloadIcon className="w-5 h-5" />
|
||||
{exporting ? 'Exporting...' : 'Export'}
|
||||
</button>
|
||||
{onSaveSearch && (
|
||||
<button
|
||||
onClick={() => {
|
||||
onSaveSearch();
|
||||
onClose();
|
||||
}}
|
||||
disabled={savingSearch}
|
||||
className="w-full flex items-center gap-2 px-4 py-3 text-base text-warm-300 hover:bg-navy-800 hover:text-white rounded disabled:opacity-50"
|
||||
>
|
||||
{savingSearch ? (
|
||||
<SpinnerIcon className="w-5 h-5 animate-spin" />
|
||||
) : (
|
||||
<BookmarkIcon className="w-5 h-5" />
|
||||
)}
|
||||
Save
|
||||
</button>
|
||||
)}
|
||||
{user && mobileNavItem('saved', 'Saved')}
|
||||
</div>
|
||||
)}
|
||||
{activePage !== 'dashboard' && user && mobileNavItem('saved', 'Saved')}
|
||||
</nav>
|
||||
|
||||
{/* Theme toggle + Auth section at bottom */}
|
||||
|
|
|
|||
|
|
@ -19,6 +19,8 @@ export interface AiFiltersResult {
|
|||
summary: string;
|
||||
/** The listing mode used (historical/buy/rent) */
|
||||
listingType: string;
|
||||
/** Number of properties matching the proposed filters (excludes travel time) */
|
||||
matchCount: number;
|
||||
}
|
||||
|
||||
export type AiFilterErrorType = 'auth' | 'limit' | 'error';
|
||||
|
|
@ -43,7 +45,11 @@ interface UseAiFiltersResult {
|
|||
}
|
||||
|
||||
/** Build a human-readable summary of the AI result. */
|
||||
function buildSummary(filters: FeatureFilters, travelTimeFilters: AiTravelTimeFilter[]): string {
|
||||
function buildSummary(
|
||||
filters: FeatureFilters,
|
||||
travelTimeFilters: AiTravelTimeFilter[],
|
||||
matchCount: number
|
||||
): string {
|
||||
const parts: string[] = [];
|
||||
|
||||
for (const [name, value] of Object.entries(filters)) {
|
||||
|
|
@ -63,7 +69,8 @@ function buildSummary(filters: FeatureFilters, travelTimeFilters: AiTravelTimeFi
|
|||
}
|
||||
|
||||
if (parts.length === 0) return 'No filters set';
|
||||
return `Set ${parts.length} filter${parts.length > 1 ? 's' : ''}: ${parts.join(', ')}`;
|
||||
const countStr = matchCount.toLocaleString();
|
||||
return `${countStr} properties match · Set ${parts.length} filter${parts.length > 1 ? 's' : ''}: ${parts.join(', ')}`;
|
||||
}
|
||||
|
||||
export function useAiFilters(): UseAiFiltersResult {
|
||||
|
|
@ -137,13 +144,15 @@ export function useAiFilters(): UseAiFiltersResult {
|
|||
})
|
||||
);
|
||||
const filters = json.filters as FeatureFilters;
|
||||
const summaryText = buildSummary(filters, travelTimeFilters);
|
||||
const matchCount: number = json.match_count ?? 0;
|
||||
const summaryText = buildSummary(filters, travelTimeFilters, matchCount);
|
||||
const result: AiFiltersResult = {
|
||||
filters,
|
||||
travelTimeFilters,
|
||||
notes: json.notes || '',
|
||||
summary: summaryText,
|
||||
listingType: json.listing_type || 'historical',
|
||||
matchCount,
|
||||
};
|
||||
setNotes(result.notes || null);
|
||||
setSummary(summaryText);
|
||||
|
|
|
|||
|
|
@ -1,14 +1,24 @@
|
|||
import { useState, useCallback } from 'react';
|
||||
|
||||
export function useCollapsibleGroups(): [
|
||||
Set<string>,
|
||||
/**
|
||||
* Manages collapsible group state.
|
||||
* @param defaultCollapsed When true, groups start collapsed (tracks expanded groups).
|
||||
* When false (default), groups start expanded (tracks collapsed groups).
|
||||
*/
|
||||
export function useCollapsibleGroups(defaultCollapsed = false): [
|
||||
(name: string) => boolean,
|
||||
(name: string) => void,
|
||||
(name: string) => void,
|
||||
] {
|
||||
const [collapsed, setCollapsed] = useState<Set<string>>(new Set());
|
||||
const [toggled, setToggled] = useState<Set<string>>(new Set());
|
||||
|
||||
const isExpanded = useCallback(
|
||||
(name: string) => (defaultCollapsed ? toggled.has(name) : !toggled.has(name)),
|
||||
[toggled, defaultCollapsed]
|
||||
);
|
||||
|
||||
const toggle = useCallback((name: string) => {
|
||||
setCollapsed((prev) => {
|
||||
setToggled((prev) => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(name)) next.delete(name);
|
||||
else next.add(name);
|
||||
|
|
@ -16,14 +26,24 @@ export function useCollapsibleGroups(): [
|
|||
});
|
||||
}, []);
|
||||
|
||||
const expand = useCallback((name: string) => {
|
||||
setCollapsed((prev) => {
|
||||
if (!prev.has(name)) return prev;
|
||||
const next = new Set(prev);
|
||||
next.delete(name);
|
||||
return next;
|
||||
});
|
||||
}, []);
|
||||
const expand = useCallback(
|
||||
(name: string) => {
|
||||
setToggled((prev) => {
|
||||
if (defaultCollapsed) {
|
||||
if (prev.has(name)) return prev;
|
||||
const next = new Set(prev);
|
||||
next.add(name);
|
||||
return next;
|
||||
} else {
|
||||
if (!prev.has(name)) return prev;
|
||||
const next = new Set(prev);
|
||||
next.delete(name);
|
||||
return next;
|
||||
}
|
||||
});
|
||||
},
|
||||
[defaultCollapsed]
|
||||
);
|
||||
|
||||
return [collapsed, toggle, expand];
|
||||
return [isExpanded, toggle, expand];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -321,7 +321,7 @@ export function useDeckLayers({
|
|||
ttVal as number,
|
||||
ttVal as number,
|
||||
clr,
|
||||
null,
|
||||
fr,
|
||||
0,
|
||||
densityGradientRef.current,
|
||||
dark,
|
||||
|
|
@ -422,7 +422,7 @@ export function useDeckLayers({
|
|||
ttVal as number,
|
||||
ttVal as number,
|
||||
clr,
|
||||
null,
|
||||
fr,
|
||||
0,
|
||||
densityGradientRef.current,
|
||||
dark,
|
||||
|
|
|
|||
|
|
@ -119,11 +119,14 @@ export function useMapData({
|
|||
const boundsStr = `${bounds.south},${bounds.west},${bounds.north},${bounds.east}`;
|
||||
const isTravelTimeDrag = activeFeature.startsWith('tt_');
|
||||
const dragTravelParam = isTravelTimeDrag ? buildTravelParam(activeFeature) : travelParam;
|
||||
// Travel time fields are computed from the travel param, not regular feature columns.
|
||||
// Sending a tt_* name as fields would cause a 400 (unknown field). Use empty string instead.
|
||||
const fieldsParam = isTravelTimeDrag ? '' : activeFeature;
|
||||
|
||||
if (usePostcodeView) {
|
||||
const params = new URLSearchParams({ bounds: boundsStr });
|
||||
if (filtersStr) params.set('filters', filtersStr);
|
||||
params.set('fields', activeFeature);
|
||||
params.set('fields', fieldsParam);
|
||||
if (dragTravelParam) params.set('travel', dragTravelParam);
|
||||
|
||||
fetch(apiUrl('postcodes', params), authHeaders({ signal: dragAbortRef.current.signal }))
|
||||
|
|
@ -140,7 +143,7 @@ export function useMapData({
|
|||
bounds: boundsStr,
|
||||
});
|
||||
if (filtersStr) params.set('filters', filtersStr);
|
||||
params.set('fields', activeFeature);
|
||||
params.set('fields', fieldsParam);
|
||||
if (dragTravelParam) params.set('travel', dragTravelParam);
|
||||
|
||||
fetch(apiUrl('hexagons', params), authHeaders({ signal: dragAbortRef.current.signal }))
|
||||
|
|
|
|||
|
|
@ -189,22 +189,7 @@ export const STACKED_ENUM_GROUPS: Record<
|
|||
valueColors: ['#3b82f6', '#f59e0b'],
|
||||
},
|
||||
],
|
||||
Environment: [
|
||||
{
|
||||
label: 'Ground Risk',
|
||||
feature: 'Environmental risk',
|
||||
components: [
|
||||
'Collapsible deposits risk',
|
||||
'Compressible ground risk',
|
||||
'Landslide risk',
|
||||
'Running sand risk',
|
||||
'Shrink-swell risk',
|
||||
'Soluble rocks risk',
|
||||
],
|
||||
valueOrder: ['Low', 'Moderate', 'Significant'],
|
||||
valueColors: ['#22c55e', '#eab308', '#ef4444'],
|
||||
},
|
||||
],
|
||||
Environment: [],
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -443,52 +443,6 @@ const FEATURE_ICON_PATHS: Record<string, ReactNode> = {
|
|||
<line x1="12" y1="20" x2="12.01" y2="20" />
|
||||
</>
|
||||
),
|
||||
'Environmental risk': (
|
||||
<>
|
||||
<path d="M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0z" />
|
||||
<line x1="12" y1="9" x2="12" y2="13" />
|
||||
<line x1="12" y1="17" x2="12.01" y2="17" />
|
||||
</>
|
||||
),
|
||||
'Collapsible deposits risk': (
|
||||
<>
|
||||
<polyline points="12 2 2 7 12 12 22 7 12 2" />
|
||||
<polyline points="2 17 12 22 22 17" />
|
||||
<polyline points="2 12 12 17 22 12" />
|
||||
</>
|
||||
),
|
||||
'Compressible ground risk': (
|
||||
<>
|
||||
<line x1="12" y1="2" x2="12" y2="22" />
|
||||
<polyline points="16 6 12 2 8 6" />
|
||||
<polyline points="16 18 12 22 8 18" />
|
||||
<line x1="4" y1="12" x2="20" y2="12" />
|
||||
</>
|
||||
),
|
||||
'Landslide risk': (
|
||||
<>
|
||||
<path d="M8 3l4 8 5-5 5 15H2L8 3z" />
|
||||
</>
|
||||
),
|
||||
'Running sand risk': (
|
||||
<>
|
||||
<path d="M2 6c2-1 4-1 6 0s4 1 6 0 4-1 6 0" />
|
||||
<path d="M2 12c2-1 4-1 6 0s4 1 6 0 4-1 6 0" />
|
||||
<path d="M2 18c2-1 4-1 6 0s4 1 6 0 4-1 6 0" />
|
||||
</>
|
||||
),
|
||||
'Shrink-swell risk': (
|
||||
<>
|
||||
<line x1="2" y1="12" x2="22" y2="12" />
|
||||
<polyline points="6 8 2 12 6 16" />
|
||||
<polyline points="18 8 22 12 18 16" />
|
||||
</>
|
||||
),
|
||||
'Soluble rocks risk': (
|
||||
<>
|
||||
<path d="M12 2.69l5.66 5.66a8 8 0 11-11.31 0z" />
|
||||
</>
|
||||
),
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -186,5 +186,13 @@ export function summarizeParams(queryString: string): string {
|
|||
}
|
||||
}
|
||||
|
||||
const ttParams = params.getAll('tt');
|
||||
if (ttParams.length > 0) {
|
||||
const count = ttParams.filter(Boolean).length;
|
||||
if (count > 0) {
|
||||
parts.push(`${count} travel time ${count === 1 ? 'destination' : 'destinations'}`);
|
||||
}
|
||||
}
|
||||
|
||||
return parts.length > 0 ? parts.join(' + ') : 'No filters';
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue