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