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