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