frontend/containers/TravelColumns/index.tsx (view raw)
1import {useState} from 'react';
2import dynamic from 'next/dynamic';
3import Container from '@mui/material/Container';
4import Masonry from '@mui/lab/Masonry';
5import Box from '@mui/material/Box';
6import {useTranslation} from 'react-i18next';
7import {useTheme} from '@mui/material/styles';
8import useEventStore from '../../stores/useEventStore';
9import useToastStore from '../../stores/useToastStore';
10import useMapStore from '../../stores/useMapStore';
11import useProfile from '../../hooks/useProfile';
12import useAddToEvents from '../../hooks/useAddToEvents';
13import usePassengersActions from '../../hooks/usePassengersActions';
14import Map from '../Map';
15import Travel from '../Travel';
16import NoCar from './NoCar';
17import {Travel as TravelData, TravelEntity} from '../../generated/graphql';
18import {AddPassengerToTravel} from '../NewPassengerDialog';
19
20const EventMarker = dynamic(() => import('../EventMarker'), {ssr: false});
21const TravelMarker = dynamic(() => import('../TravelMarker'), {ssr: false});
22
23type TravelType = TravelData & {id: string};
24
25interface Props {
26 toggle: () => void;
27}
28
29const TravelColumns = (props: Props) => {
30 const theme = useTheme();
31 const {
32 focusedTravel,
33 preventUpdateKey,
34 setPreventUpdateKey,
35 setMarkers,
36 setBounds,
37 } = useMapStore();
38 const event = useEventStore(s => s.event);
39 const travels = event?.travels?.data || [];
40 const {t} = useTranslation();
41 const addToast = useToastStore(s => s.addToast);
42 const {addToEvent} = useAddToEvents();
43 const {profile, userId} = useProfile();
44
45 const [newPassengerTravelContext, toggleNewPassengerToTravel] = useState<{
46 travel: TravelType;
47 } | null>(null);
48 const {addPassenger} = usePassengersActions();
49 const sortedTravels = travels?.slice().sort(sortTravels);
50
51 const addSelfToTravel = async (travel: TravelType) => {
52 const hasName = profile.firstName && profile.lastName;
53 const userName = profile.firstName + ' ' + profile.lastName;
54 try {
55 await addPassenger({
56 user: userId,
57 email: profile.email,
58 name: hasName ? userName : profile.username,
59 travel: travel.id,
60 event: event.id,
61 });
62 addToEvent(event.id);
63 addToast(t('passenger.success.added_self_to_car'));
64 } catch (error) {
65 console.error(error);
66 }
67 };
68
69 if (!event || travels?.length === 0)
70 return (
71 <NoCar
72 showImage
73 eventName={event?.name}
74 title={t('event.no_travel.title')}
75 />
76 );
77
78 const {latitude, longitude} = event;
79 const showMap =
80 (latitude && longitude) ||
81 travels.some(
82 ({attributes: {meeting_latitude, meeting_longitude}}) =>
83 meeting_latitude && meeting_longitude
84 );
85 let coordsString = `${latitude}${longitude}`;
86
87 const {markers, bounds} = travels.reduce(
88 ({markers, bounds}, travel) => {
89 const {
90 attributes: {meeting_latitude, meeting_longitude},
91 } = travel;
92 if (meeting_latitude && meeting_longitude) {
93 const travelObject = {id: travel.id, ...travel.attributes};
94 coordsString =
95 coordsString + String(meeting_latitude) + String(meeting_longitude);
96 return {
97 markers: [
98 ...markers,
99 <TravelMarker
100 travel={travelObject}
101 focused={focusedTravel === travel.id}
102 />,
103 ],
104 bounds: [...bounds, [meeting_latitude, meeting_longitude]],
105 };
106 }
107 return {markers, bounds};
108 },
109 {markers: [], bounds: []}
110 );
111
112 const mapUpdateKey = `${event.uuid}.travels+${coordsString}.${latitude}.${longitude}.${focusedTravel}`;
113 if (preventUpdateKey !== mapUpdateKey) {
114 setPreventUpdateKey(mapUpdateKey);
115 if (latitude && longitude) {
116 bounds.push([latitude, longitude]);
117 markers.push(<EventMarker event={event} />);
118 }
119 if (!focusedTravel) {
120 setBounds(bounds);
121 }
122 setMarkers(markers);
123 }
124
125 return (
126 <>
127 {showMap && <Map />}
128 <Box
129 p={0}
130 pt={showMap ? 4 : 9}
131 pb={11}
132 sx={{
133 overflowX: 'hidden',
134 overflowY: 'auto',
135 maxHeight: showMap ? '50vh' : '100vh',
136 [theme.breakpoints.down('md')]: {
137 maxHeight: showMap ? '50vh' : '100vh',
138 px: 1,
139 },
140 }}
141 >
142 <Masonry columns={{xl: 4, lg: 3, md: 2, sm: 2, xs: 1}} spacing={0}>
143 {sortedTravels?.map(({id, attributes}) => {
144 const travel = {id, ...attributes};
145 return (
146 <Container
147 key={travel.id}
148 maxWidth="sm"
149 sx={{
150 padding: theme.spacing(1),
151 marginBottom: theme.spacing(10),
152 outline: 'none',
153 '& > *': {
154 cursor: 'default',
155 },
156
157 [theme.breakpoints.down('md')]: {
158 marginBottom: `calc(${theme.spacing(10)} + 56px)`,
159 },
160 }}
161 >
162 <Travel
163 travel={travel}
164 {...props}
165 getAddPassengerFunction={(addSelf: boolean) => () =>
166 addSelf
167 ? addSelfToTravel(travel)
168 : toggleNewPassengerToTravel({travel})}
169 />
170 </Container>
171 );
172 })}
173 <Container
174 maxWidth="sm"
175 sx={{
176 padding: theme.spacing(1),
177 marginBottom: theme.spacing(10),
178 outline: 'none',
179 '& > *': {
180 cursor: 'default',
181 },
182
183 [theme.breakpoints.down('md')]: {
184 marginBottom: `calc(${theme.spacing(10)} + 56px)`,
185 },
186 }}
187 >
188 <NoCar
189 eventName={event?.name}
190 title={t('event.no_other_travel.title')}
191 />
192 </Container>
193 </Masonry>
194 </Box>
195 {!!newPassengerTravelContext && (
196 <AddPassengerToTravel
197 open={!!newPassengerTravelContext}
198 toggle={() => toggleNewPassengerToTravel(null)}
199 travel={newPassengerTravelContext.travel}
200 />
201 )}
202 </>
203 );
204};
205
206const sortTravels = (
207 {attributes: a}: TravelEntity,
208 {attributes: b}: TravelEntity
209) => {
210 if (!b) return 1;
211 const dateA = new Date(a.departure).getTime();
212 const dateB = new Date(b.departure).getTime();
213 if (dateA === dateB)
214 return new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime();
215 else return dateA - dateB;
216};
217
218export default TravelColumns;