all repos — caroster @ ceda4c783969e70cbca2e5d932085b1d544319dd

[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 (e, 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 (e, search: string) => {
 69    if (search !== '') {
 70      getPlacesSuggestions({search, proximity: 'ip', locale}).then(
 71        suggestions => {
 72          let defaultOptions = [];
 73          if (previousOption) {
 74            defaultOptions = [previousOption];
 75          }
 76          if (search && search !== previousOption?.place_name) {
 77            defaultOptions = [...defaultOptions, {place_name: search}];
 78          }
 79          if (suggestions?.length >= 1) {
 80            setMapboxAvailable(true);
 81            const suggestionsWithoutCopies = suggestions.filter(
 82              ({place_name}) =>
 83                place_name !== search &&
 84                place_name !== previousOption?.place_name
 85            );
 86            const uniqueOptions = [
 87              ...defaultOptions,
 88              ...suggestionsWithoutCopies,
 89            ];
 90            setOptions(uniqueOptions);
 91          } else {
 92            setMapboxAvailable(false);
 93            setOptions(defaultOptions);
 94          }
 95        }
 96      );
 97    }
 98  }, 400);
 99
100  const getHelperText = () => {
101    if (!place) return undefined;
102    else if (!mapboxAvailable) return t`placeInput.mapboxUnavailable`;
103    else if (noCoordinates) return t`placeInput.noCoordinates`;
104  };
105
106  const handleBlur = e => {
107    onSelect({
108      place: e.target.value,
109      latitude,
110      longitude,
111    });
112  };
113
114  return (
115    <Autocomplete
116      freeSolo
117      disableClearable
118      autoComplete
119      getOptionLabel={option => option?.place_name}
120      options={options}
121      defaultValue={previousOption}
122      filterOptions={x => x}
123      noOptionsText={t('autocomplete.noMatch')}
124      onChange={onChange}
125      onInputChange={updateOptions}
126      disabled={disabled}
127      renderInput={params => (
128        <TextField
129          label={label}
130          multiline
131          maxRows={4}
132          helperText={MAPBOX_CONFIGURED && getHelperText()}
133          FormHelperTextProps={{sx: {color: 'warning.main'}}}
134          InputProps={{
135            type: 'search',
136            endAdornment: (
137              <InputAdornment position="end" sx={{mr: -0.5}}>
138                <PlaceOutlinedIcon />
139              </InputAdornment>
140            ),
141          }}
142          {...params}
143          {...textFieldProps}
144          onBlur={handleBlur}
145        />
146      )}
147      renderOption={({key, ...props}, option) => {
148        if (option.previous) return null;
149
150        return (
151          <ListItem key={key || option.id || 'text'} {...props}>
152            <ListItemText
153              primary={option.place_name}
154              secondary={!option.center && t`placeInput.item.noCoordinates`}
155              secondaryTypographyProps={{
156                color: option.center ? 'inherit' : 'warning.main',
157              }}
158            />
159          </ListItem>
160        );
161      }}
162    />
163  );
164};
165
166export default PlaceInput;