all repos — caroster @ d0d47704e427face6c4cea5a2e1326c3679f6f5d

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

feat:✨ Set new navigation

#308 #301 #299
Simon Mulquin simon@octree.ch
Mon, 30 May 2022 15:34:32 +0000
commit

d0d47704e427face6c4cea5a2e1326c3679f6f5d

parent

7aaf2e08839a69d11fe38936bef5abaf5789b853

M frontend/components/Banner/index.tsxfrontend/components/Banner/index.tsx

@@ -24,12 +24,12 @@ if (typeof document != 'undefined') {

useEventListener('scroll', () => { const y = window.scrollY; if (y > height) { - setBannerOffset({offset: 0}) + setBannerOffset({offset: 0}); } if (y <= height) { setBannerOffset({offset: height - y}); } - }) + }); } if (!open) return null;

@@ -53,11 +53,12 @@

const useStyles = makeStyles(theme => ({ banner: { position: 'relative', - background: `linear-gradient(90deg, ${theme.palette.secondary.main} 20%, ${theme.palette.primary.main} 90%)`, + background: `linear-gradient(90deg, #FCDC61 20%, #78B2AC 90%)`, width: '100%', padding: '12px 60px', textAlign: 'center', zIndex: theme.zIndex.appBar - 1, + color: 'black', }, clear: { position: 'absolute',
M frontend/components/Toasts/index.tsxfrontend/components/Toasts/index.tsx

@@ -4,6 +4,7 @@ import useToastStore from '../../stores/useToastStore';

const Toasts = () => { const toast = useToastStore(s => s.toast); + const action = useToastStore(s => s.action); const clearToast = useToastStore(s => s.clearToast); const classes = useStyles();

@@ -18,6 +19,7 @@ autoHideDuration={6000}

open={!!toast} message={toast} onClose={clearToast} + action={action} /> ); };
M frontend/containers/DrawerMenu/DrawerMenuItem.tsxfrontend/containers/DrawerMenu/DrawerMenuItem.tsx

@@ -6,14 +6,15 @@

interface Props { Icon: JSX.Element; title: string; - href: string; + onClick: () => void; + active: boolean; } -const DrawerMenuItem = ({Icon, title, href}: Props) => { - const classes = useStyles(); +const DrawerMenuItem = ({Icon, title, onClick, active}: Props) => { + const classes = useStyles({active}); return ( <Box className={classes.drawerMenuItem}> - <Button className={classes.button} color="primary" variant='contained' href={href}> + <Button className={classes.button} color="inherit" onClick={onClick}> {Icon} </Button> <Typography
M frontend/containers/DrawerMenu/index.tsxfrontend/containers/DrawerMenu/index.tsx

@@ -1,39 +1,47 @@

import Drawer from '@material-ui/core/Drawer'; import Icon from '@material-ui/core/Icon'; import {useTranslation} from 'react-i18next'; -import { useRouter } from 'next/router'; +import router, {useRouter} from 'next/router'; import DrawerMenuItem from './DrawerMenuItem'; import useStyles from './styles'; import useBannerStore from '../../stores/useBannerStore'; +import useEventStore from '../../stores/useEventStore'; const DrawerMenu = () => { const {t} = useTranslation(); - const bannerOffset = useBannerStore(s => s.offset) + const bannerOffset = useBannerStore(s => s.offset); + const areDetailsOpened = useEventStore(s => s.areDetailsOpened); + const setAreDetailsOpened = useEventStore(s => s.setAreDetailsOpened); const classes = useStyles({bannerOffset}); - const {query: {uuid}} = useRouter(); + const { + query: {uuid}, + } = useRouter(); return ( <Drawer variant="permanent" className={classes.drawer}> <DrawerMenuItem title={t('drawer.travels')} - href={`/e/${uuid}`} - Icon={ - <Icon>directions_car</Icon> - } + onClick={() => { + router.push(`/e/${uuid}`); + setAreDetailsOpened(false); + }} + Icon={<Icon>directions_car</Icon>} + active={router.pathname == `/e/[uuid]`} /> <DrawerMenuItem title={t('drawer.waitingList')} - href={`/e/${uuid}`} - Icon={ - <Icon>group</Icon> - } + onClick={() => { + router.push(`/e/${uuid}/waitingList`); + setAreDetailsOpened(false); + }} + Icon={<Icon>group</Icon>} + active={router.pathname == `/e/[uuid]/waitingList`} /> <DrawerMenuItem title={t('drawer.information')} - href={`/e/${uuid}`} - Icon={ - <Icon>info</Icon> - } + onClick={() => setAreDetailsOpened(true)} + Icon={<Icon>info</Icon>} + active={areDetailsOpened} /> </Drawer> );
M frontend/containers/DrawerMenu/styles.tsfrontend/containers/DrawerMenu/styles.ts

@@ -19,9 +19,8 @@

[theme.breakpoints.down('sm')]: { bottom: 0, top: 'auto', - paddingTop: theme.spacing(0.5), - paddingBottom: theme.spacing(2.5), - height: '80px', + paddingTop: 0, + height: '56px', width: '100%', flexDirection: 'row', },

@@ -34,18 +33,18 @@ minWidth: 0,

padding: 0, borderRadius: '50%', }, - drawerMenuItem: { + drawerMenuItem: ({active}) => ({ margin: `${theme.spacing(3)}px auto`, width: `calc(${theme.mixins.toolbar.minHeight}px - 16px)`, height: `calc(${theme.mixins.toolbar.minHeight}px - 16px)`, textAlign: 'center', + color: active ? '#fff' : 'rgba(256, 256, 256, .76)', [theme.breakpoints.down('sm')]: { - margin: '8px auto', + margin: '0 auto', }, - }, + }), drawerText: { - paddingTop: theme.spacing(1), fontSize: '0.7em', lineHeight: '1.1em', display: 'flex',
M frontend/containers/EventBar/index.tsxfrontend/containers/EventBar/index.tsx

@@ -21,13 +21,14 @@ import EventDetails from '../EventDetails';

import useBannerStore from '../../stores/useBannerStore'; import Banner from '../../components/Banner'; -const EventBar = ({event, onAdd, onSave, onShare}) => { +const EventBar = ({event, onAdd, onSave}) => { const {t} = useTranslation(); const router = useRouter(); - const [detailsOpen, toggleDetails] = useReducer(i => !i, false); const [anchorEl, setAnchorEl] = useState(null); const isEditing = useEventStore(s => s.isEditing); + const areDetailsOpened = useEventStore(s => s.areDetailsOpened); const setIsEditing = useEventStore(s => s.setIsEditing); + const setAreDetailsOpened = useEventStore(s => s.setAreDetailsOpened); const token = useAuthStore(s => s.token); const {user} = useProfile(); const settings = useSettings();

@@ -35,7 +36,7 @@ const setTour = useTourStore(s => s.setTour);

const tourStep = useTourStore(s => s.step); const bannerOffset = useBannerStore(s => s.offset); const bannerHeight = useBannerStore(s => s.height); - const classes = useStyles({detailsOpen, bannerOffset, bannerHeight}); + const classes = useStyles({areDetailsOpened, bannerOffset, bannerHeight}); const announcement = settings?.announcement || ''; const [lastAnnouncementSeen, setLastAnnouncementSeen] = useState( typeof localStorage !== 'undefined'

@@ -52,14 +53,6 @@ }

setLastAnnouncementSeen(announcement); }; - useEffect(() => { - onTourChange(toggleDetails); - }, [tourStep]); - - useEffect(() => { - if (!detailsOpen) setIsEditing(false); - }, [detailsOpen]); // eslint-disable-line react-hooks/exhaustive-deps - const signUp = () => router.push({ pathname: '/auth/register',

@@ -128,7 +121,7 @@ const userInfos = user

? [{label: user.username, id: 'Email'}, {divider: true}] : []; - const appLink = user ? '/dashboard' : settings?.['about_link'] || ''; + const appLink = user ? '/dashboard' : `/e/${event.uuid}` || ''; const UserIcon = user ? ( <Avatar className={classes.avatar}>

@@ -143,7 +136,9 @@ <AppBar

className={classes.appbar} position="fixed" color="primary" - id={(isEditing && 'EditEvent') || (detailsOpen && 'Details') || 'Menu'} + id={ + (isEditing && 'EditEvent') || (areDetailsOpened && 'Details') || 'Menu' + } > <Banner message={announcement}

@@ -170,7 +165,7 @@ {event.name}

</Typography> </Tooltip> - {detailsOpen && ( + {areDetailsOpened && ( <IconButton className="tour_event_edit" color="inherit"

@@ -182,14 +177,14 @@ <Icon>{isEditing ? 'done' : 'edit'}</Icon>

</IconButton> )} </div> - {detailsOpen ? ( + {areDetailsOpened ? ( <IconButton color="inherit" edge="end" id="CloseDetailsBtn" onClick={() => { setIsEditing(false); - toggleDetails(); + setAreDetailsOpened(!areDetailsOpened); }} > <Icon>close</Icon>

@@ -201,7 +196,7 @@ className={classes.shareIcon}

color="inherit" edge="end" id="ShareBtn" - onClick={toggleDetails} + onClick={() => setAreDetailsOpened(!areDetailsOpened)} > <Icon>share</Icon> </IconButton>

@@ -210,7 +205,7 @@ className={clsx(classes.iconButtons, 'tour_event_infos')}

color="inherit" edge="end" id="ShareBtn" - onClick={toggleDetails} + onClick={() => setAreDetailsOpened(!areDetailsOpened)} > <Icon>information_outline</Icon> </IconButton>

@@ -224,7 +219,7 @@ {UserIcon}

</IconButton> </> )} - {!detailsOpen && ( + {!areDetailsOpened && ( <GenericMenu anchorEl={anchorEl} setAnchorEl={setAnchorEl}

@@ -232,12 +227,12 @@ actions={[

...userInfos, ...[ { - label: detailsOpen + label: areDetailsOpened ? t('event.actions.hide_details') : t('event.actions.show_details'), onClick: e => { setAnchorEl(null); - toggleDetails(); + setAreDetailsOpened(!areDetailsOpened); }, id: 'DetailsTab', },

@@ -247,21 +242,20 @@ ]}

/> )} </Toolbar> - {detailsOpen && ( - <EventDetails toggleDetails={toggleDetails} onShare={onShare} /> - )} + {areDetailsOpened && <EventDetails />} </AppBar> ); }; -const onTourChange = (toggleDetails: Function) => { +const onTourChange = (toggleDetailsOpened: Function) => { const {prev, step, isCreator} = useTourStore.getState(); const fromTo = (step1: number, step2: number) => prev === step1 && step === step2; if (isCreator) { - if (fromTo(3, 2) || fromTo(2, 3) || fromTo(4, 5)) toggleDetails(); - } else if (fromTo(2, 3) || fromTo(3, 2) || fromTo(3, 4)) toggleDetails(); + if (fromTo(3, 2) || fromTo(2, 3) || fromTo(4, 5)) toggleDetailsOpened(); + } else if (fromTo(2, 3) || fromTo(3, 2) || fromTo(3, 4)) + toggleDetailsOpened(); }; const useStyles = makeStyles(theme => ({
M frontend/containers/EventDetails/index.tsxfrontend/containers/EventDetails/index.tsx

@@ -11,6 +11,7 @@ import useEventStore from '../../stores/useEventStore';

import {caroster} from '../../theme'; import CopyLink from '../../components/CopyLink'; import useToastStore from '../../stores/useToastStore'; +import useBannerStore from '../../stores/useBannerStore'; const EventDetails = () => { const {t} = useTranslation();

@@ -20,7 +21,8 @@ const setEventUpdate = useEventStore(s => s.setEventUpdate);

const isEditing = useEventStore(s => s.isEditing); const shareInput = useRef(null); const idPrefix = isEditing ? 'EditEvent' : 'Event'; - const classes = useStyles(); + const bannerOffset = useBannerStore(s => s.offset); + const classes = useStyles({bannerOffset}); if (!event) return null;

@@ -159,14 +161,20 @@ },

}); const useStyles = makeStyles(theme => ({ - container: { + container: ({bannerOffset}) => ({ padding: theme.spacing(2, 9), marginBottom: theme.spacing(12), + minHeight: `calc(100vh - ${ + theme.mixins.toolbar.minHeight + bannerOffset + }px)`, [theme.breakpoints.down('xs')]: { padding: theme.spacing(2), + minHeight: `calc(100vh - ${ + theme.mixins.toolbar.minHeight + bannerOffset + 56 + }px)`, }, - }, + }), section: { marginBottom: theme.spacing(2), width: '540px',
M frontend/containers/NewTravelDialog/index.tsxfrontend/containers/NewTravelDialog/index.tsx

@@ -272,14 +272,21 @@ padding: `${theme.spacing(2)}px 0`,

}, field: { ...addSpacing(theme, 1), - paddingBottom: theme.spacing(1) + paddingBottom: theme.spacing(1), }, halfWidthWrapper: { - ...addSpacing(theme, .5) + ...addSpacing(theme, 0.5), }, halfWidthField: { margin: `0 ${theme.spacing(1.5)}px`, width: `calc(50% - ${theme.spacing(3)}px)`, + + '& > .MuiFormLabel-root': { + textOverflow: 'ellipsis', + whiteSpace: 'nowrap', + width: '100%', + overflow: 'hidden', + }, }, slider: { ...addSpacing(theme, 1),

@@ -289,7 +296,7 @@ margin: `${theme.spacing(2)}px 0`,

}, actions: { paddingTop: 0, - } + }, })); export default NewTravelDialog;
A frontend/containers/TravelColumns/NoCar.tsx

@@ -0,0 +1,53 @@

+import Typography from '@material-ui/core/Typography'; +import Box from '@material-ui/core/Box'; +import {makeStyles} from '@material-ui/core/styles'; +import {useTranslation} from 'react-i18next'; +import Copylink from '../../components/CopyLink'; +import useToastStore from '../../stores/useToastStore'; + +interface Props { + eventName: string; + title: string; + image?: boolean; +} + +const NoCar = ({eventName, title, image}: Props) => { + const classes = useStyles({image}); + const {t} = useTranslation(); + const addToast = useToastStore(s => s.addToast); + + return ( + <Box className={classes.noTravel}> + <Typography variant="h5">{title}</Typography> + <img className={classes.noTravelImage} src="/assets/car.png" /> + <Typography>{t('event.no_travel.desc')}</Typography> + <Copylink + color="primary" + className={classes.share} + buttonText={t('event.fields.share')} + title={`Caroster ${eventName}`} + url={`${window.location.href}`} + onShare={() => addToast(t('event.actions.copied'))} + /> + </Box> + ); +}; + +const useStyles = makeStyles(theme => ({ + noTravel: ({image}) => ({ + margin: `${theme.spacing(4)}px auto`, + marginTop: image ? 0 : theme.spacing(8), + width: '280px', + maxWidth: '100%', + textAlign: 'center', + }), + noTravelImage: ({image}) => ({ + width: image ? '100%' : 0, + height: image ? 'auto' : theme.spacing(6), + }), + share: { + marginTop: theme.spacing(6), + }, +})); + +export default NoCar;
M frontend/containers/TravelColumns/index.tsxfrontend/containers/TravelColumns/index.tsx

@@ -1,27 +1,18 @@

-import {useEffect, useMemo, useRef, useState} from 'react'; +import {useMemo, useRef, useState} from 'react'; import {makeStyles} from '@material-ui/core/styles'; import Container from '@material-ui/core/Container'; import Slider from 'react-slick'; import {useTranslation} from 'react-i18next'; import {Travel as TravelType} from '../../generated/graphql'; import useEventStore from '../../stores/useEventStore'; -import useTourStore from '../../stores/useTourStore'; import useToastStore from '../../stores/useToastStore'; import useProfile from '../../hooks/useProfile'; import useAddToEvents from '../../hooks/useAddToEvents'; -import { - AddPassengerToTravel, - AddPassengerToWaitingList, -} from '../NewPassengerDialog'; -import WaitingList from '../WaitingList'; +import {AddPassengerToTravel} from '../NewPassengerDialog'; import Travel from '../Travel'; -import AddTravel from './AddTravel'; import sliderSettings from './_SliderSettings'; import usePassengersActions from '../../hooks/usePassengersActions'; - -interface NewPassengerDialogContext { - addSelf: boolean; -} +import NoCar from './NoCar'; interface Props { toggle: () => void;

@@ -32,7 +23,6 @@ const event = useEventStore(s => s.event);

const {travels = []} = event || {}; const slider = useRef(null); const {t} = useTranslation(); - const tourStep = useTourStore(s => s.step); const addToast = useToastStore(s => s.addToast); const {addToEvent} = useAddToEvents(); const {user} = useProfile();

@@ -40,8 +30,6 @@ const classes = useStyles();

const [newPassengerTravelContext, toggleNewPassengerToTravel] = useState<{ travel: TravelType; } | null>(null); - const [addPassengerToWaitingListContext, toggleNewPassengerToWaitingList] = - useState<NewPassengerDialogContext | null>(null); const {addPassenger} = usePassengersActions(); const sortedTravels = travels?.slice().sort(sortTravels);

@@ -70,54 +58,48 @@ } catch (error) {

console.error(error); } }; - - const slideToTravel = (travelId: string) => { - const travelIndex = sortedTravels.findIndex( - travel => travel.id === travelId - ); - const slideIndex = travelIndex + 1; - slider.current.slickGoTo(slideIndex); - }; - - // On tour step changes : component update - useEffect(() => { - onTourChange(slider.current); - }, [tourStep]); return ( <div className={classes.container}> <div className={classes.dots} id="slider-dots" /> - <div className={classes.slider}> - <Slider ref={slider} {...sliderSettings}> - <Container maxWidth="sm" className={classes.slide}> - <WaitingList - slideToTravel={slideToTravel} - canAddSelf={canAddSelf} - getToggleNewPassengerDialogFunction={(addSelf: boolean) => () => - toggleNewPassengerToWaitingList({addSelf})} - /> - </Container> - {sortedTravels?.map(travel => ( - <Container key={travel.id} maxWidth="sm" className={classes.slide}> - <Travel - travel={travel} - {...props} - canAddSelf={canAddSelf} - getAddPassengerFunction={(addSelf: boolean) => () => { - if (addSelf) { - return addSelfToTravel(travel); - } else { - return toggleNewPassengerToTravel({travel}); - } - }} + {(travels.length === 0 && ( + <NoCar + image + eventName={event?.name} + title={t('event.no_travel.title')} + /> + )) || ( + <div className={classes.slider}> + <Slider ref={slider} {...sliderSettings}> + {sortedTravels?.map(travel => ( + <Container + key={travel.id} + maxWidth="sm" + className={classes.slide} + > + <Travel + travel={travel} + {...props} + canAddSelf={canAddSelf} + getAddPassengerFunction={(addSelf: boolean) => () => { + if (addSelf) { + return addSelfToTravel(travel); + } else { + return toggleNewPassengerToTravel({travel}); + } + }} + /> + </Container> + ))} + <Container maxWidth="sm" className={classes.slide}> + <NoCar + eventName={event?.name} + title={t('event.no_other_travel.title')} /> </Container> - ))} - <Container maxWidth="sm" className={classes.slide}> - <AddTravel {...props} /> - </Container> - </Slider> - </div> + </Slider> + </div> + )} {!!newPassengerTravelContext && ( <AddPassengerToTravel open={!!newPassengerTravelContext}

@@ -125,27 +107,10 @@ toggle={() => toggleNewPassengerToTravel(null)}

travel={newPassengerTravelContext.travel} /> )} - {!!addPassengerToWaitingListContext && ( - <AddPassengerToWaitingList - open={!!addPassengerToWaitingListContext} - toggle={() => toggleNewPassengerToWaitingList(null)} - addSelf={addPassengerToWaitingListContext.addSelf} - /> - )} </div> ); }; -const onTourChange = slider => { - const {prev, step, isCreator} = useTourStore.getState(); - const fromTo = (step1: number, step2: number) => - prev === step1 && step === step2; - - if (isCreator) { - if (fromTo(2, 3) || fromTo(4, 3)) slider?.slickGoTo(0, true); - } else if (fromTo(0, 1)) slider?.slickGoTo(0, true); -}; - const sortTravels = (a: TravelType, b: TravelType) => { if (!b) return 1; const dateA = new Date(a.departure).getTime();

@@ -180,7 +145,7 @@ position: 'static',

'& li': { display: 'block', '& button:before': { - fontSize: '20px', + fontSize: '10px', }, }, },

@@ -202,10 +167,14 @@ },

}, slide: { padding: theme.spacing(1), - marginBottom: theme.spacing(12), + marginBottom: theme.spacing(10), outline: 'none', '& > *': { cursor: 'default', + }, + + [theme.breakpoints.down('sm')]: { + marginBottom: `${theme.spacing(10) + 56}px`, }, }, }));
M frontend/containers/WaitingList/TravelDialog.tsxfrontend/containers/WaitingList/TravelDialog.tsx

@@ -178,10 +178,13 @@ textAlign: 'center',

}, noTravelImage: { width: '100%', + [theme.breakpoints.down('sm')]: { + width: '50%', + }, }, share: { - marginTop: theme.spacing(2) - } + marginTop: theme.spacing(2), + }, })); export default TravelDialog;
M frontend/containers/WaitingList/index.tsxfrontend/containers/WaitingList/index.tsx

@@ -1,42 +1,47 @@

import {useReducer, useState, useMemo, useCallback} from 'react'; -import {makeStyles} from '@material-ui/core/styles'; +import clsx from 'clsx'; import Typography from '@material-ui/core/Typography'; import IconButton from '@material-ui/core/IconButton'; import Icon from '@material-ui/core/Icon'; import Paper from '@material-ui/core/Paper'; import Divider from '@material-ui/core/Divider'; -import clsx from 'clsx'; +import {makeStyles} from '@material-ui/core/styles'; import {Trans, useTranslation} from 'react-i18next'; import useToastStore from '../../stores/useToastStore'; import useEventStore from '../../stores/useEventStore'; +import usePassengersActions from '../../hooks/usePassengersActions'; import PassengersList from '../PassengersList'; import RemoveDialog from '../RemoveDialog'; import AddPassengerButtons from '../AddPassengerButtons'; -import TravelDialog from './TravelDialog'; import ClearButton from '../ClearButton'; import AssignButton from './AssignButton'; -import usePassengersActions from '../../hooks/usePassengersActions'; +import TravelDialog from './TravelDialog'; +import Button from '@material-ui/core/Button'; +import router from 'next/dist/client/router'; +import useBannerStore from '../../stores/useBannerStore'; +import Box from '@material-ui/core/Box'; +import Container from '@material-ui/core/Container'; interface Props { getToggleNewPassengerDialogFunction: (addSelf: boolean) => () => void; canAddSelf: boolean; - slideToTravel: (travelId: string) => void; } const WaitingList = ({ getToggleNewPassengerDialogFunction, canAddSelf, - slideToTravel, }: Props) => { - const classes = useStyles(); + const bannerOffset = useBannerStore(s => s.offset); + const classes = useStyles({bannerOffset}); const {t} = useTranslation(); + const clearToast = useToastStore(s => s.clearToast); const event = useEventStore(s => s.event); const addToast = useToastStore(s => s.addToast); const [isEditing, toggleEditing] = useReducer(i => !i, false); const [removingPassenger, setRemovingPassenger] = useState(null); const [addingPassenger, setAddingPassenger] = useState(null); const travels = - event?.travels?.length > 0 ? event.travels.slice().sort(sortTravels) : []; + event?.travels?.length > 0 ? event?.travels.slice().sort(sortTravels) : []; const {updatePassenger, removePassenger} = usePassengersActions(); const availability = useMemo(() => {

@@ -58,11 +63,21 @@ event: null,

travel: travel.id, }); setAddingPassenger(null); - slideToTravel(travel.id); addToast( t('passenger.success.added_to_car', { name: addingPassenger.name, - }) + }), + <Button + size="small" + color="primary" + variant="contained" + onClick={() => { + router.push(`/e/${event.uuid}`); + clearToast(); + }} + > + {t('passenger.success.goToTravels')} + </Button> ); } catch (error) { console.error(error);

@@ -74,7 +89,7 @@ );

const onPress = useCallback( (passengerId: string) => { - const selectedPassenger = event.waitingPassengers.find( + const selectedPassenger = event?.waitingPassengers.find( item => item.id === passengerId ); if (isEditing) setRemovingPassenger(selectedPassenger);

@@ -101,36 +116,38 @@ <AssignButton onClick={onClick} tabIndex={-1} disabled={disabled} />

); return ( - <> - <Paper className={classes.root}> - <div className={clsx(classes.header, 'tour_waiting_list')}> - <IconButton - size="small" - color="primary" - className={classes.editBtn} - disabled={!event.waitingPassengers?.length} - onClick={toggleEditing} - > - {isEditing ? <Icon>check</Icon> : <Icon>edit</Icon>} - </IconButton> - <Typography variant="h5">{t('passenger.title')}</Typography> - <Typography variant="overline"> - {t('passenger.availability.seats', {count: availability})} - </Typography> - </div> - <Divider /> - <AddPassengerButtons - getOnClickFunction={getToggleNewPassengerDialogFunction} - canAddSelf={canAddSelf} - variant="waitingList" - /> - <Divider /> - <PassengersList - passengers={event.waitingPassengers} - onPress={onPress} - Button={ListButton} - /> - </Paper> + <Box className={classes.root}> + <Container maxWidth="sm" className={classes.card}> + <Paper> + <div className={classes.header}> + <IconButton + size="small" + color="primary" + className={classes.editBtn} + disabled={!event?.waitingPassengers?.length} + onClick={toggleEditing} + > + {isEditing ? <Icon>check</Icon> : <Icon>edit</Icon>} + </IconButton> + <Typography variant="h5">{t('passenger.title')}</Typography> + <Typography variant="overline"> + {t('passenger.availability.seats', {count: availability})} + </Typography> + </div> + <Divider /> + <AddPassengerButtons + getOnClickFunction={getToggleNewPassengerDialogFunction} + canAddSelf={canAddSelf} + variant="waitingList" + /> + <Divider /> + <PassengersList + passengers={event?.waitingPassengers} + onPress={onPress} + Button={ListButton} + /> + </Paper> + </Container> <RemoveDialog text={ <Trans

@@ -146,14 +163,14 @@ onClose={() => setRemovingPassenger(null)}

onRemove={onRemove} /> <TravelDialog - eventName={event.name} + eventName={event?.name} travels={travels} passenger={addingPassenger} open={!!addingPassenger} onClose={() => setAddingPassenger(null)} onSelect={selectTravel} /> - </> + </Box> ); };

@@ -168,8 +185,17 @@

const useStyles = makeStyles(theme => ({ root: { position: 'relative', + paddingLeft: '80px', + + [theme.breakpoints.down('sm')]: { + paddingLeft: 0, + }, + }, + card: { + marginTop: theme.spacing(6), }, header: { + position: 'relative', padding: theme.spacing(2), }, editBtn: {
M frontend/locales/en.jsonfrontend/locales/en.json

@@ -72,6 +72,9 @@ },

"event": { "title": "{{title}} - Caroster", "not_found": "Project not found", + "no_travel.title": "There are currently no cars", + "no_other_travel.title": "There are currently no other car", + "no_travel.desc": "Share the event and register to the waiting list to get notified when a car will be available!", "fields": { "name": "Name of the event", "date": "Event date",

@@ -142,7 +145,7 @@ "name": "Name of the car",

"seats": "Number of seats", "meeting": "Meeting place", "phone": "Telephone number", - "phoneHelper.faq": "/en/terms", + "phoneHelper.faq": "/en/faq", "phoneHelper.why": "Why do we ask for a phone number ?", "notes": "Additional information", "created": "The car has been created",

@@ -258,7 +261,8 @@ "success": {

"added_self_to_car": "You have been added to this car", "added_to_car": "{{name}} has been added to this car", "added_self_to_waitlist": "You have been added to the waitlist. You'll be notified when new cars will be added.", - "added_to_waitlist": "{{name}} has been added to the waitlist" + "added_to_waitlist": "{{name}} has been added to the waitlist", + "goToTravels": "Go to travels" }, "input": { "email": "Your email",
M frontend/locales/fr.jsonfrontend/locales/fr.json

@@ -72,6 +72,9 @@ },

"event": { "title": "{{title}} - Caroster", "not_found": "Projet introuvable", + "no_travel.title": "Pas de voitures pour le moment", + "no_other_travel.title": "Pas d'autres voitures pour le moment", + "no_travel.desc": "Partagez l’événement et inscrivez-vous dans liste d’attente pour être notifié lorsqu’une voiture sera ajoutée !", "fields": { "name": "Nom de l'événement", "date": "Date de l'événement",

@@ -142,7 +145,7 @@ "name": "Nom de la voiture",

"seats": "Nombre de places", "meeting": "Lieu de rencontre", "phone": "Numéro de téléphone", - "phoneHelper.faq": "/fr/conditions-utilisation", + "phoneHelper.faq": "/fr/faq", "phoneHelper.why": "Pourquoi le num. de tél. est-il demandé?", "notes": "Infos complémentaires", "created": "La voiture a été créée",

@@ -258,7 +261,8 @@ "success": {

"added_self_to_car": "Vous avez été ajouté à la voiture", "added_to_car": "{{name}} a été ajouté à la voiture", "added_self_to_waitlist": "Vous avez été ajouté à la liste d’attente. Vous serez notifié à l’ajout de nouvelles voitures", - "added_to_waitlist": "{{name}} ajouté à la liste d'attente" + "added_to_waitlist": "{{name}} ajouté à la liste d'attente", + "goToTravels": "Aller aux trajets" }, "input": { "email": "Votre email",
M frontend/pages/e/[uuid].tsxfrontend/pages/e/[uuid]/index.tsx

@@ -2,18 +2,18 @@ import {useState, useReducer, useEffect} from 'react';

import Box from '@material-ui/core/Box'; import {makeStyles, useTheme} from '@material-ui/core/styles'; import {useTranslation} from 'react-i18next'; -import {initializeApollo} from '../../lib/apolloClient'; -import useToastStore from '../../stores/useToastStore'; -import useEventStore from '../../stores/useEventStore'; -import Layout from '../../layouts/Default'; -import AddToMyEventDialog from '../../containers/AddToMyEventDialog'; -import TravelColumns from '../../containers/TravelColumns'; -import NewTravelDialog from '../../containers/NewTravelDialog'; -import VehicleChoiceDialog from '../../containers/VehicleChoiceDialog'; -import WelcomeDialog from '../../containers/WelcomeDialog'; -import EventBar from '../../containers/EventBar'; -import Loading from '../../containers/Loading'; -import OnBoardingTour from '../../containers/OnBoardingTour'; +import {initializeApollo} from '../../../lib/apolloClient'; +import useToastStore from '../../../stores/useToastStore'; +import useEventStore from '../../../stores/useEventStore'; +import Layout from '../../../layouts/Default'; +import AddToMyEventDialog from '../../../containers/AddToMyEventDialog'; +import TravelColumns from '../../../containers/TravelColumns'; +import NewTravelDialog from '../../../containers/NewTravelDialog'; +import VehicleChoiceDialog from '../../../containers/VehicleChoiceDialog'; +import WelcomeDialog from '../../../containers/WelcomeDialog'; +import EventBar from '../../../containers/EventBar'; +import Loading from '../../../containers/Loading'; +import OnBoardingTour from '../../../containers/OnBoardingTour'; import { useUpdateEventMutation, Event as EventType,

@@ -21,13 +21,13 @@ useEventByUuidQuery,

EventByUuidDocument, EditEventInput, useFindUserVehiclesQuery, -} from '../../generated/graphql'; -import ErrorPage from '../_error'; -import useProfile from '../../hooks/useProfile'; -import Fab from '../../containers/Fab'; +} from '../../../generated/graphql'; +import ErrorPage from '../../_error'; +import useProfile from '../../../hooks/useProfile'; +import Fab from '../../../containers/Fab'; import useMediaQuery from '@material-ui/core/useMediaQuery'; -import useBannerStore from '../../stores/useBannerStore'; -import DrawerMenu from '../../containers/DrawerMenu'; +import useBannerStore from '../../../stores/useBannerStore'; +import DrawerMenu from '../../../containers/DrawerMenu'; const POLL_INTERVAL = 10000;

@@ -65,11 +65,6 @@ pollInterval: POLL_INTERVAL,

variables: {uuid: eventUUID}, }); const matches = useMediaQuery(theme.breakpoints.down('sm')); - const addCarClasses = matches ? 'tour_travel_add' : ''; - - useEffect(() => { - if (event) setEvent(event as EventType); - }, [event]); const onSave = async e => { try {

@@ -86,6 +81,10 @@ addToast(t('event.errors.cant_update'));

} }; + useEffect(() => { + if (event) setEvent(event as EventType); + }, [event]); + const addTravelClickHandler = user && vehicles?.length != 0 ? toggleVehicleChoice

@@ -103,16 +102,14 @@ >

<EventBar event={event} onAdd={setIsAddToMyEvent} onSave={onSave} /> <DrawerMenu /> <TravelColumns toggle={addTravelClickHandler} /> - <Box className={classes.bottomRight}> - <Fab - onClick={addTravelClickHandler} - aria-label="add-car" - color="primary" - className={addCarClasses} - > - {t('travel.creation.title')} - </Fab> - </Box> + <Fab + onClick={addTravelClickHandler} + aria-label="add-car" + color="primary" + className={classes.bottomRight} + > + {t('travel.creation.title')} + </Fab> <NewTravelDialog context={openNewTravelContext} toggle={() => toggleNewTravel({opened: false})}

@@ -161,12 +158,12 @@ layout: ({bannerOffset}) => ({

paddingTop: theme.mixins.toolbar.minHeight + bannerOffset, }), bottomRight: { - position: 'absolute', - bottom: theme.spacing(1), + bottom: 0, right: theme.spacing(6), - width: 200, + [theme.breakpoints.down('sm')]: { right: theme.spacing(1), + bottom: 56, }, }, }));
A frontend/pages/e/[uuid]/waitingList.tsx

@@ -0,0 +1,159 @@

+import {useState, useEffect, useMemo} from 'react'; +import {makeStyles} from '@material-ui/core/styles'; +import {useTranslation} from 'react-i18next'; +import {initializeApollo} from '../../../lib/apolloClient'; +import useToastStore from '../../../stores/useToastStore'; +import useEventStore from '../../../stores/useEventStore'; +import Layout from '../../../layouts/Default'; +import {Travel as TravelType} from '../../../generated/graphql'; +import EventBar from '../../../containers/EventBar'; +import Loading from '../../../containers/Loading'; +import { + useUpdateEventMutation, + Event as EventType, + useEventByUuidQuery, + EventByUuidDocument, + EditEventInput, + useFindUserVehiclesQuery, +} from '../../../generated/graphql'; +import ErrorPage from '../../_error'; +import useProfile from '../../../hooks/useProfile'; +import useBannerStore from '../../../stores/useBannerStore'; +import DrawerMenu from '../../../containers/DrawerMenu'; +import WaitingList from '../../../containers/WaitingList'; +import { + AddPassengerToTravel, + AddPassengerToWaitingList, +} from '../../../containers/NewPassengerDialog'; +import AddToMyEventDialog from '../../../containers/AddToMyEventDialog'; + +const POLL_INTERVAL = 10000; + +interface NewPassengerDialogContext { + addSelf: boolean; +} + +interface Props { + event: EventType; + eventUUID: string; +} + +const EventPage = props => { + const {t} = useTranslation(); + const {event} = props; + if (!event) return <ErrorPage statusCode={404} title={t`event.not_found`} />; + return <Event {...props} />; +}; + +const Event = (props: Props) => { + const {eventUUID} = props; + const bannerOffset = useBannerStore(s => s.offset); + const classes = useStyles({bannerOffset}); + const {t} = useTranslation(); + const {user} = useProfile(); + const {data: {me: {profile: {vehicles = []} = {}} = {}} = {}, loading} = + useFindUserVehiclesQuery(); + const addToast = useToastStore(s => s.addToast); + const setEvent = useEventStore(s => s.setEvent); + const eventUpdate = useEventStore(s => s.event); + const setIsEditing = useEventStore(s => s.setIsEditing); + const [isAddToMyEvent, setIsAddToMyEvent] = useState(false); + const [addPassengerToWaitingListContext, toggleNewPassengerToWaitingList] = + useState<NewPassengerDialogContext | null>(null); + const [updateEvent] = useUpdateEventMutation(); + const {data: {eventByUUID: event} = {}} = useEventByUuidQuery({ + pollInterval: POLL_INTERVAL, + variables: {uuid: eventUUID}, + }); + + useEffect(() => { + if (event) setEvent(event as EventType); + }, [event]); + + const onSave = async e => { + try { + const {uuid, ...data} = eventUpdate; + const {id, __typename, travels, users, waitingList, ...input} = data; + await updateEvent({ + variables: {uuid, eventUpdate: input as EditEventInput}, + refetchQueries: ['eventByUUID'], + }); + setIsEditing(false); + } catch (error) { + console.error(error); + addToast(t('event.errors.cant_update')); + } + }; + + const canAddSelf = useMemo(() => { + if (!user) return false; + const isInWaitingList = event?.waitingPassengers?.some( + passenger => passenger.user?.id === `${user.id}` + ); + const isInTravel = event?.travels.some(travel => + travel.passengers.some(passenger => passenger.user?.id === `${user.id}`) + ); + return !(isInWaitingList || isInTravel); + }, [event, user]); + + if (!event || loading) return <Loading />; + + return ( + <Layout + className={classes.layout} + pageTitle={t('event.title', {title: event.name})} + menuTitle={t('event.title', {title: event.name})} + displayMenu={false} + > + <EventBar event={event} onAdd={setIsAddToMyEvent} onSave={onSave} /> + <DrawerMenu /> + <WaitingList + canAddSelf={canAddSelf} + getToggleNewPassengerDialogFunction={(addSelf: boolean) => () => + toggleNewPassengerToWaitingList({addSelf})} + /> + <AddToMyEventDialog + event={event} + open={isAddToMyEvent} + onClose={() => setIsAddToMyEvent(false)} + /> + {!!addPassengerToWaitingListContext && ( + <AddPassengerToWaitingList + open={!!addPassengerToWaitingListContext} + toggle={() => toggleNewPassengerToWaitingList(null)} + addSelf={addPassengerToWaitingListContext.addSelf} + /> + )} + </Layout> + ); +}; + +export async function getServerSideProps(ctx) { + const {uuid} = ctx.query; + const apolloClient = initializeApollo(); + const {data = {}} = await apolloClient.query({ + query: EventByUuidDocument, + variables: {uuid}, + }); + const {eventByUUID: event} = data; + const {host = ''} = ctx.req.headers; + + return { + props: { + event, + eventUUID: uuid, + metas: { + title: event?.name || '', + url: `https://${host}${ctx.resolvedUrl}`, + }, + }, + }; +} + +const useStyles = makeStyles(theme => ({ + layout: ({bannerOffset}) => ({ + paddingTop: theme.mixins.toolbar.minHeight + bannerOffset, + }), +})); + +export default EventPage;
M frontend/stores/useEventStore.tsfrontend/stores/useEventStore.ts

@@ -5,6 +5,8 @@ type State = {

event: Event; setEvent: (event: Event) => void; setEventUpdate: (event: Event) => void; + areDetailsOpened: boolean; + setAreDetailsOpened: (areDetailsOpened: boolean) => void; isEditing: boolean; setIsEditing: (isEditing: boolean) => void; };

@@ -15,6 +17,13 @@ setEvent: event => set({event}),

setEventUpdate: eventUpdate => { const event = get().event; set({event: {...event, ...eventUpdate}}); + }, + areDetailsOpened: false, + setAreDetailsOpened: areDetailsOpened => { + if (!areDetailsOpened) { + set({areDetailsOpened, isEditing: false}); + } + set({areDetailsOpened}); }, isEditing: false, setIsEditing: isEditing => set({isEditing}),
M frontend/stores/useToastStore.tsfrontend/stores/useToastStore.ts

@@ -1,15 +1,18 @@

+import {ReactNode} from 'react'; import create from 'zustand'; type State = { toast?: string; - addToast: (message: string) => void; + action?: ReactNode; + addToast: (message: string, action?: ReactNode) => void; clearToast: () => void; }; const useToastStore = create<State>(set => ({ toast: null, - addToast: toast => set({toast}), - clearToast: () => set({toast: null}), + action: null, + addToast: (toast, action = null) => set({toast, action}), + clearToast: () => set({toast: null, action: null}), })); export default useToastStore;