Lots of improvements
This commit is contained in:
parent
3853b5dce7
commit
b94cf17d75
33 changed files with 2587 additions and 1866 deletions
|
|
@ -1,9 +1,11 @@
|
|||
import { useState, useCallback, useRef, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import type { PostcodeGeometry } from '../../types';
|
||||
import { authHeaders } from '../../lib/api';
|
||||
import { useIsMobile } from '../../hooks/useIsMobile';
|
||||
import { useLocationSearch, type SearchResult } from '../../hooks/useLocationSearch';
|
||||
import { PlaceSearchInput } from '../ui/PlaceSearchInput';
|
||||
import { LocateIcon } from '../ui/icons/LocateIcon';
|
||||
import { SearchIcon } from '../ui/icons/SearchIcon';
|
||||
|
||||
export interface SearchedLocation {
|
||||
|
|
@ -14,6 +16,7 @@ export interface SearchedLocation {
|
|||
const ZOOM_FOR_TYPE: Record<string, number> = {
|
||||
city: 10,
|
||||
borough: 12,
|
||||
outcode: 14,
|
||||
town: 13,
|
||||
suburb: 14,
|
||||
quarter: 14,
|
||||
|
|
@ -35,6 +38,7 @@ export default function LocationSearch({
|
|||
onLocationSearched?: (postcode: SearchedLocation | null) => void;
|
||||
onMouseEnter?: () => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const search = useLocationSearch();
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
|
@ -80,7 +84,7 @@ export default function LocationSearch({
|
|||
try {
|
||||
const res = await fetch(`/api/postcode/${encodeURIComponent(result.label)}`, authHeaders());
|
||||
if (!res.ok) {
|
||||
setError('Postcode not found');
|
||||
setError(t('locationSearch.postcodeNotFound'));
|
||||
return;
|
||||
}
|
||||
const json: {
|
||||
|
|
@ -94,7 +98,7 @@ export default function LocationSearch({
|
|||
search.clear();
|
||||
if (isMobile) setExpanded(false);
|
||||
} catch {
|
||||
setError('Lookup failed');
|
||||
setError(t('locationSearch.lookupFailed'));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
|
|
@ -102,17 +106,71 @@ export default function LocationSearch({
|
|||
[onFlyTo, onLocationSearched, isMobile, search]
|
||||
);
|
||||
|
||||
// Mobile collapsed state: just a search icon button
|
||||
const [locating, setLocating] = useState(false);
|
||||
|
||||
const locateUser = useCallback(async () => {
|
||||
if (!navigator.geolocation) {
|
||||
setError(t('locationSearch.geolocationUnsupported'));
|
||||
return;
|
||||
}
|
||||
setError(null);
|
||||
setLocating(true);
|
||||
search.close();
|
||||
try {
|
||||
const position = await new Promise<GeolocationPosition>((resolve, reject) => {
|
||||
navigator.geolocation.getCurrentPosition(resolve, reject, {
|
||||
enableHighAccuracy: true,
|
||||
timeout: 10000,
|
||||
});
|
||||
});
|
||||
const { latitude, longitude } = position.coords;
|
||||
const res = await fetch(
|
||||
`/api/nearest-postcode?lat=${latitude}&lng=${longitude}`,
|
||||
authHeaders()
|
||||
);
|
||||
if (!res.ok) {
|
||||
setError(t('locationSearch.lookupFailed'));
|
||||
return;
|
||||
}
|
||||
const json: {
|
||||
postcode: string;
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
geometry: PostcodeGeometry;
|
||||
} = await res.json();
|
||||
onFlyTo(json.latitude, json.longitude, 16);
|
||||
onLocationSearched?.({ postcode: json.postcode, geometry: json.geometry });
|
||||
search.clear();
|
||||
if (isMobile) setExpanded(false);
|
||||
} catch {
|
||||
setError(t('locationSearch.geolocationFailed'));
|
||||
} finally {
|
||||
setLocating(false);
|
||||
}
|
||||
}, [onFlyTo, onLocationSearched, isMobile, search, t]);
|
||||
|
||||
// Mobile collapsed state: search icon + locate button
|
||||
if (isMobile && !expanded) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setExpanded(true)}
|
||||
className="p-2 bg-white dark:bg-warm-800 rounded shadow-lg pointer-events-auto"
|
||||
aria-label="Search places or postcodes"
|
||||
>
|
||||
<SearchIcon className="w-5 h-5 text-warm-600 dark:text-warm-300" />
|
||||
</button>
|
||||
<div className="flex gap-2 pointer-events-auto">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setExpanded(true)}
|
||||
className="p-2 bg-white dark:bg-warm-800 rounded shadow-lg"
|
||||
aria-label={t('locationSearch.searchLabel')}
|
||||
>
|
||||
<SearchIcon className="w-5 h-5 text-warm-600 dark:text-warm-300" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={locateUser}
|
||||
disabled={locating}
|
||||
className="p-2 bg-white dark:bg-warm-800 rounded shadow-lg text-warm-600 dark:text-warm-300 hover:text-teal-600 dark:hover:text-teal-400 disabled:opacity-50"
|
||||
aria-label={t('locationSearch.locateMe')}
|
||||
>
|
||||
<LocateIcon className={`w-5 h-5 ${locating ? 'animate-pulse' : ''}`} />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -129,12 +187,22 @@ export default function LocationSearch({
|
|||
search={search}
|
||||
onSelect={selectResult}
|
||||
loading={loading}
|
||||
placeholder="Search places or postcodes..."
|
||||
placeholder={t('locationSearch.placeholder')}
|
||||
size="sm"
|
||||
inputClassName="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)}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={locateUser}
|
||||
disabled={locating}
|
||||
className="p-2 mr-0.5 rounded hover:bg-warm-100 dark:hover:bg-warm-700 text-warm-400 dark:text-warm-500 hover:text-teal-600 dark:hover:text-teal-400 disabled:opacity-50"
|
||||
aria-label={t('locationSearch.locateMe')}
|
||||
title={t('locationSearch.locateMe')}
|
||||
>
|
||||
<LocateIcon className={`w-4 h-4 ${locating ? 'animate-pulse' : ''}`} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue