all repos — caroster @ 3ede449ef52ab927e5e9bee37b447b5c86a65723

[Octree] Group carpool to your event https://caroster.io

frontend/containers/NewTravelDialog/index.tsx (view raw)

  1import {useState, forwardRef, useMemo} from 'react';
  2import Dialog from '@mui/material/Dialog';
  3import DialogActions from '@mui/material/DialogActions';
  4import DialogContent from '@mui/material/DialogContent';
  5import DialogTitle from '@mui/material/DialogTitle';
  6import Button from '@mui/material/Button';
  7import Slide from '@mui/material/Slide';
  8import TextField from '@mui/material/TextField';
  9import Slider from '@mui/material/Slider';
 10import Typography from '@mui/material/Typography';
 11import moment from 'moment';
 12import {Box, Divider} from '@mui/material';
 13import {useTheme} from '@mui/material/styles';
 14import {DatePicker} from '@mui/x-date-pickers/DatePicker';
 15import {TimePicker} from '@mui/x-date-pickers/TimePicker';
 16import {useTranslation} from 'react-i18next';
 17import useEventStore from '../../stores/useEventStore';
 18import useActions from './useActions';
 19import FAQLink from './FAQLink';
 20import {VehicleEntity} from '../../generated/graphql';
 21import PlaceInput from '../PlaceInput';
 22
 23interface Props {
 24  selectedVehicle: VehicleEntity;
 25  opened: boolean;
 26  toggle: (opts: {opened: boolean}) => void;
 27}
 28
 29const NewTravelDialog = ({selectedVehicle, opened, toggle}: Props) => {
 30  const {t} = useTranslation();
 31  const theme = useTheme();
 32  const event = useEventStore(s => s.event);
 33  const {createTravel} = useActions({event});
 34
 35  const dateMoment = useMemo(
 36    () => (event?.date ? moment(event.date) : null),
 37    [event?.date]
 38  );
 39
 40  // States
 41  const [name, setName] = useState(selectedVehicle?.attributes.name || '');
 42  const [seats, setSeats] = useState(selectedVehicle?.attributes.seats || 4);
 43  const [meeting, setMeeting] = useState('');
 44  const [meeting_latitude, setMeetingLatitude] = useState(null);
 45  const [meeting_longitude, setMeetingLongitude] = useState(null);
 46  const [date, setDate] = useState(dateMoment);
 47  const [time, setTime] = useState(dateMoment);
 48  const [phone, setPhone] = useState(
 49    selectedVehicle?.attributes.phone_number || ''
 50  );
 51  const [details, setDetails] = useState('');
 52  const [dateError, setDateError] = useState(false);
 53  const [titleError, setTitleError] = useState(false);
 54  const [isTitleEmpty, setIsTitleEmpty] = useState(true);
 55
 56  const canCreate = !!name && !!seats;
 57
 58  const clearState = () => {
 59    setName('');
 60    setSeats(4);
 61    setMeeting('');
 62    setMeetingLatitude(null);
 63    setMeetingLongitude(null);
 64    setDate(moment());
 65    setPhone('');
 66    setDetails('');
 67  };
 68
 69  const onCreate = async e => {
 70    if (e.preventDefault) e.preventDefault();
 71
 72    if (!date) {
 73      setDateError(true);
 74      return;
 75    } else {
 76      setDateError(false);
 77    }
 78
 79    if (!name.trim()) {
 80      setTitleError(true);
 81      return;
 82    } else {
 83      setTitleError(false);
 84    }
 85
 86    const travel = {
 87      meeting,
 88      meeting_latitude,
 89      meeting_longitude,
 90      details,
 91      seats,
 92      vehicleName: name,
 93      phone_number: phone,
 94      departureDate: date ? moment(date).format('YYYY-MM-DD') : '',
 95      departureTime: time ? moment(time).format('HH:mm') : '',
 96      event: event.id,
 97    };
 98    const createVehicle = !selectedVehicle;
 99
100    await createTravel(travel, createVehicle);
101    toggle({opened: false});
102
103    clearState();
104  };
105
106  const halfWidthFieldSx = {
107    margin: `0 ${theme.spacing(1.5)}`,
108    width: `calc(50% - ${theme.spacing(3)})`,
109
110    '& > .MuiFormLabel-root': {
111      textOverflow: 'ellipsis',
112      whiteSpace: 'nowrap',
113      width: '100%',
114      overflow: 'hidden',
115    },
116  };
117
118  const handleTitleChange = e => {
119    const inputValue = e.target.value;
120    setName(inputValue);
121    setIsTitleEmpty(inputValue.trim() === '');
122  };
123
124  return (
125    <Dialog
126      fullWidth
127      maxWidth="xs"
128      open={opened}
129      onClose={() => {
130        toggle({opened: false});
131        clearState();
132      }}
133      TransitionComponent={Transition}
134    >
135      <form onSubmit={onCreate}>
136        <DialogTitle sx={{paddingBottom: 0}}>
137          {t('travel.creation.title')}
138        </DialogTitle>
139        <DialogContent sx={{padding: `${theme.spacing(2)} 0`}}>
140          <Typography
141            sx={{...addSpacing(theme, 1), paddingBottom: theme.spacing(1.5)}}
142          >
143            {t('travel.creation.car.title')}
144          </Typography>
145          <TextField
146            variant="outlined"
147            size="small"
148            sx={{...addSpacing(theme, 1), paddingBottom: theme.spacing(1)}}
149            label={t('travel.creation.name')}
150            fullWidth
151            value={name}
152            onChange={handleTitleChange}
153            name="name"
154            id="NewTravelName"
155            required
156            error={titleError}
157            helperText={
158              isTitleEmpty ? t('travel.creation.travel.titleHelper') : ''
159            }
160            FormHelperTextProps={{sx: {color: 'warning.main'}}}
161          />
162
163          <TextField
164            variant="outlined"
165            size="small"
166            sx={{...addSpacing(theme, 1), paddingBottom: theme.spacing(1)}}
167            label={t('travel.creation.phone')}
168            fullWidth
169            inputProps={{type: 'tel'}}
170            value={phone}
171            onChange={e => setPhone(e.target.value)}
172            name="phone"
173            helperText={
174              <Typography variant="caption">
175                <FAQLink
176                  link={t('travel.creation.phoneHelper.faq')}
177                  text={t('travel.creation.phoneHelper.why')}
178                />
179              </Typography>
180            }
181            id="NewTravelPhone"
182          />
183          <Box sx={addSpacing(theme, 1)}>
184            <Typography variant="caption">
185              {t('travel.creation.seats')}
186            </Typography>
187            <Slider
188              size="small"
189              value={seats}
190              onChange={(e, value) => setSeats(value)}
191              step={1}
192              marks={MARKS}
193              min={1}
194              max={MARKS.length}
195              valueLabelDisplay="auto"
196              id="NewTravelSeats"
197            />
198          </Box>
199          <Divider
200            sx={{
201              margin: `${theme.spacing(2)} 0`,
202            }}
203          />
204          <Typography
205            sx={{...addSpacing(theme, 1), paddingBottom: theme.spacing(1.5)}}
206          >
207            {t('travel.creation.travel.title')}
208          </Typography>
209          <Box sx={addSpacing(theme, 0.5)} pb={1}>
210            <DatePicker
211              slotProps={{
212                textField: {
213                  variant: 'outlined',
214                  size: 'small',
215                  helperText: dateError
216                    ? t('travel.creation.travel.dateHelper')
217                    : '',
218                  error: dateError,
219                  FormHelperTextProps: {sx: {color: 'warning.main'}},
220                  sx: halfWidthFieldSx,
221                },
222              }}
223              format="DD/MM/YYYY"
224              label={t('travel.creation.date')}
225              value={date}
226              onChange={setDate}
227              autoFocus
228            />
229            <TimePicker
230              slotProps={{
231                textField: {
232                  variant: 'outlined',
233                  size: 'small',
234                  helperText: '',
235                  sx: halfWidthFieldSx,
236                },
237              }}
238              label={t('travel.creation.time')}
239              value={time}
240              onChange={setTime}
241              ampm={false}
242              minutesStep={5}
243            />
244          </Box>
245          <PlaceInput
246            label={t('travel.creation.meeting')}
247            textFieldProps={{
248              variant: 'outlined',
249              size: 'small',
250              sx: {...addSpacing(theme, 1), paddingBottom: theme.spacing(1)},
251            }}
252            place={meeting}
253            latitude={meeting_latitude}
254            longitude={meeting_longitude}
255            onSelect={({place, latitude, longitude}) => {
256              setMeeting(place);
257              setMeetingLatitude(latitude);
258              setMeetingLongitude(longitude);
259            }}
260          />
261          <TextField
262            variant="outlined"
263            size="small"
264            sx={{...addSpacing(theme, 1), paddingBottom: theme.spacing(1)}}
265            label={t('travel.creation.notes')}
266            fullWidth
267            multiline
268            maxRows={4}
269            inputProps={{maxLength: 250}}
270            helperText={`${details.length}/250`}
271            value={details}
272            onChange={e => setDetails(e.target.value)}
273            name="details"
274            id="NewTravelDetails"
275          />
276        </DialogContent>
277        <DialogActions
278          sx={{
279            paddingTop: 0,
280          }}
281        >
282          <Button
283            color="primary"
284            id="NewTravelCancel"
285            onClick={() => toggle({opened: false})}
286            tabIndex={-1}
287          >
288            {t('generic.cancel')}
289          </Button>
290          <Button
291            color="primary"
292            variant="contained"
293            type="submit"
294            disabled={!canCreate}
295            aria-disabled={!canCreate}
296            id="NewTravelSubmit"
297          >
298            {t('travel.creation.submit')}
299          </Button>
300        </DialogActions>
301      </form>
302    </Dialog>
303  );
304};
305
306const Transition = forwardRef(function Transition(props, ref) {
307  return <Slide direction="up" ref={ref} {...props} />;
308});
309
310const MARKS = [1, 2, 3, 4, 5, 6, 7, 8].map(value => ({
311  value,
312  label: value,
313}));
314
315const addSpacing = (theme, ratio) => ({
316  margin: `0 ${theme.spacing(3 * ratio)}`,
317  width: `calc(100% - ${theme.spacing(6 * ratio)})`,
318});
319
320export default NewTravelDialog;