all repos — caroster @ 392f026c797ca85247bf71cdef9b86c274532e60

[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 'react-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 previousOption = place ? {place_name: place, previous: true} : null;
 47
 48  const [options, setOptions] = useState([] as Array<any>);
 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 (!mapboxAvailable) return t`placeInput.mapboxUnavailable`;
102    else if (noCoordinates) return t`placeInput.noCoordinates`;
103  };
104
105  const handleBlur = e => {
106    onSelect({
107      place: e.target.value,
108      latitude,
109      longitude,
110    });
111  };
112
113  return (
114    <Autocomplete
115      freeSolo
116      disableClearable
117      getOptionLabel={option => option.place_name}
118      options={options}
119      autoComplete
120      defaultValue={previousOption}
121      filterOptions={x => x}
122      noOptionsText={t('autocomplete.noMatch')}
123      onChange={onChange}
124      onInputChange={updateOptions}
125      disabled={disabled}
126      renderInput={params => (
127        <TextField
128          label={label}
129          multiline
130          maxRows={4}
131          helperText={MAPBOX_CONFIGURED && getHelperText()}
132          FormHelperTextProps={{sx: {color: 'warning.main'}}}
133          InputProps={{
134            type: 'search',
135            endAdornment: (
136              <InputAdornment position="end" sx={{mr: -0.5}}>
137                <PlaceOutlinedIcon />
138              </InputAdornment>
139            ),
140          }}
141          {...params}
142          {...textFieldProps}
143          onBlur={handleBlur}
144        />
145      )}
146      renderOption={(props, option) => {
147        if (option.previous) return null;
148
149        return (
150          <ListItem key={option.id || 'text'} {...props}>
151            <ListItemText
152              primary={option.place_name}
153              secondary={!option.center && t`placeInput.item.noCoordinates`}
154              secondaryTypographyProps={{
155                color: option.center ? 'inherit' : 'warning.main',
156              }}
157            />
158          </ListItem>
159        );
160      }}
161    />
162  );
163};
164
165export default PlaceInput;