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