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