all repos — caroster @ ae6f0621a8c844a589741cf29191ddf231c449c4

[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, {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      departure: formatDate(date, time),
 95      event: event.id,
 96    };
 97    const createVehicle = !selectedVehicle;
 98
 99    await createTravel(travel, createVehicle);
100    toggle({opened: false});
101
102    clearState();
103  };
104
105  const halfWidthFieldSx = {
106    margin: `0 ${theme.spacing(1.5)}`,
107    width: `calc(50% - ${theme.spacing(3)})`,
108
109    '& > .MuiFormLabel-root': {
110      textOverflow: 'ellipsis',
111      whiteSpace: 'nowrap',
112      width: '100%',
113      overflow: 'hidden',
114    },
115  };
116
117  const handleTitleChange = e => {
118    const inputValue = e.target.value;
119    setName(inputValue);
120    setIsTitleEmpty(inputValue.trim() === '');
121  };
122
123  return (
124    <Dialog
125      fullWidth
126      maxWidth="xs"
127      open={opened}
128      onClose={() => {
129        toggle({opened: false});
130        clearState();
131      }}
132      TransitionComponent={Transition}
133    >
134      <form onSubmit={onCreate}>
135        <DialogTitle sx={{paddingBottom: 0}}>
136          {t('travel.creation.title')}
137        </DialogTitle>
138        <DialogContent sx={{padding: `${theme.spacing(2)} 0`}}>
139          <Typography
140            sx={{...addSpacing(theme, 1), paddingBottom: theme.spacing(1.5)}}
141          >
142            {t('travel.creation.car.title')}
143          </Typography>
144          <TextField
145            variant="outlined"
146            size="small"
147            sx={{...addSpacing(theme, 1), paddingBottom: theme.spacing(1)}}
148            label={t('travel.creation.name')}
149            fullWidth
150            value={name}
151            onChange={handleTitleChange}
152            name="name"
153            id="NewTravelName"
154            required
155            error={titleError}
156            helperText={
157              isTitleEmpty ? t('travel.creation.travel.titleHelper') : ''
158            }
159            FormHelperTextProps={{sx: {color: 'warning.main'}}}
160          />
161
162          <TextField
163            variant="outlined"
164            size="small"
165            sx={{...addSpacing(theme, 1), paddingBottom: theme.spacing(1)}}
166            label={t('travel.creation.phone')}
167            fullWidth
168            inputProps={{type: 'tel'}}
169            value={phone}
170            onChange={e => setPhone(e.target.value)}
171            name="phone"
172            helperText={
173              <Typography variant="caption">
174                <FAQLink
175                  link={t('travel.creation.phoneHelper.faq')}
176                  text={t('travel.creation.phoneHelper.why')}
177                />
178              </Typography>
179            }
180            id="NewTravelPhone"
181          />
182          <Box sx={addSpacing(theme, 1)}>
183            <Typography variant="caption">
184              {t('travel.creation.seats')}
185            </Typography>
186            <Slider
187              size="small"
188              value={seats}
189              onChange={(e, value) => setSeats(value)}
190              step={1}
191              marks={MARKS}
192              min={1}
193              max={MARKS.length}
194              valueLabelDisplay="auto"
195              id="NewTravelSeats"
196            />
197          </Box>
198          <Divider
199            sx={{
200              margin: `${theme.spacing(2)} 0`,
201            }}
202          />
203          <Typography
204            sx={{...addSpacing(theme, 1), paddingBottom: theme.spacing(1.5)}}
205          >
206            {t('travel.creation.travel.title')}
207          </Typography>
208          <Box sx={addSpacing(theme, 0.5)} pb={1}>
209            <DatePicker
210              slotProps={{
211                textField: {
212                  variant: 'outlined',
213                  size: 'small',
214                  helperText: dateError
215                    ? t('travel.creation.travel.dateHelper')
216                    : '',
217                  error: dateError,
218                  FormHelperTextProps: {sx: {color: 'warning.main'}},
219                  sx: halfWidthFieldSx,
220                },
221              }}
222              format="DD/MM/YYYY"
223              label={t('travel.creation.date')}
224              value={date}
225              onChange={setDate}
226              autoFocus
227            />
228            <TimePicker
229              slotProps={{
230                textField: {
231                  variant: 'outlined',
232                  size: 'small',
233                  helperText: '',
234                  sx: halfWidthFieldSx,
235                },
236              }}
237              label={t('travel.creation.time')}
238              value={time}
239              onChange={setTime}
240              ampm={false}
241              minutesStep={5}
242            />
243          </Box>
244          <PlaceInput
245            label={t('travel.creation.meeting')}
246            textFieldProps={{
247              variant: 'outlined',
248              size: 'small',
249              sx: {...addSpacing(theme, 1), paddingBottom: theme.spacing(1)},
250            }}
251            place={meeting}
252            latitude={meeting_latitude}
253            longitude={meeting_longitude}
254            onSelect={({place, latitude, longitude}) => {
255              setMeeting(place);
256              setMeetingLatitude(latitude);
257              setMeetingLongitude(longitude);
258            }}
259          />
260          <TextField
261            variant="outlined"
262            size="small"
263            sx={{...addSpacing(theme, 1), paddingBottom: theme.spacing(1)}}
264            label={t('travel.creation.notes')}
265            fullWidth
266            multiline
267            maxRows={4}
268            inputProps={{maxLength: 250}}
269            helperText={`${details.length}/250`}
270            value={details}
271            onChange={e => setDetails(e.target.value)}
272            name="details"
273            id="NewTravelDetails"
274          />
275        </DialogContent>
276        <DialogActions
277          sx={{
278            paddingTop: 0,
279          }}
280        >
281          <Button
282            color="primary"
283            id="NewTravelCancel"
284            onClick={() => toggle({opened: false})}
285            tabIndex={-1}
286          >
287            {t('generic.cancel')}
288          </Button>
289          <Button
290            color="primary"
291            variant="contained"
292            type="submit"
293            disabled={!canCreate}
294            aria-disabled={!canCreate}
295            id="NewTravelSubmit"
296          >
297            {t('travel.creation.submit')}
298          </Button>
299        </DialogActions>
300      </form>
301    </Dialog>
302  );
303};
304
305const Transition = forwardRef(function Transition(props, ref) {
306  return <Slide direction="up" ref={ref} {...props} />;
307});
308
309const formatDate = (date: Moment, time: Moment) => {
310  return moment(
311    `${moment(date).format('YYYY-MM-DD')} ${moment(time).format('HH:mm')}`,
312    'YYYY-MM-DD HH:mm'
313  ).toISOString();
314};
315
316const MARKS = [1, 2, 3, 4, 5, 6, 7, 8].map(value => ({
317  value,
318  label: value,
319}));
320
321const addSpacing = (theme, ratio) => ({
322  margin: `0 ${theme.spacing(3 * ratio)}`,
323  width: `calc(100% - ${theme.spacing(6 * ratio)})`,
324});
325
326export default NewTravelDialog;