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, Stack} from '@mui/material';
13import {DatePicker} from '@mui/x-date-pickers/DatePicker';
14import {TimePicker} from '@mui/x-date-pickers/TimePicker';
15import {useTranslation} from 'next-i18next';
16import PhoneInput from '../../components/PhoneInput';
17import PlaceInput from '../PlaceInput';
18import useEventStore from '../../stores/useEventStore';
19import useActions from './useActions';
20import FAQLink from './FAQLink';
21import {useSession} from 'next-auth/react';
22
23interface Props {
24 opened: boolean;
25 toggle: (opts: {opened: boolean}) => void;
26}
27
28const NewTravelDialog = ({opened, toggle}: Props) => {
29 const {t} = useTranslation();
30 const event = useEventStore(s => s.event);
31 const {createTravel} = useActions({event});
32 const session = useSession();
33 const profile = session?.data?.profile;
34 const dateMoment = useMemo(
35 () => (event?.date ? moment(event.date) : null),
36 [event?.date]
37 );
38
39 // States
40 const [firstname, setFirstname] = useState(profile?.firstName || '');
41 const [lastname, setLastname] = useState(profile?.lastName || '');
42 const [seats, setSeats] = useState(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 const [phoneCountry, setPhoneCountry] = useState('');
50 const [phoneError, setPhoneError] = useState(false);
51 const [details, setDetails] = useState('');
52
53 const canCreate =
54 !!firstname?.trim() &&
55 !!lastname?.trim() &&
56 !!seats &&
57 !phoneError &&
58 phone;
59
60 const clearState = () => {
61 setFirstname('');
62 setLastname('');
63 setSeats(4);
64 setMeeting('');
65 setMeetingLatitude(null);
66 setMeetingLongitude(null);
67 setDate(moment());
68 setPhone('');
69 setPhoneCountry('');
70 setDetails('');
71 };
72
73 const onCreate = async e => {
74 if (e.preventDefault) e.preventDefault();
75
76 const travel = {
77 firstname,
78 lastname,
79 meeting,
80 meeting_latitude,
81 meeting_longitude,
82 details,
83 seats,
84 phone_number: phone,
85 phoneCountry: phoneCountry,
86 departureDate: date ? moment(date).format('YYYY-MM-DD') : '',
87 departureTime: time ? moment(time).format('HH:mm') : '',
88 event: event.id,
89 };
90
91 await createTravel(travel);
92 toggle({opened: false});
93
94 clearState();
95 };
96
97 return (
98 <Dialog
99 fullWidth
100 maxWidth="xs"
101 open={opened}
102 onClose={() => {
103 toggle({opened: false});
104 clearState();
105 }}
106 TransitionComponent={Transition}
107 >
108 <form onSubmit={onCreate}>
109 <DialogTitle sx={{paddingBottom: 0}}>
110 {t('travel.creation.title')}
111 </DialogTitle>
112 <DialogContent sx={{px: 0}}>
113 <Stack px={3} py={2} spacing={2}>
114 <Typography>{t('travel.creation.car.title')}</Typography>
115 <Box display="flex" gap={2}>
116 <TextField
117 fullWidth
118 required
119 variant="outlined"
120 size="small"
121 label={t`profile.firstName`}
122 value={firstname}
123 onChange={e => setFirstname(e.target.value)}
124 name="firstname"
125 id="NewTravelFirstname"
126 error={!firstname?.trim()}
127 />
128 <TextField
129 fullWidth
130 required
131 variant="outlined"
132 size="small"
133 label={t`profile.lastName`}
134 value={lastname}
135 onChange={e => setLastname(e.target.value)}
136 name="lastname"
137 id="NewTravelLastname"
138 error={!lastname?.trim()}
139 />
140 </Box>
141 <PhoneInput
142 required
143 value={phone}
144 onChange={({phone, country, error}) => {
145 setPhone(phone);
146 setPhoneCountry(country);
147 setPhoneError(error);
148 }}
149 label={t('travel.creation.phone')}
150 name="phone"
151 variant="outlined"
152 size="small"
153 helperText={
154 <Typography variant="caption">
155 <FAQLink
156 link={t('travel.creation.phoneHelper.faq')}
157 text={t('travel.creation.phoneHelper.why')}
158 />
159 </Typography>
160 }
161 id="NewTravelPhone"
162 />
163 <Box>
164 <Typography variant="caption">
165 {t('travel.creation.seats')}
166 </Typography>
167 <Slider
168 size="small"
169 value={seats}
170 onChange={(e, value) => setSeats(value as number)}
171 step={1}
172 marks={MARKS}
173 min={1}
174 max={MARKS.length}
175 valueLabelDisplay="auto"
176 id="NewTravelSeats"
177 />
178 </Box>
179 </Stack>
180 <Divider />
181 <Stack px={3} pt={2} spacing={2}>
182 <Typography>{t('travel.creation.travel.title')}</Typography>
183 <Box display="flex" gap={2}>
184 <DatePicker
185 slotProps={{
186 textField: {
187 variant: 'outlined',
188 size: 'small',
189 helperText: !date
190 ? t('travel.creation.travel.dateHelper')
191 : '',
192 error: !date,
193 FormHelperTextProps: {sx: {color: 'warning.main'}},
194 },
195 }}
196 format="DD/MM/YYYY"
197 label={t('travel.creation.date')}
198 value={date}
199 onChange={setDate}
200 autoFocus
201 />
202 <TimePicker
203 slotProps={{
204 textField: {
205 variant: 'outlined',
206 size: 'small',
207 helperText: '',
208 },
209 }}
210 label={t('travel.creation.time')}
211 value={time}
212 onChange={setTime}
213 ampm={false}
214 minutesStep={5}
215 />
216 </Box>
217 <PlaceInput
218 label={t(
219 event?.isReturnEvent ? 'travel.destination' : 'travel.meeting'
220 )}
221 textFieldProps={{
222 variant: 'outlined',
223 }}
224 place={meeting}
225 latitude={meeting_latitude}
226 longitude={meeting_longitude}
227 onSelect={({place, latitude, longitude}) => {
228 setMeeting(place);
229 setMeetingLatitude(latitude);
230 setMeetingLongitude(longitude);
231 }}
232 />
233 <TextField
234 variant="outlined"
235 size="small"
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 </Stack>
248 </DialogContent>
249 <DialogActions
250 sx={{
251 paddingTop: 0,
252 }}
253 >
254 <Button
255 color="primary"
256 id="NewTravelCancel"
257 onClick={() => toggle({opened: false})}
258 tabIndex={-1}
259 >
260 {t('generic.cancel')}
261 </Button>
262 <Button
263 color="primary"
264 variant="contained"
265 type="submit"
266 disabled={!canCreate}
267 aria-disabled={!canCreate}
268 id="NewTravelSubmit"
269 >
270 {t('travel.creation.submit')}
271 </Button>
272 </DialogActions>
273 </form>
274 </Dialog>
275 );
276};
277
278const Transition = forwardRef(function Transition(props, ref) {
279 return <Slide direction="up" ref={ref} {...props} />;
280});
281
282const MARKS = [1, 2, 3, 4, 5, 6, 7, 8].map(value => ({
283 value,
284 label: value,
285}));
286
287export default NewTravelDialog;