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