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 disabled?: boolean;
27}
28
29const MAPBOX_CONFIGURED = process.env['MAPBOX_CONFIGURED'] || false;
30
31const PlaceInput = ({
32 place = '',
33 latitude,
34 longitude,
35 onSelect,
36 label,
37 textFieldProps,
38 disabled,
39}: Props) => {
40 const {t} = useTranslation();
41 const {locale} = useLocale();
42 const [mapboxAvailable, setMapboxAvailable] = useState(MAPBOX_CONFIGURED);
43 const [noCoordinates, setNoCoordinates] = useState(!latitude || !longitude);
44 const previousOption = place ? {place_name: place, previous: true} : null;
45
46 const [options, setOptions] = useState([] as Array<any>);
47 const onChange = async (e, selectedOption) => {
48 if (selectedOption.previous) {
49 setNoCoordinates(!latitude || !longitude);
50 onSelect({
51 place,
52 latitude,
53 longitude,
54 });
55 } else if (selectedOption.center) {
56 const [optionLongitude, optionLatitude] = selectedOption.center;
57 setNoCoordinates(false);
58 onSelect({
59 place: selectedOption.place_name,
60 latitude: optionLatitude,
61 longitude: optionLongitude,
62 });
63 } else {
64 setNoCoordinates(true);
65 onSelect({
66 place: selectedOption.place_name,
67 latitude: null,
68 longitude: null,
69 });
70 }
71 };
72
73 const updateOptions = debounce(async (e, search: string) => {
74 if (search !== '') {
75 getPlacesSuggestions({search, proximity: 'ip', locale}).then(
76 suggestions => {
77 let defaultOptions = [];
78 if (previousOption) {
79 defaultOptions = [previousOption];
80 }
81 if (search && search !== previousOption?.place_name) {
82 defaultOptions = [...defaultOptions, {place_name: search}];
83 }
84 if (suggestions?.length >= 1) {
85 setMapboxAvailable(true);
86 const suggestionsWithoutCopies = suggestions.filter(
87 ({place_name}) =>
88 place_name !== search &&
89 place_name !== previousOption?.place_name
90 );
91 const uniqueOptions = [
92 ...defaultOptions,
93 ...suggestionsWithoutCopies,
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) return t`placeInput.mapboxUnavailable`;
107 else if (noCoordinates) return t`placeInput.noCoordinates`;
108 };
109
110 const handleBlur = e => {
111 onSelect({
112 place: e.target.value,
113 latitude,
114 longitude,
115 });
116 };
117
118 return (
119 <Autocomplete
120 freeSolo
121 disableClearable
122 getOptionLabel={option => option.place_name}
123 options={options}
124 autoComplete
125 defaultValue={previousOption}
126 filterOptions={x => x}
127 noOptionsText={t('autocomplete.noMatch')}
128 onChange={onChange}
129 onInputChange={updateOptions}
130 disabled={disabled}
131 renderInput={params => (
132 <TextField
133 label={label}
134 multiline
135 maxRows={4}
136 helperText={MAPBOX_CONFIGURED && getHelperText()}
137 FormHelperTextProps={{sx: {color: 'warning.main'}}}
138 InputProps={{
139 endAdornment: (
140 <InputAdornment position="end" sx={{mr: -0.5}}>
141 <PlaceOutlinedIcon />
142 </InputAdornment>
143 ),
144 }}
145 {...params}
146 {...textFieldProps}
147 onBlur={handleBlur}
148 />
149 )}
150 renderOption={(props, option) => {
151 return <li {...props}>{option.place_name}</li>;
152 }}
153 />
154 );
155};
156
157export default PlaceInput;