all repos — caroster @ 25ef569b9aff78df82eea6d516cca3a4aeccbb8a

[Octree] Group carpool to your event https://caroster.io

frontend/containers/TravelColumns/index.tsx (view raw)

  1import {useState} from 'react';
  2import dynamic from 'next/dynamic';
  3import Container from '@mui/material/Container';
  4import Masonry from '@mui/lab/Masonry';
  5import Box from '@mui/material/Box';
  6import {useTranslation} from 'react-i18next';
  7import {useTheme} from '@mui/material/styles';
  8import useEventStore from '../../stores/useEventStore';
  9import useToastStore from '../../stores/useToastStore';
 10import useMapStore from '../../stores/useMapStore';
 11import useProfile from '../../hooks/useProfile';
 12import useAddToEvents from '../../hooks/useAddToEvents';
 13import usePassengersActions from '../../hooks/usePassengersActions';
 14import Map from '../Map';
 15import Travel from '../Travel';
 16import NoCar from './NoCar';
 17import {Travel as TravelData, TravelEntity} from '../../generated/graphql';
 18import {AddPassengerToTravel} from '../NewPassengerDialog';
 19
 20const EventMarker = dynamic(() => import('../EventMarker'), {ssr: false});
 21const TravelMarker = dynamic(() => import('../TravelMarker'), {ssr: false});
 22
 23type TravelType = TravelData & {id: string};
 24
 25interface Props {
 26  toggle: () => void;
 27}
 28
 29const TravelColumns = (props: Props) => {
 30  const theme = useTheme();
 31  const {
 32    focusedTravel,
 33    preventUpdateKey,
 34    setPreventUpdateKey,
 35    setMarkers,
 36    setBounds,
 37  } = useMapStore();
 38  const event = useEventStore(s => s.event);
 39  const travels = event?.travels?.data || [];
 40  const {t} = useTranslation();
 41  const addToast = useToastStore(s => s.addToast);
 42  const {addToEvent} = useAddToEvents();
 43  const {profile, userId} = useProfile();
 44
 45  const [newPassengerTravelContext, toggleNewPassengerToTravel] = useState<{
 46    travel: TravelType;
 47  } | null>(null);
 48  const {addPassenger} = usePassengersActions();
 49  const sortedTravels = travels?.slice().sort(sortTravels);
 50
 51  const addSelfToTravel = async (travel: TravelType) => {
 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  if (!event || travels?.length === 0)
 70    return (
 71      <NoCar
 72        showImage
 73        eventName={event?.name}
 74        title={t('event.no_travel.title')}
 75      />
 76    );
 77
 78  const {latitude, longitude} = event;
 79  const showMap =
 80    (latitude && longitude) ||
 81    travels.some(
 82      ({attributes: {meeting_latitude, meeting_longitude}}) =>
 83        meeting_latitude && meeting_longitude
 84    );
 85  let coordsString = `${latitude}${longitude}`;
 86
 87  const {markers, bounds} = travels.reduce(
 88    ({markers, bounds}, travel) => {
 89      const {
 90        attributes: {meeting_latitude, meeting_longitude},
 91      } = travel;
 92      if (meeting_latitude && meeting_longitude) {
 93        const travelObject = {id: travel.id, ...travel.attributes};
 94        coordsString =
 95          coordsString + String(meeting_latitude) + String(meeting_longitude);
 96        return {
 97          markers: [
 98            ...markers,
 99            <TravelMarker
100              travel={travelObject}
101              focused={focusedTravel === travel.id}
102            />,
103          ],
104          bounds: [...bounds, [meeting_latitude, meeting_longitude]],
105        };
106      }
107      return {markers, bounds};
108    },
109    {markers: [], bounds: []}
110  );
111
112  const mapUpdateKey = `${event.uuid}.travels+${coordsString}.${latitude}.${longitude}.${focusedTravel}`;
113  if (preventUpdateKey !== mapUpdateKey) {
114    setPreventUpdateKey(mapUpdateKey);
115    if (latitude && longitude) {
116      bounds.push([latitude, longitude]);
117      markers.push(<EventMarker event={event} />);
118    }
119    if (!focusedTravel) {
120      setBounds(bounds);
121    }
122    setMarkers(markers);
123  }
124
125  return (
126    <>
127      {showMap && <Map />}
128      <Box
129        p={0}
130        pt={showMap ? 4 : 9}
131        pb={11}
132        sx={{
133          overflowX: 'hidden',
134          overflowY: 'auto',
135          maxHeight: showMap ? '50vh' : '100vh',
136          [theme.breakpoints.down('md')]: {
137            maxHeight: showMap ? '50vh' : '100vh',
138            px: 1,
139          },
140        }}
141      >
142        <Masonry columns={{xl: 4, lg: 3, md: 2, sm: 2, xs: 1}} spacing={0}>
143          {sortedTravels?.map(({id, attributes}) => {
144            const travel = {id, ...attributes};
145            return (
146              <Container
147                key={travel.id}
148                maxWidth="sm"
149                sx={{
150                  padding: theme.spacing(1),
151                  marginBottom: theme.spacing(10),
152                  outline: 'none',
153                  '& > *': {
154                    cursor: 'default',
155                  },
156
157                  [theme.breakpoints.down('md')]: {
158                    marginBottom: `calc(${theme.spacing(10)} + 56px)`,
159                  },
160                }}
161              >
162                <Travel
163                  travel={travel}
164                  {...props}
165                  getAddPassengerFunction={(addSelf: boolean) => () =>
166                    addSelf
167                      ? addSelfToTravel(travel)
168                      : toggleNewPassengerToTravel({travel})}
169                />
170              </Container>
171            );
172          })}
173          <Container
174            maxWidth="sm"
175            sx={{
176              padding: theme.spacing(1),
177              marginBottom: theme.spacing(10),
178              outline: 'none',
179              '& > *': {
180                cursor: 'default',
181              },
182
183              [theme.breakpoints.down('md')]: {
184                marginBottom: `calc(${theme.spacing(10)} + 56px)`,
185              },
186            }}
187          >
188            <NoCar
189              eventName={event?.name}
190              title={t('event.no_other_travel.title')}
191            />
192          </Container>
193        </Masonry>
194      </Box>
195      {!!newPassengerTravelContext && (
196        <AddPassengerToTravel
197          open={!!newPassengerTravelContext}
198          toggle={() => toggleNewPassengerToTravel(null)}
199          travel={newPassengerTravelContext.travel}
200        />
201      )}
202    </>
203  );
204};
205
206const sortTravels = (
207  {attributes: a}: TravelEntity,
208  {attributes: b}: TravelEntity
209) => {
210  if (!b) return 1;
211  const dateA = new Date(a.departure).getTime();
212  const dateB = new Date(b.departure).getTime();
213  if (dateA === dateB)
214    return new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime();
215  else return dateA - dateB;
216};
217
218export default TravelColumns;