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