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