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 suggestionsWithoutCopies = suggestions.filter(
85 ({place_name}) =>
86 place_name !== search &&
87 place_name !== previousOption?.place_name
88 );
89 const uniqueOptions = [
90 ...defaultOptions,
91 ...suggestionsWithoutCopies,
92 ];
93 setOptions(uniqueOptions);
94 } else {
95 setMapboxAvailable(false);
96 setOptions(defaultOptions);
97 }
98 }
99 );
100 }
101 }, 400);
102
103 const getHelperText = () => {
104 if (!mapboxAvailable) {
105 return t`placeInput.mapboxUnavailable`;
106 }
107 if (noCoordinates) {
108 return t`placeInput.noCoordinates`;
109 }
110 return null;
111 };
112
113 return (
114 <Autocomplete
115 freeSolo
116 disableClearable
117 getOptionLabel={option => option.place_name}
118 options={options}
119 autoComplete
120 defaultValue={previousOption}
121 filterOptions={x => x}
122 noOptionsText={t('autocomplete.noMatch')}
123 onChange={onChange}
124 onInputChange={updateOptions}
125 renderInput={params => (
126 <TextField
127 label={label}
128 multiline
129 maxRows={4}
130 helperText={MAPBOX_CONFIGURED && getHelperText()}
131 FormHelperTextProps={{sx: {color: 'warning.main'}}}
132 InputProps={{
133 endAdornment: (
134 <InputAdornment position="end" sx={{mr: -0.5}}>
135 <PlaceOutlinedIcon />
136 </InputAdornment>
137 ),
138 }}
139 {...params}
140 {...textFieldProps}
141 />
142 )}
143 renderOption={(props, option) => {
144 return <li {...props}>{option.place_name}</li>;
145 }}
146 />
147 );
148};
149
150export default PlaceInput;