frontend/containers/TravelColumns/index.tsx (view raw)
1import {useMemo, useReducer, useState} from 'react';
2import Masonry from '@mui/lab/Masonry';
3import Box from '@mui/material/Box';
4import moment from 'moment';
5import {useTranslation} from 'next-i18next';
6import {useTheme} from '@mui/material/styles';
7import useEventStore from '../../stores/useEventStore';
8import useToastStore from '../../stores/useToastStore';
9import useProfile from '../../hooks/useProfile';
10import useAddToEvents from '../../hooks/useAddToEvents';
11import usePassengersActions from '../../hooks/usePassengersActions';
12import Map from '../Map';
13import Travel from '../Travel';
14import NoCar from './NoCar';
15import {TravelEntity} from '../../generated/graphql';
16import {AddPassengerToTravel} from '../NewPassengerDialog';
17import MasonryContainer from './MasonryContainer';
18import LoginToAttend from '../LoginToAttend/LoginToAttend';
19import usePermissions from '../../hooks/usePermissions';
20import useDisplayTravels from './useDisplayTravels';
21import useDisplayMarkers from './useDisplayMarkers';
22import FilterByDate from './FilterByDate';
23import {Button, Icon} from '@mui/material';
24import useTravelsStore from '../../stores/useTravelsStore';
25
26interface Props {
27 showTravelModal: () => void;
28}
29
30const TravelColumns = (props: Props) => {
31 const theme = useTheme();
32 const event = useEventStore(s => s.event);
33 const travels = event?.travels?.data || [];
34 const {t} = useTranslation();
35 const addToast = useToastStore(s => s.addToast);
36 const {addToEvent} = useAddToEvents();
37 const {profile, userId} = useProfile();
38 const {
39 userPermissions: {canAddTravel},
40 } = usePermissions();
41
42 const [selectedTravel, setSelectedTravel] = useState<TravelEntity>();
43 const [mapEnabled, toggleMap] = useReducer(i => !i, true);
44 const datesFilters = useTravelsStore(s => s.datesFilter);
45 const {addPassenger} = usePassengersActions();
46 const {displayedTravels} = useDisplayTravels();
47 useDisplayMarkers({event});
48
49 const buttonFilterContent = useMemo(() => {
50 if (datesFilters.length > 1) return t('event.filter.dates');
51 else if (datesFilters.length === 1)
52 return datesFilters.map(date => date.format('dddd Do MMMM'));
53 else return t('event.filter.allDates');
54 }, [datesFilters, t]);
55
56 const addSelfToTravel = async (travel: TravelEntity) => {
57 const hasName = profile.firstName && profile.lastName;
58 const userName = profile.firstName + ' ' + profile.lastName;
59 try {
60 await addPassenger({
61 user: userId,
62 email: profile.email,
63 name: hasName ? userName : profile.username,
64 travel: travel.id,
65 event: event.id,
66 });
67 addToEvent(event.id);
68 addToast(t('passenger.success.added_self_to_car'));
69 } catch (error) {
70 console.error(error);
71 }
72 };
73
74 const isCarosterPlus = event?.enabled_modules?.includes('caroster-plus');
75
76 const haveGeopoints =
77 (!!event?.latitude && !!event?.longitude) ||
78 travels?.some(
79 ({attributes: {meeting_latitude, meeting_longitude}}) =>
80 meeting_latitude && meeting_longitude
81 );
82 const showMap = mapEnabled && haveGeopoints;
83
84 if (!event || travels?.length === 0)
85 return (
86 <NoCar
87 showImage
88 showTravelModal={props.showTravelModal}
89 eventName={event?.name}
90 title={t('event.no_travel.title')}
91 isCarosterPlus={isCarosterPlus}
92 />
93 );
94
95 const dates = Array.from(
96 new Set(travels.map(travel => travel?.attributes?.departureDate))
97 )
98 .map(date => moment(date))
99 .filter(date => date.isValid())
100 .sort((a, b) => (a.isAfter(b) ? 1 : -1));
101
102 return (
103 <>
104 {showMap && <Map />}
105 <Box
106 px={3}
107 pb={2}
108 pt={showMap ? 2 : 10}
109 display="flex"
110 gap={2}
111 maxWidth="100%"
112 flexWrap="wrap"
113 >
114 <FilterByDate dates={dates} buttonFilterContent={buttonFilterContent} />
115 {canAddTravel() && (
116 <Button
117 onClick={props.showTravelModal}
118 aria-label="add-car"
119 variant="contained"
120 color="secondary"
121 endIcon={<Icon>add</Icon>}
122 sx={{width: {xs: 1, sm: 'auto'}}}
123 >
124 {t('travel.creation.title')}
125 </Button>
126 )}
127 {haveGeopoints && (
128 <Button
129 sx={{width: {xs: 1, sm: 'auto'}}}
130 onClick={toggleMap}
131 startIcon={<Icon>{mapEnabled ? 'visibility_off' : 'map'}</Icon>}
132 >
133 {mapEnabled ? t`travel.hideMap` : t`travel.showMap`}
134 </Button>
135 )}
136 </Box>
137 <Box
138 p={0}
139 pt={showMap ? 0 : 3}
140 pb={11}
141 sx={{
142 overflowX: 'hidden',
143 overflowY: 'auto',
144 maxHeight: showMap ? '50vh' : '100vh',
145 [theme.breakpoints.down('md')]: {
146 maxHeight: showMap ? '50vh' : '100vh',
147 px: 1,
148 },
149 }}
150 >
151 <Masonry columns={{xl: 4, lg: 3, md: 2, sm: 2, xs: 1}} spacing={0}>
152 {!canAddTravel() && (
153 <MasonryContainer key="no_other_travel">
154 <LoginToAttend title={t('event.loginToAttend')} />
155 </MasonryContainer>
156 )}
157 {displayedTravels?.map(travel => {
158 return (
159 <MasonryContainer key={travel.id}>
160 <Travel
161 travel={travel}
162 onAddSelf={() => addSelfToTravel(travel)}
163 onAddOther={() => setSelectedTravel(travel)}
164 {...props}
165 />
166 </MasonryContainer>
167 );
168 })}
169 <MasonryContainer key="no_other_travel">
170 <NoCar
171 eventName={event?.name}
172 title={t('event.no_other_travel.title')}
173 isCarosterPlus={isCarosterPlus}
174 />
175 </MasonryContainer>
176 </Masonry>
177 </Box>
178 {!!selectedTravel && (
179 <AddPassengerToTravel
180 open={!!selectedTravel}
181 toggle={() => setSelectedTravel(null)}
182 travel={selectedTravel}
183 />
184 )}
185 </>
186 );
187};
188
189export default TravelColumns;