all repos — caroster @ ceda4c783969e70cbca2e5d932085b1d544319dd

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