all repos — caroster @ fe359e3d0bd28293a74106f4ce391f5127bce55f

[Octree] Group carpool to your event https://caroster.io

⚡️ Improve event address geocoding
#387
Simon Mulquin simon@octree.ch
Thu, 14 Dec 2023 15:12:36 +0000
commit

fe359e3d0bd28293a74106f4ce391f5127bce55f

parent

3e13ed81f9961ed582333d0e9296921db2ed91d9

D frontend/containers/AddressAutofill/index.tsx

@@ -1,110 +0,0 @@

-import {useState} from 'react'; -import TextField, {TextFieldProps} from '@mui/material/TextField'; -import InputAdornment from '@mui/material/InputAdornment'; -import PlaceOutlinedIcon from '@mui/icons-material/PlaceOutlined'; -import Autocomplete from '@mui/material/Autocomplete'; -import {debounce} from '@mui/material/utils'; -import {SessionToken, AddressAutofillSuggestion} from '@mapbox/search-js-core'; -import {useTranslation} from 'react-i18next'; -import useLocale from '../../hooks/useLocale'; - -interface Props { - address: string; - onSelect: ({ - location, - address, - }: { - location: [number, number]; - address: string; - }) => void; - label: string; - textFieldProps?: TextFieldProps; -} - -const AddressAutofill = ({ - address = '', - onSelect, - label, - textFieldProps, -}: Props) => { - const {t} = useTranslation(); - const {locale} = useLocale(); - const [value, setValue] = useState(address); - const sessionToken = new SessionToken(); - - const [options, setOptions] = useState( - [] as Array<AddressAutofillSuggestion> - ); - - const onChange = async (e, selectedOption) => { - const body = await fetch( - '/api/mapbox/autofill/retrieve?' + - new URLSearchParams({ - sessionToken, - locale, - }), - {body: JSON.stringify(selectedOption), method: 'POST'} - ).then(response => response.json()); - setValue(selectedOption); - onSelect({ - address: selectedOption.full_address, - location: body.features[0]?.geometry?.coordinates, - }); - }; - - const updateOptions = debounce(async (e, search: string) => { - if (search !== '') { - await fetch( - '/api/mapbox/autofill/suggest?' + - new URLSearchParams({ - search, - sessionToken, - locale, - }) - ) - .then(response => response.json()) - .then(body => { - setOptions(body.suggestions); - }); - } - }, 400); - - return ( - <Autocomplete - freeSolo - disableClearable - getOptionLabel={option => - option.full_address || option.place_name || address - } - options={options} - autoComplete - value={value} - filterOptions={x => x} - noOptionsText={t('autocomplete.noMatch')} - onChange={onChange} - onInputChange={updateOptions} - renderInput={params => ( - <TextField - label={label} - multiline - maxRows={4} - InputProps={{ - type: 'search', - endAdornment: ( - <InputAdornment position="end" sx={{mr: -0.5}}> - <PlaceOutlinedIcon /> - </InputAdornment> - ), - }} - {...params} - {...textFieldProps} - /> - )} - renderOption={(props, option) => { - return <li {...props}>{option.full_address || option.place_name}</li>; - }} - /> - ); -}; - -export default AddressAutofill;
M frontend/containers/CreateEvent/Step2.tsxfrontend/containers/CreateEvent/Step2.tsx

@@ -9,7 +9,7 @@ import {useRouter} from 'next/router';

import {DatePicker} from '@mui/x-date-pickers/DatePicker'; import {useTranslation} from 'react-i18next'; import useToastStore from '../../stores/useToastStore'; -import AddressAutofill from '../AddressAutofill'; +import PlaceInput from '../PlaceInput'; const Step2 = ({event, addToEvent, createEvent}) => { const {t} = useTranslation();

@@ -53,12 +53,12 @@ label={t('event.creation.date')}

value={date} onChange={setDate} /> - <AddressAutofill + <PlaceInput label={t('event.creation.address')} textFieldProps={{sx: {mt: 2}}} - address={address} - onSelect={({location, address}) => { - setAddress(address); + place={address} + onSelect={({location, place}) => { + setAddress(place); setLatitude(location[1]); setLongitude(location[0]); }}
M frontend/containers/EventPopup/index.tsxfrontend/containers/EventPopup/index.tsx

@@ -57,7 +57,7 @@ )}

{!!event.description && ( <Box sx={{marginTop: 2}}> <Typography variant="overline" color="GrayText"> - {t('event.fields.desciption')} + {t('event.fields.description')} </Typography> <Typography variant="body1">{event.description}</Typography> </Box>
M frontend/containers/NewTravelDialog/index.tsxfrontend/containers/NewTravelDialog/index.tsx

@@ -18,7 +18,7 @@ import useEventStore from '../../stores/useEventStore';

import useActions from './useActions'; import FAQLink from './FAQLink'; import {Vehicle} from '../../generated/graphql'; -import AddressAutofill from '../AddressAutofill'; +import PlaceInput from '../PlaceInput'; interface Props { context: {

@@ -206,16 +206,16 @@ ampm={false}

minutesStep={5} /> </Box> - <AddressAutofill + <PlaceInput label={t('travel.creation.meeting')} textFieldProps={{ variant: 'outlined', size: 'small', sx: {...addSpacing(theme, 1), paddingBottom: theme.spacing(1)}, }} - address={meeting} - onSelect={({location, address}) => { - setMeeting(address); + place={meeting} + onSelect={({location, place}) => { + setMeeting(place); setMeetingLatitude(location[1]); setMeetingLongitude(location[0]); }}
A frontend/containers/PlaceInput/index.tsx

@@ -0,0 +1,85 @@

+import {useState} from 'react'; +import TextField, {TextFieldProps} from '@mui/material/TextField'; +import InputAdornment from '@mui/material/InputAdornment'; +import PlaceOutlinedIcon from '@mui/icons-material/PlaceOutlined'; +import Autocomplete from '@mui/material/Autocomplete'; +import {debounce} from '@mui/material/utils'; +import {useTranslation} from 'react-i18next'; +import useLocale from '../../hooks/useLocale'; +import getPlacesSuggestions from '../../lib/getPlacesSuggestion'; + +interface Props { + place: string; + onSelect: ({ + location, + place, + }: { + location: [number, number]; + place: string; + }) => void; + label?: string; + textFieldProps?: TextFieldProps; +} + +const PlaceInput = ({ + place = '', + onSelect, + label, + textFieldProps, +}: Props) => { + const {t} = useTranslation(); + const {locale} = useLocale(); + + const [options, setOptions] = useState([] as Array<any>); + + const onChange = async (e, selectedOption) => { + onSelect({ + place: selectedOption.place_name, + location: selectedOption.center, + }); + }; + + const updateOptions = debounce(async (e, search: string) => { + if (search !== '') { + getPlacesSuggestions({search, proximity: 'ip', locale}).then(suggestions => { + setOptions(suggestions); + }); + } + }, 400); + + return ( + <Autocomplete + freeSolo + disableClearable + getOptionLabel={option => option.place_name} + options={options} + autoComplete + defaultValue={{place_name: place}} + filterOptions={x => x} + noOptionsText={t('autocomplete.noMatch')} + onChange={onChange} + onInputChange={updateOptions} + renderInput={params => ( + <TextField + label={label} + multiline + maxRows={4} + InputProps={{ + endAdornment: ( + <InputAdornment position="end" sx={{mr: -0.5}}> + <PlaceOutlinedIcon /> + </InputAdornment> + ), + }} + {...params} + {...textFieldProps} + /> + )} + renderOption={(props, option) => { + return <li {...props}>{option.place_name}</li>; + }} + /> + ); +}; + +export default PlaceInput;
M frontend/containers/Travel/HeaderEditing.tsxfrontend/containers/Travel/HeaderEditing.tsx

@@ -11,7 +11,7 @@ import {useTranslation} from 'react-i18next';

import RemoveDialog from '../RemoveDialog'; import useActions from './useActions'; import Box from '@mui/material/Box'; -import AddressAutofill from '../AddressAutofill'; +import PlaceInput from '../PlaceInput'; const HeaderEditing = ({travel, toggleEditing}) => { const {t} = useTranslation();

@@ -115,12 +115,12 @@ onChange={e => setPhone(e.target.value)}

name="phone" id="EditTravelPhone" /> - <AddressAutofill + <PlaceInput label={t('travel.creation.meeting')} textFieldProps={{sx: {pb: 2}}} - address={meeting} - onSelect={({location, address}) => { - setMeeting(address); + place={meeting} + onSelect={({location, place}) => { + setMeeting(place); setMeetingLatitude(location[1]); setMeetingLongitude(location[0]); }}
M frontend/containers/TravelColumns/index.tsxfrontend/containers/TravelColumns/index.tsx

@@ -70,7 +70,7 @@ );

const {latitude, longitude} = event; const showMap = latitude && longitude; - let coordsString = ''; + let coordsString = `${latitude}${longitude}`; const markers = travels.reduce((markers, travel) => { const { attributes: {meeting_latitude, meeting_longitude},
A frontend/lib/getPlacesSuggestion.ts

@@ -0,0 +1,31 @@

+interface Params { + search: string; + proximity?: string; + locale: string; +} + +export interface PlaceSuggestion { + place_name: string; + center: [number, number]; +} + +const getPlacesSuggestions = async ({ + search, + proximity, + locale, +}: Params): Promise<Array<PlaceSuggestion>> => { + const suggestions = await fetch( + '/api/mapbox/places?' + + new URLSearchParams({ + search, + proximity, + locale, + }) + ) + .then(res => res.json()) + .catch(console.error); + + return suggestions; +}; + +export default getPlacesSuggestions;
M frontend/middleware.tsfrontend/middleware.ts

@@ -57,7 +57,7 @@ .then(async response => {

const {data} = await response.json(); return data?.me?.profile?.lang; }) - .catch(console.log); + .catch(console.error); }; const getBrowserPreferredSupportedLanguage = req => {
D frontend/pages/api/mapbox/autofill/retrieve.ts

@@ -1,35 +0,0 @@

-import type {NextApiRequest, NextApiResponse} from 'next'; -import { - AddressAutofillCore, - AddressAutofillFeatureSuggestion, -} from '@mapbox/search-js-core'; - -const {MAPBOX_TOKEN} = process.env; - -export default async function handler( - req: NextApiRequest, - res: NextApiResponse<{features: Array<AddressAutofillFeatureSuggestion>}> -) { - const {locale, sessionToken} = req.query; - const suggestion = JSON.parse(req.body); - - if (Array.isArray(locale) || Array.isArray(sessionToken)) { - res.status(422); - return; - } - - const autofill = new AddressAutofillCore({ - accessToken: MAPBOX_TOKEN || '', - language: locale, - }); - - await autofill.suggest(suggestion.original_search_text, {sessionToken}); - const {features} = await autofill.retrieve(suggestion, {sessionToken}); - if (features.length === 0) { - res.status(404); - return; - } - - res.send({features}); - return; -}
D frontend/pages/api/mapbox/autofill/suggest.ts

@@ -1,29 +0,0 @@

-import type {NextApiRequest, NextApiResponse} from 'next'; -import {AddressAutofillCore, AddressAutofillSuggestion} from '@mapbox/search-js-core'; - -const {MAPBOX_TOKEN} = process.env; - -export default async function handler( - req: NextApiRequest, - res: NextApiResponse<{ - suggestions: Array<AddressAutofillSuggestion> - }> -) { - const {locale, search, sessionToken} = req.query; - - if (Array.isArray(locale) || Array.isArray(search) || Array.isArray(sessionToken)) { - res.status(422); - return - } - - const autofill = new AddressAutofillCore({accessToken: MAPBOX_TOKEN || '', language: locale}) - const {suggestions} = await autofill.suggest(search, {sessionToken}) - - if (suggestions.length === 0) { - res.status(404); - return; - } - - res.send({suggestions}) - return; -}
A frontend/pages/api/mapbox/places.ts

@@ -0,0 +1,30 @@

+import type {NextApiRequest, NextApiResponse} from 'next'; + +type ResponseData = Array<{place_name: string; center: [number, number]}>; + +const {MAPBOX_TOKEN, MAPBOX_URL} = process.env; + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse<ResponseData> +) { + const {search, proximity = 'ip', locale} = req.query; + if (!search) return res.status(400); + if (!MAPBOX_TOKEN) return res.status(500); + + const url = `${MAPBOX_URL}geocoding/v5/mapbox.places/${search}.json?proximity=${proximity}&access_token=${MAPBOX_TOKEN}&language=${locale}`; + + const mapBoxResult = await fetch(url) + .then(response => response.json()) + .catch(error => { + console.error(error); + res.send(500); + }); + + if (mapBoxResult?.features) { + res.send(mapBoxResult.features); + res.status(200); + } + + return; +}
M frontend/pages/e/[uuid]/details.tsxfrontend/pages/e/[uuid]/details.tsx

@@ -24,7 +24,7 @@ import {

EventByUuidDocument, useUpdateEventMutation, } from '../../../generated/graphql'; -import AddressAutofill from '../../../containers/AddressAutofill'; +import PlaceInput from '../../../containers/PlaceInput'; interface Props { eventUUID: string;

@@ -187,12 +187,11 @@ <Typography variant="overline">

{t('event.fields.address')} </Typography> {isEditing ? ( - <AddressAutofill - label={t('event.creation.address')} - address={event.address} - onSelect={({location, address}) => { + <PlaceInput + place={event.address} + onSelect={({location, place}) => { setEventUpdate({ - address, + address: place, latitude: location[1], longitude: location[0], });