all repos — caroster @ 1361276a89549d6339e2dda712b9021072c46f7f

[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 ListItem from '@mui/material/ListItem';
  5import ListItemText from '@mui/material/ListItemText';
  6import PlaceOutlinedIcon from '@mui/icons-material/PlaceOutlined';
  7import Autocomplete from '@mui/material/Autocomplete';
  8import {debounce} from '@mui/material/utils';
  9import {SessionToken} from '@mapbox/search-js-core';
 10import {useTranslation} from 'react-i18next';
 11import useLocale from '../../hooks/useLocale';
 12import {MapboxSuggestion} from '../../pages/api/mapbox/searchbox/suggest';
 13import {GeocodedOption} from '../../pages/api/mapbox/searchbox/retrieve';
 14
 15interface Props {
 16  place: string;
 17  latitude?: number;
 18  longitude?: number;
 19  onSelect: ({
 20    latitude,
 21    longitude,
 22    place,
 23  }: {
 24    latitude?: number;
 25    longitude?: number;
 26    place: string;
 27  }) => void;
 28  label?: string;
 29  textFieldProps?: TextFieldProps;
 30  disabled?: boolean;
 31}
 32
 33type Option = MapboxSuggestion | {name: String; previous?: Boolean};
 34
 35const MAPBOX_CONFIGURED = process.env['MAPBOX_CONFIGURED'] || false;
 36
 37const PlaceInput = ({
 38  latitude,
 39  longitude,
 40  place = '',
 41  onSelect,
 42  label,
 43  textFieldProps,
 44  disabled,
 45}: Props) => {
 46  const {t} = useTranslation();
 47  const {locale} = useLocale();
 48  const [mapboxAvailable, setMapboxAvailable] = useState(MAPBOX_CONFIGURED);
 49  const [noCoordinates, setNoCoordinates] = useState(!latitude || !longitude);
 50  const previousOption = place ? {name: place, previous: true} : null;
 51  const sessionToken = new SessionToken();
 52
 53  const [options, setOptions] = useState([] as Array<Option>);
 54
 55  const getOptionDecorators = option => {
 56    if (option.mapbox_id) {
 57      return {secondary: option.address || option.place_formatted};
 58    } else {
 59      return {
 60        secondary: t`placeInput.item.noCoordinates`,
 61        color: 'warning.main',
 62      };
 63    }
 64  };
 65
 66  const onChange = async (e, selectedOption) => {
 67    if (selectedOption.mapbox_id) {
 68      const geocodedFeature: GeocodedOption = await fetch(
 69        '/api/mapbox/searchbox/retrieve?' +
 70          new URLSearchParams({
 71            id: selectedOption.mapbox_id,
 72            sessionToken: String(sessionToken),
 73            locale,
 74          })
 75      ).then(response => response.json());
 76      const {longitude, latitude} = geocodedFeature.coordinates;
 77      setNoCoordinates(false);
 78      onSelect({
 79        place: geocodedFeature.name,
 80        latitude,
 81        longitude,
 82      });
 83    } else {
 84      setNoCoordinates(true);
 85      onSelect({
 86        place: selectedOption.name,
 87        latitude: null,
 88        longitude: null,
 89      });
 90    }
 91  };
 92
 93  const updateOptions = debounce(async (e, search: string) => {
 94    if (search !== '') {
 95      try {
 96        await fetch(
 97          '/api/mapbox/searchbox/suggest?' +
 98            new URLSearchParams({
 99              search,
100              sessionToken,
101              locale,
102            })
103        )
104          .then(response => response.json())
105          .then(suggestions => setOptions([{name: search}, ...suggestions]));
106      } catch (err) {
107        console.warn(err);
108        setMapboxAvailable(false);
109      }
110    }
111  }, 400);
112
113  const getHelperText = () => {
114    if (!mapboxAvailable) return t`placeInput.mapboxUnavailable`;
115    else if (noCoordinates) return t`placeInput.noCoordinates`;
116  };
117
118  const handleBlur = e => {
119    onSelect({
120      place: e.target.value,
121      latitude,
122      longitude,
123    });
124  };
125
126  return (
127    <Autocomplete
128      freeSolo
129      disableClearable
130      getOptionLabel={option => option?.name || place}
131      options={options}
132      defaultValue={previousOption}
133      autoComplete
134      filterOptions={x => x}
135      noOptionsText={t('autocomplete.noMatch')}
136      onChange={onChange}
137      onInputChange={updateOptions}
138      disabled={disabled}
139      renderInput={params => (
140        <TextField
141          label={label}
142          multiline
143          maxRows={4}
144          helperText={MAPBOX_CONFIGURED && getHelperText()}
145          FormHelperTextProps={{sx: {color: 'warning.main'}}}
146          InputProps={{
147            type: 'search',
148            endAdornment: (
149              <InputAdornment position="end" sx={{mr: -0.5}}>
150                <PlaceOutlinedIcon />
151              </InputAdornment>
152            ),
153          }}
154          {...params}
155          {...textFieldProps}
156          onBlur={handleBlur}
157        />
158      )}
159      renderOption={(props, option) => {
160        const {color, secondary} = getOptionDecorators(option);
161        if (option.previous) return null;
162        return (
163          <ListItem key={option.mapbox_id || 'text'} {...props}>
164            <ListItemText
165              primary={option.name}
166              secondary={secondary}
167              secondaryTypographyProps={{color}}
168            />
169          </ListItem>
170        );
171      }}
172    />
173  );
174};
175
176export default PlaceInput;