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 useEventStore from '../../stores/useEventStore';
7import useToastStore from '../../stores/useToastStore';
8import useProfile from '../../hooks/useProfile';
9import useAddToEvents from '../../hooks/useAddToEvents';
10import usePassengersActions from '../../hooks/usePassengersActions';
11import Map from '../Map';
12import Travel from '../Travel';
13import NoCar from './NoCar';
14import {TravelEntity} from '../../generated/graphql';
15import {AddPassengerToTravel} from '../NewPassengerDialog';
16import MasonryContainer from './MasonryContainer';
17import useDisplayTravels from './useDisplayTravels';
18import useDisplayMarkers from './useDisplayMarkers';
19import FilterByDate from './FilterByDate';
20import {Button, Icon, useMediaQuery} from '@mui/material';
21import useTravelsStore from '../../stores/useTravelsStore';
22import AddTravelButton from '../AddTravelButton';
23import MapActions from '../MapActions';
24import theme from '../../theme';
25
26interface Props {}
27
28const TravelColumns = (props: Props) => {
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 <MapActions />
100 <Box
101 px={3}
102 pb={2}
103 pt={showMap ? 2 : isMobile ? 22 : 18}
104 display="flex"
105 gap={2}
106 maxWidth="100%"
107 flexWrap="wrap"
108 >
109 <FilterByDate dates={dates} buttonFilterContent={buttonFilterContent} />
110 <AddTravelButton />
111 {haveGeopoints && (
112 <Button
113 sx={{width: {xs: 1, sm: 'auto'}}}
114 onClick={toggleMap}
115 startIcon={<Icon>{mapEnabled ? 'visibility_off' : 'map'}</Icon>}
116 >
117 {mapEnabled ? t`travel.hideMap` : t`travel.showMap`}
118 </Button>
119 )}
120 </Box>
121 <Box
122 p={0}
123 pt={showMap ? 0 : 3}
124 pb={11}
125 sx={{
126 overflowX: 'hidden',
127 overflowY: 'auto',
128 maxHeight: showMap ? '50vh' : '100vh',
129 [theme.breakpoints.down('md')]: {
130 maxHeight: showMap ? '50vh' : '100vh',
131 px: 1,
132 },
133 }}
134 >
135 <Masonry columns={{xl: 4, lg: 3, md: 2, sm: 2, xs: 1}} spacing={0}>
136 {displayedTravels?.map(travel => {
137 return (
138 <MasonryContainer key={travel.id}>
139 <Travel
140 travel={travel}
141 onAddSelf={() => addSelfToTravel(travel)}
142 onAddOther={() => setSelectedTravel(travel)}
143 {...props}
144 />
145 </MasonryContainer>
146 );
147 })}
148 <MasonryContainer key="no_other_travel">
149 <NoCar
150 eventName={event?.name}
151 title={t('event.no_other_travel.title')}
152 isCarosterPlus={isCarosterPlus}
153 />
154 </MasonryContainer>
155 </Masonry>
156 </Box>
157 {!!selectedTravel && (
158 <AddPassengerToTravel
159 open={!!selectedTravel}
160 toggle={() => setSelectedTravel(null)}
161 travel={selectedTravel}
162 />
163 )}
164 </>
165 );
166};
167
168export default TravelColumns;