updates on the ui changes and changed into doormile
This commit is contained in:
197
src/components/doormile/AddressAutocomplete.jsx
Normal file
197
src/components/doormile/AddressAutocomplete.jsx
Normal file
@@ -0,0 +1,197 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
const useDebouncedCallback = (callback, delay) => { const timeoutRef = React.useRef(null); return React.useCallback((...args) => { if (timeoutRef.current) { clearTimeout(timeoutRef.current); } timeoutRef.current = setTimeout(() => { callback(...args); }, delay); }, [callback, delay]); };
|
||||
|
||||
const NOMINATIM_SEARCH_URL = 'https://nominatim.openstreetmap.org/search';
|
||||
|
||||
const toPlace = (nom) => {
|
||||
const c = nom.address || {};
|
||||
const addr = [
|
||||
{ long_name: c.house_number || '', short_name: c.house_number || '', types: ['street_number'] },
|
||||
{ long_name: c.road || '', short_name: c.road || '', types: ['route'] },
|
||||
{ long_name: c.city || c.town || c.village || '', short_name: c.city || c.town || c.village || '', types: ['locality', 'political'] },
|
||||
{ long_name: c.county || c.state_district || '', short_name: c.county || c.state_district || '', types: ['administrative_area_level_2', 'political'] },
|
||||
{ long_name: c.state || '', short_name: c.state || '', types: ['administrative_area_level_1', 'political'] },
|
||||
{ long_name: c.country || '', short_name: c.country_code?.toUpperCase() || '', types: ['country', 'political'] },
|
||||
{ long_name: c.postcode || '', short_name: c.postcode || '', types: ['postal_code'] }
|
||||
].filter((p) => p.long_name);
|
||||
|
||||
return {
|
||||
address_components: addr,
|
||||
formatted_address: nom.display_name,
|
||||
geometry: {
|
||||
location: {
|
||||
lat: () => Number(nom.lat),
|
||||
lng: () => Number(nom.lon)
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const AddressAutocomplete = ({
|
||||
id,
|
||||
label,
|
||||
placeholder,
|
||||
value,
|
||||
onChange,
|
||||
onPlaceSelected,
|
||||
bias,
|
||||
fullWidth = true,
|
||||
disabled = false,
|
||||
status
|
||||
}) => {
|
||||
const [inputValue, setInputValue] = useState(value || '');
|
||||
const [options, setOptions] = useState([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const wrapRef = useRef(null);
|
||||
const abortCtrl = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
setInputValue(value || '');
|
||||
}, [value]);
|
||||
|
||||
const biasKey = bias ? `${bias.lat},${bias.lng}` : '';
|
||||
|
||||
const runSearch = useDebouncedCallback(async (query) => {
|
||||
if (!query || query.length < 3) {
|
||||
setOptions([]);
|
||||
setIsOpen(false);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (abortCtrl.current) abortCtrl.current.abort();
|
||||
abortCtrl.current = new AbortController();
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
const url = new URL(NOMINATIM_SEARCH_URL);
|
||||
url.searchParams.set('q', query);
|
||||
url.searchParams.set('format', 'jsonv2');
|
||||
url.searchParams.set('addressdetails', '1');
|
||||
url.searchParams.set('limit', '5');
|
||||
|
||||
if (bias && bias.lat && bias.lng) {
|
||||
url.searchParams.set('lat', bias.lat);
|
||||
url.searchParams.set('lon', bias.lng);
|
||||
}
|
||||
|
||||
const res = await fetch(url.toString(), {
|
||||
headers: { 'Accept-Language': 'en-GB,en;q=0.9' },
|
||||
signal: abortCtrl.current.signal
|
||||
});
|
||||
|
||||
if (!res.ok) throw new Error('Search failed');
|
||||
const data = await res.json();
|
||||
setOptions(data || []);
|
||||
setIsOpen((data || []).length > 0);
|
||||
} catch (e) {
|
||||
if (e.name !== 'AbortError') console.error('Nominatim search error:', e);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, 500);
|
||||
|
||||
useEffect(() => {
|
||||
if (inputValue && inputValue !== value) runSearch(inputValue);
|
||||
}, [inputValue, biasKey]);
|
||||
|
||||
useEffect(() => {
|
||||
const onDocDown = (e) => {
|
||||
if (wrapRef.current && !wrapRef.current.contains(e.target)) setIsOpen(false);
|
||||
};
|
||||
document.addEventListener('mousedown', onDocDown);
|
||||
return () => document.removeEventListener('mousedown', onDocDown);
|
||||
}, []);
|
||||
|
||||
const pick = (option) => {
|
||||
setInputValue(option.display_name || '');
|
||||
onChange?.(option.display_name || '');
|
||||
onPlaceSelected?.(toPlace(option));
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div ref={wrapRef} className="relative" style={{ width: fullWidth ? '100%' : undefined }}>
|
||||
{label && <label htmlFor={id} className="block text-sm font-medium text-gray-700 mb-1">{label}</label>}
|
||||
<div className="relative">
|
||||
<input
|
||||
id={id}
|
||||
type="text"
|
||||
placeholder={placeholder}
|
||||
value={inputValue}
|
||||
disabled={disabled}
|
||||
onChange={(e) => {
|
||||
setInputValue(e.target.value);
|
||||
onChange?.(e.target.value);
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Escape') setIsOpen(false);
|
||||
}}
|
||||
className="flex h-10 w-full rounded-md border border-gray-300 bg-transparent px-3 py-2 text-sm placeholder:text-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-400 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
/>
|
||||
{loading && (
|
||||
<span className="absolute inset-y-0 right-0 flex items-center pr-3 pointer-events-none">
|
||||
<span className="w-4 h-4 rounded-full border-2 border-t-2 border-gray-200 border-t-blue-500 animate-spin"></span>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isOpen && options.length > 0 && (
|
||||
<ul className="absolute z-50 w-full mt-1 bg-white border border-gray-200 rounded-md shadow-lg max-h-60 overflow-y-auto" role="listbox">
|
||||
{options.map((option) => (
|
||||
<li key={option.place_id}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => pick(option)}
|
||||
className="w-full text-left px-3 py-2 text-sm hover:bg-gray-100 focus:bg-gray-100 focus:outline-none"
|
||||
>
|
||||
<span className="block truncate">{option.display_name}</span>
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
AddressAutocomplete.propTypes = {
|
||||
id: PropTypes.string,
|
||||
label: PropTypes.string,
|
||||
placeholder: PropTypes.string,
|
||||
value: PropTypes.string,
|
||||
onChange: PropTypes.func,
|
||||
onPlaceSelected: PropTypes.func,
|
||||
bias: PropTypes.object,
|
||||
fullWidth: PropTypes.bool,
|
||||
disabled: PropTypes.bool,
|
||||
status: PropTypes.object
|
||||
};
|
||||
|
||||
export default AddressAutocomplete;
|
||||
|
||||
|
||||
export async function geocodeAddress(address, { bias } = {}) {
|
||||
const url = new URL(NOMINATIM_SEARCH_URL);
|
||||
url.searchParams.set('q', address);
|
||||
url.searchParams.set('format', 'jsonv2');
|
||||
url.searchParams.set('addressdetails', '1');
|
||||
url.searchParams.set('limit', '1');
|
||||
if (bias && bias.lat && bias.lng) {
|
||||
url.searchParams.set('lat', bias.lat);
|
||||
url.searchParams.set('lon', bias.lng);
|
||||
}
|
||||
try {
|
||||
const res = await fetch(url.toString(), { headers: { 'Accept-Language': 'en-GB,en;q=0.9' } });
|
||||
if (!res.ok) throw new Error('Search failed');
|
||||
const data = await res.json();
|
||||
if (data && data.length > 0) return toPlace(data[0]);
|
||||
return null;
|
||||
} catch (err) {
|
||||
console.error('geocodeAddress error:', err);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user