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