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 FormHelperTextProps={{
144 component: () => (
145 <Typography variant="caption">
146 <FAQLink
147 sx={{textDecoration: 'none'}}
148 link={t('travel.creation.phoneHelper.faq')}
149 text={t('travel.creation.phoneHelper.why')}
150 />
151 </Typography>
152 ),
153 }}
154 id="NewTravelPhone"
155 />
156 <Box sx={addSpacing(theme, 1)}>
157 <Typography variant="caption">
158 {t('travel.creation.seats')}
159 </Typography>
160 <Slider
161 size="small"
162 value={seats}
163 onChange={(e, value) => setSeats(value)}
164 step={1}
165 marks={MARKS}
166 min={1}
167 max={MARKS.length}
168 valueLabelDisplay="auto"
169 id="NewTravelSeats"
170 />
171 </Box>
172 <Divider
173 sx={{
174 margin: `${theme.spacing(2)} 0`,
175 }}
176 />
177 <Typography
178 sx={{...addSpacing(theme, 1), paddingBottom: theme.spacing(1.5)}}
179 >
180 {t('travel.creation.travel.title')}
181 </Typography>
182 <Box sx={addSpacing(theme, 0.5)}>
183 <DatePicker
184 renderInput={props => (
185 <TextField
186 {...props}
187 variant="outlined"
188 size="small"
189 helperText=" "
190 sx={halfWidthFieldSx}
191 />
192 )}
193 inputFormat="DD/MM/yyyy"
194 label={t('travel.creation.date')}
195 value={date}
196 onChange={setDate}
197 autoFocus
198 />
199 <TimePicker
200 renderInput={props => (
201 <TextField
202 {...props}
203 variant="outlined"
204 size="small"
205 helperText=" "
206 sx={halfWidthFieldSx}
207 />
208 )}
209 label={t('travel.creation.time')}
210 value={time}
211 onChange={setTime}
212 ampm={false}
213 minutesStep={5}
214 />
215 </Box>
216 <TextField
217 variant="outlined"
218 size="small"
219 sx={{...addSpacing(theme, 1), paddingBottom: theme.spacing(1)}}
220 label={t('travel.creation.meeting')}
221 fullWidth
222 multiline
223 maxRows={4}
224 inputProps={{maxLength: 250}}
225 helperText={`${meeting.length}/250`}
226 value={meeting}
227 onChange={e => setMeeting(e.target.value)}
228 name="meeting"
229 id="NewTravelMeeting"
230 />
231 <TextField
232 variant="outlined"
233 size="small"
234 sx={{...addSpacing(theme, 1), paddingBottom: theme.spacing(1)}}
235 label={t('travel.creation.notes')}
236 fullWidth
237 multiline
238 maxRows={4}
239 inputProps={{maxLength: 250}}
240 helperText={`${details.length}/250`}
241 value={details}
242 onChange={e => setDetails(e.target.value)}
243 name="details"
244 id="NewTravelDetails"
245 />
246 </DialogContent>
247 <DialogActions
248 sx={{
249 paddingTop: 0,
250 }}
251 >
252 <Button
253 color="primary"
254 id="NewTravelCancel"
255 onClick={() => toggle({opened: false})}
256 tabIndex={-1}
257 >
258 {t('generic.cancel')}
259 </Button>
260 <Button
261 color="primary"
262 variant="contained"
263 type="submit"
264 disabled={!canCreate}
265 aria-disabled={!canCreate}
266 id="NewTravelSubmit"
267 >
268 {t('travel.creation.submit')}
269 </Button>
270 </DialogActions>
271 </form>
272 </Dialog>
273 );
274};
275
276const Transition = forwardRef(function Transition(props, ref) {
277 return <Slide direction="up" ref={ref} {...props} />;
278});
279
280const formatDate = (date: Moment, time: Moment) => {
281 return moment(
282 `${moment(date).format('YYYY-MM-DD')} ${moment(time).format('HH:mm')}`,
283 'YYYY-MM-DD HH:mm'
284 ).toISOString();
285};
286
287const MARKS = [1, 2, 3, 4, 5, 6, 7, 8].map(value => ({
288 value,
289 label: value,
290}));
291
292const addSpacing = (theme, ratio) => ({
293 margin: `0 ${theme.spacing(3 * ratio)}`,
294 width: `calc(100% - ${theme.spacing(6 * ratio)})`,
295});
296
297export default NewTravelDialog;