seems fine
This commit is contained in:
parent
48983e3b4b
commit
7a1696541f
37 changed files with 4999 additions and 1242 deletions
|
|
@ -178,7 +178,7 @@ export function useHexagonSelection({
|
|||
);
|
||||
|
||||
const fetchPostcodeProperties = useCallback(
|
||||
async (postcode: string, offset = 0) => {
|
||||
async (postcode: string, offset = 0, focusAddress?: string) => {
|
||||
setLoadingProperties(true);
|
||||
try {
|
||||
const params = new URLSearchParams({
|
||||
|
|
@ -186,6 +186,9 @@ export function useHexagonSelection({
|
|||
limit: '100',
|
||||
offset: offset.toString(),
|
||||
});
|
||||
if (focusAddress && offset === 0) {
|
||||
params.set('focus_address', focusAddress);
|
||||
}
|
||||
|
||||
const filterStr = buildFilterString(filters, features);
|
||||
if (filterStr) params.append('filters', filterStr);
|
||||
|
|
@ -464,13 +467,20 @@ export function useHexagonSelection({
|
|||
]);
|
||||
|
||||
const handleLocationSearch = useCallback(
|
||||
(postcode: string, geometry: PostcodeGeometry, lat?: number, lng?: number) => {
|
||||
trackEvent('Postcode Search');
|
||||
(
|
||||
postcode: string,
|
||||
geometry: PostcodeGeometry,
|
||||
lat?: number,
|
||||
lng?: number,
|
||||
openProperties = false,
|
||||
focusAddress?: string
|
||||
) => {
|
||||
trackEvent(openProperties ? 'Address Search' : 'Postcode Search');
|
||||
setProperties([]);
|
||||
setPropertiesTotal(0);
|
||||
setPropertiesOffset(0);
|
||||
setUnfilteredAreaCount(null);
|
||||
setRightPaneTab('area');
|
||||
setRightPaneTab(openProperties ? 'properties' : 'area');
|
||||
setLoadingAreaStats(true);
|
||||
|
||||
// First try the postcode; if it has no properties, fall back to hexagons
|
||||
|
|
@ -482,6 +492,9 @@ export function useHexagonSelection({
|
|||
setSelectedPostcodeGeometry(geometry);
|
||||
setAreaStats(stats);
|
||||
refreshUnfilteredAreaCount(selection, stats.count);
|
||||
if (openProperties) {
|
||||
fetchPostcodeProperties(postcode, 0, focusAddress);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -493,6 +506,7 @@ export function useHexagonSelection({
|
|||
setSelectedPostcodeGeometry(geometry);
|
||||
setAreaStats(stats);
|
||||
refreshUnfilteredAreaCount(selection, stats.count);
|
||||
setRightPaneTab('area');
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -507,6 +521,7 @@ export function useHexagonSelection({
|
|||
setSelectedPostcodeGeometry(null);
|
||||
setAreaStats(hexStats);
|
||||
refreshUnfilteredAreaCount(selection, hexStats.count);
|
||||
setRightPaneTab('area');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
|
@ -519,11 +534,18 @@ export function useHexagonSelection({
|
|||
setSelectedPostcodeGeometry(null);
|
||||
setAreaStats(fallbackStats);
|
||||
refreshUnfilteredAreaCount(selection, fallbackStats.count);
|
||||
setRightPaneTab('area');
|
||||
})
|
||||
.catch((error) => logNonAbortError('Failed to fetch postcode stats', error))
|
||||
.finally(() => setLoadingAreaStats(false));
|
||||
},
|
||||
[resolution, fetchPostcodeStats, fetchHexagonStats, refreshUnfilteredAreaCount]
|
||||
[
|
||||
resolution,
|
||||
fetchPostcodeStats,
|
||||
fetchHexagonStats,
|
||||
fetchPostcodeProperties,
|
||||
refreshUnfilteredAreaCount,
|
||||
]
|
||||
);
|
||||
|
||||
const handleCurrentLocationSearch = useCallback(
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { useState, useCallback, useRef, useEffect } from 'react';
|
||||
import type { PlaceResult } from '../types';
|
||||
import type { AddressResult, PlaceResult } from '../types';
|
||||
import { authHeaders, logNonAbortError } from '../lib/api';
|
||||
|
||||
/** Matches a full UK postcode with complete inward code (e.g. "E14 2DG", "SW1A1AA").
|
||||
|
|
@ -19,8 +19,54 @@ function normalizePostcode(s: string): string {
|
|||
return stripped;
|
||||
}
|
||||
|
||||
function searchableTextForResult(result: SearchResult): string {
|
||||
if (result.type === 'postcode') {
|
||||
return result.label;
|
||||
}
|
||||
if (result.type === 'address') {
|
||||
return `${result.address} ${result.postcode}`;
|
||||
}
|
||||
return `${result.name} ${result.city ?? ''}`;
|
||||
}
|
||||
|
||||
function searchTokens(value: string): string[] {
|
||||
return value.toLowerCase().match(/[a-z0-9]+/g) ?? [];
|
||||
}
|
||||
|
||||
function compactSearchText(value: string): string {
|
||||
return searchTokens(value).join('');
|
||||
}
|
||||
|
||||
function resultMatchesQuery(result: SearchResult, query: string): boolean {
|
||||
const queryTokens = searchTokens(query);
|
||||
if (queryTokens.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const searchable = searchableTextForResult(result);
|
||||
const resultText = searchTokens(searchable).join(' ');
|
||||
const resultCompact = compactSearchText(searchable);
|
||||
const queryCompact = queryTokens.join('');
|
||||
|
||||
return (
|
||||
queryTokens.every((token) => resultText.includes(token)) ||
|
||||
(queryCompact.length >= 2 && resultCompact.includes(queryCompact))
|
||||
);
|
||||
}
|
||||
|
||||
function filterResultsForQuery(results: SearchResult[], query: string): SearchResult[] {
|
||||
return results.filter((result) => resultMatchesQuery(result, query)).slice(0, 20);
|
||||
}
|
||||
|
||||
export type SearchResult =
|
||||
| { type: 'postcode'; label: string }
|
||||
| {
|
||||
type: 'address';
|
||||
address: string;
|
||||
postcode: string;
|
||||
lat: number;
|
||||
lon: number;
|
||||
}
|
||||
| {
|
||||
type: 'place';
|
||||
name: string;
|
||||
|
|
@ -38,10 +84,13 @@ export function useLocationSearch(mode?: string) {
|
|||
const [open, setOpen] = useState(false);
|
||||
const abortRef = useRef<AbortController | null>(null);
|
||||
const debounceRef = useRef<ReturnType<typeof setTimeout>>();
|
||||
const latestQueryRef = useRef('');
|
||||
const lastResultsRef = useRef<SearchResult[]>([]);
|
||||
|
||||
const handleInputChange = useCallback(
|
||||
(value: string) => {
|
||||
setQuery(value);
|
||||
latestQueryRef.current = value;
|
||||
setActiveIndex(-1);
|
||||
|
||||
abortRef.current?.abort();
|
||||
|
|
@ -50,12 +99,16 @@ export function useLocationSearch(mode?: string) {
|
|||
const trimmed = value.trim();
|
||||
if (!trimmed) {
|
||||
setResults([]);
|
||||
lastResultsRef.current = [];
|
||||
setOpen(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!mode && looksLikePostcode(trimmed)) {
|
||||
setResults([{ type: 'postcode', label: normalizePostcode(trimmed) }]);
|
||||
const postcodeResults: SearchResult[] = [
|
||||
{ type: 'postcode', label: normalizePostcode(trimmed) },
|
||||
];
|
||||
setResults(postcodeResults);
|
||||
setOpen(true);
|
||||
return;
|
||||
}
|
||||
|
|
@ -66,19 +119,27 @@ export function useLocationSearch(mode?: string) {
|
|||
return;
|
||||
}
|
||||
|
||||
const locallyFilteredResults = filterResultsForQuery(lastResultsRef.current, trimmed);
|
||||
setResults(locallyFilteredResults);
|
||||
setOpen(locallyFilteredResults.length > 0);
|
||||
|
||||
debounceRef.current = setTimeout(async () => {
|
||||
const controller = new AbortController();
|
||||
abortRef.current = controller;
|
||||
try {
|
||||
const params = new URLSearchParams({ q: trimmed, limit: '7' });
|
||||
const params = new URLSearchParams({ q: trimmed, limit: '20' });
|
||||
if (mode) params.set('mode', mode);
|
||||
const res = await fetch(
|
||||
`/api/places?${params}`,
|
||||
authHeaders({ signal: controller.signal })
|
||||
);
|
||||
if (!res.ok) return;
|
||||
const json: { places: PlaceResult[] } = await res.json();
|
||||
const placeResults: SearchResult[] = json.places.map((p) => ({
|
||||
const json: {
|
||||
places: PlaceResult[];
|
||||
postcodes?: string[];
|
||||
addresses?: AddressResult[];
|
||||
} = await res.json();
|
||||
const placeResults = json.places.map((p) => ({
|
||||
type: 'place' as const,
|
||||
name: p.name,
|
||||
slug: p.slug,
|
||||
|
|
@ -87,8 +148,34 @@ export function useLocationSearch(mode?: string) {
|
|||
lon: p.lon,
|
||||
city: p.city === 'City of London' ? 'London' : p.city,
|
||||
}));
|
||||
setResults(placeResults);
|
||||
setOpen(placeResults.length > 0);
|
||||
const outcodeResults = placeResults.filter((result) => result.place_type === 'outcode');
|
||||
const otherPlaceResults = placeResults.filter(
|
||||
(result) => result.place_type !== 'outcode'
|
||||
);
|
||||
const postcodeResults: SearchResult[] = (json.postcodes ?? []).map((postcode) => ({
|
||||
type: 'postcode' as const,
|
||||
label: postcode,
|
||||
}));
|
||||
const addressResults: SearchResult[] = (json.addresses ?? []).map((address) => ({
|
||||
type: 'address' as const,
|
||||
address: address.address,
|
||||
postcode: address.postcode,
|
||||
lat: address.lat,
|
||||
lon: address.lon,
|
||||
}));
|
||||
const containsHouseNumber = /\d/.test(trimmed);
|
||||
const combinedResults = (
|
||||
containsHouseNumber
|
||||
? [...outcodeResults, ...postcodeResults, ...addressResults, ...otherPlaceResults]
|
||||
: [...outcodeResults, ...postcodeResults, ...otherPlaceResults, ...addressResults]
|
||||
).slice(0, 20);
|
||||
if (controller.signal.aborted || latestQueryRef.current.trim() !== trimmed) {
|
||||
return;
|
||||
}
|
||||
lastResultsRef.current = combinedResults;
|
||||
const matchingResults = filterResultsForQuery(combinedResults, trimmed);
|
||||
setResults(matchingResults);
|
||||
setOpen(matchingResults.length > 0);
|
||||
} catch (err) {
|
||||
logNonAbortError('places search', err);
|
||||
}
|
||||
|
|
@ -101,7 +188,9 @@ export function useLocationSearch(mode?: string) {
|
|||
|
||||
const clear = useCallback(() => {
|
||||
setQuery('');
|
||||
latestQueryRef.current = '';
|
||||
setResults([]);
|
||||
lastResultsRef.current = [];
|
||||
setOpen(false);
|
||||
setActiveIndex(-1);
|
||||
}, []);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue