updates on the build regarding the google map api key removal
This commit is contained in:
193
src/components/nearle_components/AddressAutocomplete.js
Normal file
193
src/components/nearle_components/AddressAutocomplete.js
Normal file
@@ -0,0 +1,193 @@
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import { Autocomplete, TextField, CircularProgress } from '@mui/material';
|
||||
import { debounce } from '@mui/material/utils';
|
||||
|
||||
// Free OSM Nominatim geocoder — replaces Google Places Autocomplete /
|
||||
// react-geocode across the console. No API key required. Nominatim's usage
|
||||
// policy caps the public endpoint at ~1 req/sec, so every caller here is
|
||||
// debounced (500ms) and cancels stale in-flight predictions.
|
||||
const NOMINATIM_SEARCH_URL = 'https://nominatim.openstreetmap.org/search';
|
||||
|
||||
// Builds a Google-Places-shaped `address_components` array (each entry
|
||||
// `{ long_name, short_name, types: [] }`) from Nominatim's flat `address`
|
||||
// object, so existing call sites' `place.address_components.forEach(...)`
|
||||
// type-switch parsing keeps working unchanged.
|
||||
const buildAddressComponents = (addr = {}) => {
|
||||
const components = [];
|
||||
const push = (longName, types) => {
|
||||
if (longName) components.push({ long_name: longName, short_name: longName, types });
|
||||
};
|
||||
push(addr.house_number, ['street_number']);
|
||||
push(addr.road || addr.pedestrian, ['route']);
|
||||
push(addr.suburb || addr.neighbourhood || addr.quarter, ['sublocality_level_1', 'sublocality']);
|
||||
push(addr.city_district || addr.county, ['administrative_area_level_3']);
|
||||
push(addr.city || addr.town || addr.village, ['locality']);
|
||||
push(addr.state, ['administrative_area_level_1']);
|
||||
push(addr.country, ['country']);
|
||||
push(addr.postcode, ['postal_code']);
|
||||
return components;
|
||||
};
|
||||
|
||||
// Shapes a Nominatim result close enough to a Google Places `place` object
|
||||
// (formatted_address, geometry.location.lat()/lng(), address_components)
|
||||
// that call sites need only mechanical edits, not rewrites.
|
||||
export const toPlace = (result) => ({
|
||||
formatted_address: result.display_name,
|
||||
name: result.display_name?.split(',')[0] || '',
|
||||
geometry: {
|
||||
location: {
|
||||
lat: () => parseFloat(result.lat),
|
||||
lng: () => parseFloat(result.lon)
|
||||
}
|
||||
},
|
||||
address_components: buildAddressComponents(result.address)
|
||||
});
|
||||
|
||||
const buildParams = (query, bias) => {
|
||||
const params = new URLSearchParams({ q: query, format: 'json', addressdetails: '1', limit: '5' });
|
||||
if (bias?.lat && bias?.lng) {
|
||||
const d = 0.5; // ~55km box around the operator's zone — soft bias, not a hard filter
|
||||
params.set('viewbox', `${bias.lng - d},${bias.lat + d},${bias.lng + d},${bias.lat - d}`);
|
||||
}
|
||||
return params;
|
||||
};
|
||||
|
||||
// Standalone forward-geocode helper — replaces `Geocode.fromAddress(address)`
|
||||
// for call sites that resolve a plain text address without a predictions
|
||||
// dropdown (e.g. re-geocoding on submit). Returns a `toPlace`-shaped object,
|
||||
// or null if nothing matched.
|
||||
export async function geocodeAddress(address, { bias } = {}) {
|
||||
if (!address) return null;
|
||||
try {
|
||||
const params = buildParams(address, bias);
|
||||
params.set('limit', '1');
|
||||
const res = await fetch(`${NOMINATIM_SEARCH_URL}?${params.toString()}`, { headers: { Accept: 'application/json' } });
|
||||
if (!res.ok) return null;
|
||||
const results = await res.json();
|
||||
if (!results?.length) return null;
|
||||
return toPlace(results[0]);
|
||||
} catch (err) {
|
||||
console.error('geocodeAddress error:', err);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Address search-as-you-type — replaces Google Places Autocomplete
|
||||
// (usePlacesWidget / window.google.maps.places.Autocomplete /
|
||||
// AutocompleteService) across the console. Renders as an MUI Autocomplete
|
||||
// with freeSolo so the operator can still submit free text.
|
||||
//
|
||||
// Props:
|
||||
// value / onChange(text) — controlled free-text input value
|
||||
// onPlaceSelected(place) — fired when a suggestion is chosen; `place`
|
||||
// is shaped like a Google Places `place`
|
||||
// bias { lat, lng } — optional soft bias toward the operator's zone
|
||||
const AddressAutocomplete = ({
|
||||
id,
|
||||
label,
|
||||
placeholder = 'Search address',
|
||||
value,
|
||||
onChange,
|
||||
onPlaceSelected,
|
||||
bias,
|
||||
sx,
|
||||
textFieldSx,
|
||||
fullWidth = true,
|
||||
disabled,
|
||||
inputRef,
|
||||
TextFieldProps = {}
|
||||
}) => {
|
||||
const [inputValue, setInputValue] = useState(value || '');
|
||||
const [options, setOptions] = useState([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (value !== undefined && value !== inputValue) setInputValue(value || '');
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [value]);
|
||||
|
||||
const fetchPredictions = useMemo(
|
||||
() =>
|
||||
debounce((query, bias1, callback) => {
|
||||
if (!query) {
|
||||
callback([]);
|
||||
return;
|
||||
}
|
||||
const params = buildParams(query, bias1);
|
||||
fetch(`${NOMINATIM_SEARCH_URL}?${params.toString()}`, { headers: { Accept: 'application/json' } })
|
||||
.then((res) => (res.ok ? res.json() : []))
|
||||
.then((results) => callback(results || []))
|
||||
.catch(() => callback([]));
|
||||
}, 500),
|
||||
[]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
let active = true;
|
||||
if (!inputValue) {
|
||||
setOptions([]);
|
||||
setLoading(false);
|
||||
return undefined;
|
||||
}
|
||||
setLoading(true);
|
||||
fetchPredictions(inputValue, bias, (results) => {
|
||||
if (!active) return;
|
||||
setLoading(false);
|
||||
setOptions(results);
|
||||
});
|
||||
return () => {
|
||||
active = false;
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [inputValue, bias?.lat, bias?.lng, fetchPredictions]);
|
||||
|
||||
return (
|
||||
<Autocomplete
|
||||
id={id}
|
||||
freeSolo
|
||||
fullWidth={fullWidth}
|
||||
disabled={disabled}
|
||||
sx={sx}
|
||||
options={options}
|
||||
filterOptions={(x) => x}
|
||||
getOptionLabel={(option) => (typeof option === 'string' ? option : option.display_name || '')}
|
||||
isOptionEqualToValue={(option, val) => option.place_id === val?.place_id}
|
||||
inputValue={inputValue}
|
||||
onInputChange={(event, newInputValue) => {
|
||||
setInputValue(newInputValue);
|
||||
onChange?.(newInputValue);
|
||||
}}
|
||||
onChange={(event, selected) => {
|
||||
if (selected && typeof selected !== 'string') {
|
||||
setInputValue(selected.display_name || '');
|
||||
onPlaceSelected?.(toPlace(selected));
|
||||
}
|
||||
}}
|
||||
renderInput={(params) => (
|
||||
<TextField
|
||||
{...params}
|
||||
{...TextFieldProps}
|
||||
inputRef={inputRef}
|
||||
label={label}
|
||||
placeholder={placeholder}
|
||||
sx={textFieldSx}
|
||||
autoComplete="off"
|
||||
InputProps={{
|
||||
...params.InputProps,
|
||||
...TextFieldProps.InputProps,
|
||||
startAdornment: TextFieldProps.InputProps?.startAdornment ?? params.InputProps.startAdornment,
|
||||
endAdornment: (
|
||||
<>
|
||||
{loading ? <CircularProgress color="inherit" size={16} /> : null}
|
||||
{TextFieldProps.InputProps?.endAdornment ?? params.InputProps.endAdornment}
|
||||
</>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddressAutocomplete;
|
||||
Reference in New Issue
Block a user