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