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