alright
This commit is contained in:
parent
c645b0f1d4
commit
39ef5c6646
79 changed files with 5660 additions and 2199 deletions
|
|
@ -1,7 +1,7 @@
|
|||
import { useState, useCallback, useRef, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import type { MapFlyToOptions, PostcodeGeometry } from '../../types';
|
||||
import { authHeaders } from '../../lib/api';
|
||||
import { authHeaders, isAbortError } from '../../lib/api';
|
||||
import { POSTCODE_SEARCH_ZOOM } from '../../lib/consts';
|
||||
import { useIsMobile } from '../../hooks/useIsMobile';
|
||||
import { useLocationSearch, type SearchResult } from '../../hooks/useLocationSearch';
|
||||
|
|
@ -16,6 +16,7 @@ export interface SearchedLocation {
|
|||
geometry: PostcodeGeometry;
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
zoom: number;
|
||||
markerLatitude?: number;
|
||||
markerLongitude?: number;
|
||||
openProperties?: boolean;
|
||||
|
|
@ -73,6 +74,34 @@ export default function LocationSearch({
|
|||
const isMobile = useIsMobile();
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const lookupAbortRef = useRef<AbortController | null>(null);
|
||||
const lookupRequestIdRef = useRef(0);
|
||||
|
||||
const cancelLookup = useCallback((updateLoading = true) => {
|
||||
lookupRequestIdRef.current += 1;
|
||||
lookupAbortRef.current?.abort();
|
||||
lookupAbortRef.current = null;
|
||||
if (updateLoading) setLoading(false);
|
||||
}, []);
|
||||
|
||||
const beginLookup = useCallback(() => {
|
||||
lookupAbortRef.current?.abort();
|
||||
const controller = new AbortController();
|
||||
lookupAbortRef.current = controller;
|
||||
lookupRequestIdRef.current += 1;
|
||||
setError(null);
|
||||
setLoading(true);
|
||||
search.close();
|
||||
return { controller, requestId: lookupRequestIdRef.current };
|
||||
}, [search]);
|
||||
|
||||
const isCurrentLookup = useCallback((requestId: number, controller: AbortController) => {
|
||||
return (
|
||||
lookupRequestIdRef.current === requestId &&
|
||||
lookupAbortRef.current === controller &&
|
||||
!controller.signal.aborted
|
||||
);
|
||||
}, []);
|
||||
|
||||
// Close on outside click
|
||||
useEffect(() => {
|
||||
|
|
@ -93,106 +122,136 @@ export default function LocationSearch({
|
|||
}
|
||||
}, [isMobile, expanded]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => cancelLookup(false);
|
||||
}, [cancelLookup]);
|
||||
|
||||
const selectResult = useCallback(
|
||||
async (result: SearchResult) => {
|
||||
const { controller, requestId } = beginLookup();
|
||||
|
||||
if (result.type === 'place') {
|
||||
const zoom = ZOOM_FOR_TYPE[result.place_type] ?? 14;
|
||||
setError(null);
|
||||
setLoading(true);
|
||||
search.close();
|
||||
onFlyTo(result.lat, result.lon, zoom);
|
||||
// On mobile the drawer opens after onLocationSearched; MapPage handles
|
||||
// the fly-to there with the correct viewport inset so the target isn't
|
||||
// hidden behind the drawer. On desktop fly immediately for snappy feedback.
|
||||
if (!isMobile) onFlyTo(result.lat, result.lon, zoom);
|
||||
try {
|
||||
const params = new URLSearchParams({
|
||||
lat: String(result.lat),
|
||||
lng: String(result.lon),
|
||||
log: 'false',
|
||||
});
|
||||
const res = await fetch(`/api/nearest-postcode?${params}`, authHeaders());
|
||||
const res = await fetch(
|
||||
`/api/nearest-postcode?${params}`,
|
||||
authHeaders({ signal: controller.signal })
|
||||
);
|
||||
if (!isCurrentLookup(requestId, controller)) return;
|
||||
if (!res.ok) {
|
||||
setError(t('locationSearch.lookupFailed'));
|
||||
return;
|
||||
}
|
||||
const json: PostcodeLookupResponse = await res.json();
|
||||
if (!isCurrentLookup(requestId, controller)) return;
|
||||
onLocationSearched?.({
|
||||
postcode: json.postcode,
|
||||
geometry: json.geometry,
|
||||
latitude: json.latitude,
|
||||
longitude: json.longitude,
|
||||
zoom,
|
||||
markerLatitude: result.lat,
|
||||
markerLongitude: result.lon,
|
||||
});
|
||||
search.saveRecentSearch(result);
|
||||
search.clear();
|
||||
if (isMobile) setExpanded(false);
|
||||
} catch {
|
||||
} catch (error) {
|
||||
if (!isCurrentLookup(requestId, controller) || isAbortError(error)) return;
|
||||
setError(t('locationSearch.lookupFailed'));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
if (isCurrentLookup(requestId, controller)) {
|
||||
lookupAbortRef.current = null;
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.type === 'address') {
|
||||
setError(null);
|
||||
setLoading(true);
|
||||
search.close();
|
||||
try {
|
||||
const res = await fetch(
|
||||
`/api/postcode/${encodeURIComponent(result.postcode)}`,
|
||||
authHeaders()
|
||||
authHeaders({ signal: controller.signal })
|
||||
);
|
||||
if (!isCurrentLookup(requestId, controller)) return;
|
||||
if (!res.ok) {
|
||||
setError(t('locationSearch.postcodeNotFound'));
|
||||
return;
|
||||
}
|
||||
const json: PostcodeLookupResponse = await res.json();
|
||||
onFlyTo(result.lat, result.lon, 17);
|
||||
if (!isCurrentLookup(requestId, controller)) return;
|
||||
if (!isMobile) onFlyTo(result.lat, result.lon, 17);
|
||||
onLocationSearched?.({
|
||||
postcode: json.postcode,
|
||||
geometry: json.geometry,
|
||||
latitude: result.lat,
|
||||
longitude: result.lon,
|
||||
zoom: 17,
|
||||
markerLatitude: result.lat,
|
||||
markerLongitude: result.lon,
|
||||
openProperties: true,
|
||||
focusAddress: result.address,
|
||||
});
|
||||
search.saveRecentSearch(result);
|
||||
search.clear();
|
||||
if (isMobile) setExpanded(false);
|
||||
} catch {
|
||||
} catch (error) {
|
||||
if (!isCurrentLookup(requestId, controller) || isAbortError(error)) return;
|
||||
setError(t('locationSearch.lookupFailed'));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
if (isCurrentLookup(requestId, controller)) {
|
||||
lookupAbortRef.current = null;
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Postcode — fetch geometry
|
||||
setError(null);
|
||||
setLoading(true);
|
||||
search.close();
|
||||
try {
|
||||
const res = await fetch(`/api/postcode/${encodeURIComponent(result.label)}`, authHeaders());
|
||||
const res = await fetch(
|
||||
`/api/postcode/${encodeURIComponent(result.label)}`,
|
||||
authHeaders({ signal: controller.signal })
|
||||
);
|
||||
if (!isCurrentLookup(requestId, controller)) return;
|
||||
if (!res.ok) {
|
||||
setError(t('locationSearch.postcodeNotFound'));
|
||||
return;
|
||||
}
|
||||
const json: PostcodeLookupResponse = await res.json();
|
||||
onFlyTo(json.latitude, json.longitude, POSTCODE_SEARCH_ZOOM);
|
||||
if (!isCurrentLookup(requestId, controller)) return;
|
||||
if (!isMobile) onFlyTo(json.latitude, json.longitude, POSTCODE_SEARCH_ZOOM);
|
||||
onLocationSearched?.({
|
||||
postcode: json.postcode,
|
||||
geometry: json.geometry,
|
||||
latitude: json.latitude,
|
||||
longitude: json.longitude,
|
||||
zoom: POSTCODE_SEARCH_ZOOM,
|
||||
});
|
||||
search.saveRecentSearch(result);
|
||||
search.clear();
|
||||
if (isMobile) setExpanded(false);
|
||||
} catch {
|
||||
} catch (error) {
|
||||
if (!isCurrentLookup(requestId, controller) || isAbortError(error)) return;
|
||||
setError(t('locationSearch.lookupFailed'));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
if (isCurrentLookup(requestId, controller)) {
|
||||
lookupAbortRef.current = null;
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
},
|
||||
[onFlyTo, onLocationSearched, isMobile, search, t]
|
||||
[beginLookup, isCurrentLookup, onFlyTo, onLocationSearched, isMobile, search, t]
|
||||
);
|
||||
|
||||
const [locating, setLocating] = useState(false);
|
||||
|
|
@ -203,6 +262,7 @@ export default function LocationSearch({
|
|||
return;
|
||||
}
|
||||
setError(null);
|
||||
cancelLookup();
|
||||
setLocating(true);
|
||||
search.close();
|
||||
try {
|
||||
|
|
@ -234,7 +294,7 @@ export default function LocationSearch({
|
|||
} finally {
|
||||
setLocating(false);
|
||||
}
|
||||
}, [onFlyTo, onCurrentLocationFound, isMobile, search, t]);
|
||||
}, [cancelLookup, onFlyTo, onCurrentLocationFound, isMobile, search, t]);
|
||||
|
||||
// Mobile collapsed state: search icon + locate button
|
||||
if (isMobile && !expanded) {
|
||||
|
|
@ -281,7 +341,10 @@ export default function LocationSearch({
|
|||
'px-2 py-2 text-sm w-56 border-none outline-none bg-transparent text-warm-700 dark:text-warm-200 placeholder-warm-400 dark:placeholder-warm-500'
|
||||
}
|
||||
inputRef={inputRef}
|
||||
onInputChange={() => setError(null)}
|
||||
onInputChange={() => {
|
||||
setError(null);
|
||||
cancelLookup();
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue