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