all repos — caroster @ d2e622c85d686fafcb7ba38d2d47b959f085de8c

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