all repos — caroster @ v8.0

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

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      size="small"
122      renderInput={params => (
123        <TextField
124          label={label}
125          multiline
126          maxRows={4}
127          helperText={MAPBOX_CONFIGURED && getHelperText()}
128          {...textFieldProps}
129          slotProps={{
130            formHelperText: {
131              sx: {color: 'warning.main'},
132            },
133            input: {
134              style: {paddingRight: 0},
135              ...params.InputProps,
136              ...(textFieldProps?.slotProps?.input || {
137                paddingRight: 0,
138                endAdornment: (
139                  <InputAdornment position="end">
140                    <PlaceOutlinedIcon />
141                  </InputAdornment>
142                ),
143              }),
144            },
145          }}
146          {...params}
147        />
148      )}
149      renderOption={({key, ...props}, option) => {
150        if (option.previous) return null;
151
152        return (
153          <ListItem key={key || option.id || 'text'} {...props}>
154            <ListItemText
155              primary={option?.place_name || ''}
156              secondary={!option.center && t`placeInput.item.noCoordinates`}
157              secondaryTypographyProps={{
158                color: option.center ? 'inherit' : 'warning.main',
159              }}
160            />
161          </ListItem>
162        );
163      }}
164    />
165  );
166};
167
168export default PlaceInput;