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