frontend/containers/PlaceInput/index.tsx (view raw)
1import {useState} from 'react';
2import TextField, {TextFieldProps} from '@mui/material/TextField';
3import InputAdornment from '@mui/material/InputAdornment';
4import PlaceOutlinedIcon from '@mui/icons-material/PlaceOutlined';
5import Autocomplete from '@mui/material/Autocomplete';
6import {debounce} from '@mui/material/utils';
7import {useTranslation} from 'react-i18next';
8import useLocale from '../../hooks/useLocale';
9import getPlacesSuggestions from '../../lib/getPlacesSuggestion';
10
11interface Props {
12 place: string;
13 latitude?: number;
14 longitude?: number;
15 onSelect: ({
16 latitude,
17 longitude,
18 place,
19 }: {
20 latitude?: number;
21 longitude?: number;
22 place: string;
23 }) => void;
24 label?: string;
25 textFieldProps?: TextFieldProps;
26}
27
28const MAPBOX_CONFIGURED = process.env['MAPBOX_CONFIGURED'] || false;
29
30const PlaceInput = ({
31 place = '',
32 latitude,
33 longitude,
34 onSelect,
35 label,
36 textFieldProps,
37}: Props) => {
38 const {t} = useTranslation();
39 const {locale} = useLocale();
40 const [mapboxAvailable, setMapboxAvailable] = useState(MAPBOX_CONFIGURED);
41 const [noCoordinates, setNoCoordinates] = useState(!latitude || !longitude);
42 const previousOption = place ? {place_name: place, previous: true} : null;
43
44 const [options, setOptions] = useState([] as Array<any>);
45 const onChange = async (e, selectedOption) => {
46 if (selectedOption.previous) {
47 setNoCoordinates(!latitude || !longitude);
48 onSelect({
49 place,
50 latitude,
51 longitude,
52 });
53 } else if (selectedOption.center) {
54 const [optionLongitude, optionLatitude] = selectedOption.center;
55 setNoCoordinates(false);
56 onSelect({
57 place: selectedOption.place_name,
58 latitude: optionLatitude,
59 longitude: optionLongitude,
60 });
61 } else {
62 setNoCoordinates(true);
63 onSelect({
64 place: selectedOption.place_name,
65 latitude: null,
66 longitude: null,
67 });
68 }
69 };
70
71 const updateOptions = debounce(async (e, search: string) => {
72 if (search !== '') {
73 getPlacesSuggestions({search, proximity: 'ip', locale}).then(
74 suggestions => {
75 let defaultOptions = [];
76 if (previousOption) {
77 defaultOptions = [previousOption];
78 }
79 if (search && search !== previousOption?.place_name) {
80 defaultOptions = [...defaultOptions, {place_name: search}];
81 }
82 if (suggestions?.length >= 1) {
83 setMapboxAvailable(true);
84 const [firstSuggestion, ...otherSuggestions] = suggestions;
85 let uniqueOptions = [...defaultOptions, ...otherSuggestions];
86 if (
87 firstSuggestion.place_name !== search ||
88 firstSuggestion.place_name !== previousOption?.place_name
89 )
90 uniqueOptions = [
91 ...defaultOptions,
92 firstSuggestion,
93 ...otherSuggestions,
94 ];
95 setOptions(uniqueOptions);
96 } else {
97 setMapboxAvailable(false);
98 setOptions(defaultOptions);
99 }
100 }
101 );
102 }
103 }, 400);
104
105 const getHelperText = () => {
106 if (!mapboxAvailable) {
107 return t`placeInput.mapboxUnavailable`;
108 }
109 if (noCoordinates) {
110 return t`placeInput.noCoordinates`;
111 }
112 return null;
113 };
114
115 return (
116 <Autocomplete
117 freeSolo
118 disableClearable
119 getOptionLabel={option => option.place_name}
120 options={options}
121 autoComplete
122 defaultValue={previousOption}
123 filterOptions={x => x}
124 noOptionsText={t('autocomplete.noMatch')}
125 onChange={onChange}
126 onInputChange={updateOptions}
127 renderInput={params => (
128 <TextField
129 label={label}
130 multiline
131 maxRows={4}
132 helperText={MAPBOX_CONFIGURED && getHelperText()}
133 FormHelperTextProps={{sx: {color: 'warning.main'}}}
134 InputProps={{
135 endAdornment: (
136 <InputAdornment position="end" sx={{mr: -0.5}}>
137 <PlaceOutlinedIcon />
138 </InputAdornment>
139 ),
140 }}
141 {...params}
142 {...textFieldProps}
143 />
144 )}
145 renderOption={(props, option) => {
146 return <li {...props}>{option.place_name}</li>;
147 }}
148 />
149 );
150};
151
152export default PlaceInput;