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