all repos — caroster @ 1612fe42101ebc0ab86492e9e6c0ceed14c878d1

[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      disableClearable
113      autoComplete
114      getOptionLabel={option => option?.place_name}
115      options={options}
116      defaultValue={previousOption}
117      filterOptions={x => x}
118      noOptionsText={t('autocomplete.noMatch')}
119      onChange={onChange}
120      onInputChange={updateOptions}
121      disabled={disabled}
122      renderInput={params => (
123        <TextField
124          label={label}
125          multiline
126          maxRows={4}
127          helperText={MAPBOX_CONFIGURED && getHelperText()}
128          FormHelperTextProps={{sx: {color: 'warning.main'}}}
129          InputProps={{
130            type: 'search',
131            endAdornment: (
132              <InputAdornment position="end" sx={{mr: -0.5}}>
133                <PlaceOutlinedIcon />
134              </InputAdornment>
135            ),
136          }}
137          {...params}
138          {...textFieldProps}
139        />
140      )}
141      renderOption={({key, ...props}, option) => {
142        if (option.previous) return null;
143
144        return (
145          <ListItem key={key || option.id || 'text'} {...props}>
146            <ListItemText
147              primary={option.place_name}
148              secondary={!option.center && t`placeInput.item.noCoordinates`}
149              secondaryTypographyProps={{
150                color: option.center ? 'inherit' : 'warning.main',
151              }}
152            />
153          </ListItem>
154        );
155      }}
156    />
157  );
158};
159
160export default PlaceInput;