all repos — caroster @ fbc9e225b5270c61549fce0c2fd4c1c1faa05e48

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