all repos — caroster @ 65bcb7d208677b65df7ba31f656ee6ee0cfb1d1d

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