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