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 label={t('travel.creation.date')}
194 value={date}
195 onChange={setDate}
196 autoFocus
197 />
198 <TimePicker
199 renderInput={props => (
200 <TextField
201 {...props}
202 variant="outlined"
203 size="small"
204 helperText=" "
205 sx={halfWidthFieldSx}
206 />
207 )}
208 label={t('travel.creation.time')}
209 value={time}
210 onChange={setTime}
211 ampm={false}
212 minutesStep={5}
213 />
214 </Box>
215 <TextField
216 variant="outlined"
217 size="small"
218 sx={{...addSpacing(theme, 1), paddingBottom: theme.spacing(1)}}
219 label={t('travel.creation.meeting')}
220 fullWidth
221 multiline
222 maxRows={4}
223 inputProps={{maxLength: 250}}
224 helperText={`${meeting.length}/250`}
225 value={meeting}
226 onChange={e => setMeeting(e.target.value)}
227 name="meeting"
228 id="NewTravelMeeting"
229 />
230 <TextField
231 variant="outlined"
232 size="small"
233 sx={{...addSpacing(theme, 1), paddingBottom: theme.spacing(1)}}
234 label={t('travel.creation.notes')}
235 fullWidth
236 multiline
237 maxRows={4}
238 inputProps={{maxLength: 250}}
239 helperText={`${details.length}/250`}
240 value={details}
241 onChange={e => setDetails(e.target.value)}
242 name="details"
243 id="NewTravelDetails"
244 />
245 </DialogContent>
246 <DialogActions
247 sx={{
248 paddingTop: 0,
249 }}
250 >
251 <Button
252 color="primary"
253 id="NewTravelCancel"
254 onClick={() => toggle({opened: false})}
255 tabIndex={-1}
256 >
257 {t('generic.cancel')}
258 </Button>
259 <Button
260 color="primary"
261 variant="contained"
262 type="submit"
263 disabled={!canCreate}
264 aria-disabled={!canCreate}
265 id="NewTravelSubmit"
266 >
267 {t('travel.creation.submit')}
268 </Button>
269 </DialogActions>
270 </form>
271 </Dialog>
272 );
273};
274
275const Transition = forwardRef(function Transition(props, ref) {
276 return <Slide direction="up" ref={ref} {...props} />;
277});
278
279const formatDate = (date: Moment, time: Moment) => {
280 return moment(
281 `${moment(date).format('YYYY-MM-DD')} ${moment(time).format('HH:mm')}`,
282 'YYYY-MM-DD HH:mm'
283 ).toISOString();
284};
285
286const MARKS = [1, 2, 3, 4, 5, 6, 7, 8].map(value => ({
287 value,
288 label: value,
289}));
290
291const addSpacing = (theme, ratio) => ({
292 margin: `0 ${theme.spacing(3 * ratio)}`,
293 width: `calc(100% - ${theme.spacing(6 * ratio)})`,
294});
295
296export default NewTravelDialog;