all repos — caroster @ a60257e204f1cc253e9d375f87bc6d7ea661c1d8

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

feat: ✨ Set dialog for passenger creation

#238
Simon Mulquin simon@octree.ch
Wed, 26 Jan 2022 12:05:53 +0000
commit

a60257e204f1cc253e9d375f87bc6d7ea661c1d8

parent

5fa52dfbd467d93ed40542222227bea53bc4dee0

A frontend/containers/AddPassengerButtons/index.tsx

@@ -0,0 +1,33 @@

+import Icon from '@material-ui/core/Icon'; +import Box from '@material-ui/core/Box'; +import Button from '@material-ui/core/Button'; +import {makeStyles} from '@material-ui/core/styles'; +import { useTranslation } from 'react-i18next'; + +const AddPassengerButtons = ({toggleNewPassenger}) => { + const classes = useStyles(); + const {t} = useTranslation(); + + return ( + <Box className={classes.addButtonsContainer}> + <Button + variant="outlined" + color="primary" + fullWidth + startIcon={<Icon>person_add</Icon>} + onClick={toggleNewPassenger} + > + {t('travel.passengers.add')} + </Button> + </Box> + ); +}; + +const useStyles = makeStyles(theme => ({ + addButtonsContainer: { + padding: theme.spacing(2), + textAlign: 'center', + }, +})); + +export default AddPassengerButtons;
A frontend/containers/NewPassengerDialog/AddPassengerCommonFields.tsx

@@ -0,0 +1,68 @@

+import {Fragment} from 'react'; +import TextField from '@material-ui/core/TextField'; +import {useTranslation} from 'react-i18next'; +import Icon from '@material-ui/core/Icon'; +import Box from '@material-ui/core/Box'; +import Typography from '@material-ui/core/Typography'; +import useStyles from './useStyles'; + +interface Props { + name: string; + setName: (name: string) => void; + email: string; + setEmail: (email: string) => void; +} + +const AddPassengerCommonFields = ({name, setName, email, setEmail}: Props) => { + const {t} = useTranslation(); + const classes = useStyles(); + + return ( + <Fragment> + <Box className={classes.inputBox}> + <label htmlFor="name"> + <Typography> + <Icon className={classes.labelIcon}> + person + </Icon>{' '} + {t('travel.passengers.name')} + </Typography> + </label> + <TextField + id="PassengerName" + name="name" + value={name} + onChange={e => setName(e.target.value)} + variant="outlined" + size="small" + fullWidth + label="" + placeholder={t('travel.passengers.name_placeholder')} + /> + </Box> + <Box className={classes.inputBox}> + <label htmlFor="email"> + <Typography> + <Icon className={classes.labelIcon}> + mail_outlined + </Icon>{' '} + {t('travel.passengers.email')} + </Typography> + </label> + <TextField + id="PassengerEmail" + name="email" + value={email} + onChange={e => setEmail(e.target.value)} + variant="outlined" + size="small" + fullWidth + label="" + placeholder={t('travel.passengers.email_placeholder')} + /> + </Box> + </Fragment> + ); +}; + +export default AddPassengerCommonFields;
A frontend/containers/NewPassengerDialog/AddPassengerToTravel.tsx

@@ -0,0 +1,97 @@

+import {FormEvent, useState} from 'react'; +import Dialog from '@material-ui/core/Dialog'; +import DialogContent from '@material-ui/core/DialogContent'; +import DialogTitle from '@material-ui/core/DialogTitle'; +import Icon from '@material-ui/core/Icon'; +import {useTranslation} from 'react-i18next'; +import useAddToEvents from '../../hooks/useAddToEvents'; +import useEventStore from '../../stores/useEventStore'; +import { + useUpdateTravelMutation, + Travel as TravelType, +} from '../../generated/graphql'; +import SubmitButton from './SubmitButton'; +import Transition from './Transition'; +import AddPassengerCommonFields from './AddPassengerCommonFields'; +import useStyles from './useStyles'; + +interface Props { + travel: TravelType; + toggle: () => void; + open: boolean; +} + +const NewPassengerDialog = ({open, toggle, travel}: Props) => { + const {t} = useTranslation(); + const classes = useStyles(); + const event = useEventStore(s => s.event); + const [updateTravel] = useUpdateTravelMutation(); + const {addToEvent} = useAddToEvents(); + + // States + const [name, setName] = useState(''); + const [email, setEmail] = useState(''); + const canAddPassenger = !!name && !!email; + + const addPassenger = async (e: FormEvent) => { + e.preventDefault(); + const passenger = { + email, + name, + }; + + try { + const existingPassengers = + travel.passengers?.map(({__typename, ...item}) => item) || []; + const passengers = [...existingPassengers, passenger]; + await updateTravel({ + variables: { + id: travel.id, + travelUpdate: { + passengers, + }, + }, + }); + addToEvent(event.id); + toggle(); + } catch (error) { + console.error(error); + } + }; + + return ( + <Dialog + fullWidth + maxWidth="xs" + open={open} + onClose={toggle} + TransitionComponent={Transition} + > + <form onSubmit={addPassenger}> + <DialogTitle className={classes.title}> + {travel.vehicle.name} + <Icon + className={classes.closeIcon} + onClick={toggle} + aria-label="close" + > + close + </Icon> + </DialogTitle> + <DialogContent className={classes.dialogContent}> + <AddPassengerCommonFields + email={email} + setEmail={setEmail} + name={name} + setName={setName} + /> + <SubmitButton disabled={!canAddPassenger}> + {t('travel.passengers.add_to_car')} + </SubmitButton> + </DialogContent> + </form> + </Dialog> + ); +}; + +export default NewPassengerDialog;
A frontend/containers/NewPassengerDialog/AddPassengerToWaitingList.tsx

@@ -0,0 +1,123 @@

+import {FormEvent, useState} from 'react'; +import Dialog from '@material-ui/core/Dialog'; +import DialogContent from '@material-ui/core/DialogContent'; +import DialogTitle from '@material-ui/core/DialogTitle'; +import TextField from '@material-ui/core/TextField'; +import Box from '@material-ui/core/Box'; +import Typography from '@material-ui/core/Typography'; +import Icon from '@material-ui/core/Icon'; +import {useTranslation} from 'react-i18next'; +import useToastStore from '../../stores/useToastStore'; +import useEventStore from '../../stores/useEventStore'; +import useAddToEvents from '../../hooks/useAddToEvents'; +import SubmitButton from './SubmitButton'; +import Transition from './Transition'; +import AddPassengerCommonFields from './AddPassengerCommonFields'; +import useStyles from './useStyles'; +import {useUpdateEventMutation} from '../../generated/graphql'; + +interface Props { + toggle: () => void; + open: boolean; +} + +const NewPassengerDialog = ({ + open, + toggle, +}: Props) => { + const {t} = useTranslation(); + const classes = useStyles(); + const event = useEventStore(s => s.event); + const addToast = useToastStore(s => s.addToast); + const {addToEvent} = useAddToEvents(); + const [updateEvent] = useUpdateEventMutation(); + + // States + const [name, setName] = useState(''); + const [email, setEmail] = useState(''); + const [location, setlocation] = useState(''); + const canAddPassenger = !!name && !!email; + + const addPassenger = async (e: FormEvent) => { + e.preventDefault(); + const passenger = { + email, + name, + }; + + try { + const waitingList = [...event.waitingList, passenger].map( + ({__typename, ...item}) => item + ); + await updateEvent({ + variables: {uuid: event.uuid, eventUpdate: {waitingList}}, + refetchQueries: ['eventByUUID'], + }); + addToEvent(event.id); + toggle(); + } catch (error) { + console.error(error); + addToast(t('passenger.errors.cant_add_passenger')); + } + }; + + return ( + <Dialog + fullWidth + maxWidth="xs" + open={open} + onClose={toggle} + TransitionComponent={Transition} + > + <form onSubmit={addPassenger}> + <DialogTitle className={classes.title}> + {t('travel.passengers.register_to_waiting_list')} + <Icon + className={classes.closeIcon} + onClick={toggle} + aria-label="close" + > + close + </Icon> + </DialogTitle> + <DialogContent className={classes.dialogContent}> + <AddPassengerCommonFields + email={email} + setEmail={setEmail} + name={name} + setName={setName} + /> + <Box className={classes.inputBox}> + <label htmlFor="location"> + <Typography> + <Icon className={classes.labelIcon}> + place + </Icon>{' '} + {t('travel.passengers.location')} + </Typography> + </label> + <TextField + id="Passengerlocation" + name="location" + value={location} + onChange={e => setlocation(e.target.value)} + variant="outlined" + size="small" + fullWidth + label="" + placeholder={t('travel.passengers.location_placeholder')} + /> + <Typography variant="caption"> + {t('travel.passengers.location_helper')} + </Typography> + </Box> + <SubmitButton disabled={!canAddPassenger}> + {t('travel.passengers.add_someone')} + </SubmitButton> + </DialogContent> + </form> + </Dialog> + ); +}; + +export default NewPassengerDialog;
A frontend/containers/NewPassengerDialog/SubmitButton.tsx

@@ -0,0 +1,33 @@

+import Box from '@material-ui/core/Box'; +import Button from '@material-ui/core/Button'; +import Icon from '@material-ui/core/Icon'; +import {ReactNode} from 'react'; +import useStyles from './useStyles'; + +const SubmitButton = ({ + disabled, + children, +}: { + disabled: boolean; + children: ReactNode; +}) => { + const classes = useStyles(); + return ( + <Box className={classes.buttonBox}> + <Button + color="primary" + variant="outlined" + fullWidth + type="submit" + disabled={disabled} + aria-disabled={disabled} + id="AddPassenger" + startIcon={<Icon>person_add</Icon>} + > + {children} + </Button> + </Box> + ); +}; + +export default SubmitButton;
A frontend/containers/NewPassengerDialog/Transition.tsx

@@ -0,0 +1,8 @@

+import Slide from '@material-ui/core/Slide'; +import {forwardRef} from 'react'; + +const Transition = forwardRef(function Transition(props, ref) { + return <Slide direction="up" ref={ref} {...props} />; +}); + +export default Transition;
A frontend/containers/NewPassengerDialog/index.tsx

@@ -0,0 +1,7 @@

+import AddPassengerToTravel from './AddPassengerToTravel'; +import AddPassengerToWaitingList from './AddPassengerToWaitingList'; + +export { + AddPassengerToTravel, + AddPassengerToWaitingList +}
A frontend/containers/NewPassengerDialog/useStyles.ts

@@ -0,0 +1,33 @@

+import { makeStyles } from "@material-ui/core/styles"; + +const useStyles = makeStyles(theme => ({ + dialogContent: { + padding: theme.spacing(1, 3, 3, 3), + }, + labelIcon: { + verticalAlign: 'middle', + fontSize: '1rem' + }, + inputBox: { + padding: theme.spacing(1, 0), + }, + buttonBox: { + padding: theme.spacing(2, 0, 1, 0), + }, + title: { + textAlign: 'center', + width: '100%', + padding: theme.spacing(2, 8, 0, 8), + }, + closeIcon: { + position: 'absolute', + top: theme.spacing(2), + right: theme.spacing(2), + cursor: 'pointer', + padding: theme.spacing(0.5), + width: theme.spacing(4), + height: theme.spacing(4), + }, + })); + + export default useStyles;
M frontend/containers/PassengersList/index.tsxfrontend/containers/PassengersList/index.tsx

@@ -2,7 +2,6 @@ import List from '@material-ui/core/List';

import ListItem from '@material-ui/core/ListItem'; import {makeStyles} from '@material-ui/core/styles'; -import Input from './Input'; import Passenger from './Passenger'; import { ComponentPassengerPassenger,

@@ -30,7 +29,6 @@ icon,

onClick, onPress, disabled, - isVehicle, } = props; const classes = useStyles(); let list = passengers;

@@ -44,17 +42,6 @@ }

return ( <div className={classes.container}> - {(places - ? passengers - ? places - passengers.length > 0 - : places > 0 - : true) && ( - <Input - addPassenger={addPassenger} - id={!!places ? 'Vehicle' : 'Waiting'} - isVehicle={isVehicle} - /> - )} <List disablePadding> {!!list && list.map((passenger, index) => (

@@ -84,7 +71,7 @@ };

const useStyles = makeStyles(theme => ({ container: { - padding: theme.spacing(1, 0), + padding: theme.spacing(0, 0, 1, 0), }, }));
M frontend/containers/Travel/index.tsxfrontend/containers/Travel/index.tsx

@@ -3,14 +3,15 @@ import {makeStyles} from '@material-ui/core/styles';

import Divider from '@material-ui/core/Divider'; import Paper from '@material-ui/core/Paper'; import PassengersList from '../PassengersList'; +import AddPassengerButtons from '../AddPassengerButtons'; import HeaderEditing from './HeaderEditing'; import Header from './Header'; - +import useActions from './useActions'; import {Travel as TravelType} from '../../generated/graphql'; -import useActions from './useActions'; interface Props { travel: TravelType; + toggleNewPassenger: () => void; } const Travel = (props: Props) => {

@@ -29,6 +30,8 @@ ) : (

<Header travel={travel} toggleEditing={toggleEditing} /> )} <Divider /> + <AddPassengerButtons toggleNewPassenger={props.toggleNewPassenger} /> + <Divider /> {!isEditing && ( <PassengersList passengers={travel.passengers}

@@ -36,7 +39,6 @@ places={travel?.vehicle?.seats}

addPassenger={actions.addPassenger} onClick={actions.removePassenger} icon="close" - isTravel /> )} </Paper>
M frontend/containers/TravelColumns/index.tsxfrontend/containers/TravelColumns/index.tsx

@@ -1,10 +1,14 @@

-import {useEffect, useRef} from 'react'; +import {useEffect, useReducer, useRef, useState} from 'react'; import {makeStyles} from '@material-ui/core/styles'; import Container from '@material-ui/core/Container'; import Slider from 'react-slick'; import {Travel as TravelType} from '../../generated/graphql'; import useEventStore from '../../stores/useEventStore'; import useTourStore from '../../stores/useTourStore'; +import { + AddPassengerToTravel, + AddPassengerToWaitingList, +} from '../NewPassengerDialog'; import WaitingList from '../WaitingList'; import Travel from '../Travel'; import AddTravel from './AddTravel';

@@ -20,6 +24,9 @@ const {travels = []} = event || {};

const slider = useRef(null); const tourStep = useTourStore(s => s.step); const classes = useStyles(); + const [newPassengerTravel, toggleNewPassengerToTravel] = useState<TravelType | null>(null); + const [openNewPassengerToWaitingList, toggleNewPassengerToWaitingList] = + useReducer(i => !i, false); // On tour step changes : component update useEffect(() => {

@@ -32,7 +39,7 @@ <div className={classes.dots} id="slider-dots" />

<div className={classes.slider}> <Slider ref={slider} {...sliderSettings}> <Container maxWidth="sm" className={classes.slide}> - <WaitingList /> + <WaitingList toggleNewPassenger={toggleNewPassengerToWaitingList} /> </Container> {travels ?.slice()

@@ -43,7 +50,13 @@ key={travel.id}

maxWidth="sm" className={classes.slide} > - <Travel travel={travel} {...props} /> + <Travel + travel={travel} + {...props} + toggleNewPassenger={() => + toggleNewPassengerToTravel(travel) + } + /> </Container> ))} <Container maxWidth="sm" className={classes.slide}>

@@ -51,6 +64,17 @@ <AddTravel {...props} />

</Container> </Slider> </div> + {!!newPassengerTravel && ( + <AddPassengerToTravel + travel={newPassengerTravel} + open={!!newPassengerTravel} + toggle={() => toggleNewPassengerToTravel(null)} + /> + )} + <AddPassengerToWaitingList + open={openNewPassengerToWaitingList} + toggle={toggleNewPassengerToWaitingList} + /> </div> ); };

@@ -103,9 +127,10 @@ '& li': {

display: 'block', }, }, - '& .slick-dots li:first-child button:before, & .slick-dots li:last-child button:before': { - color: theme.palette.primary.main, - }, + '& .slick-dots li:first-child button:before, & .slick-dots li:last-child button:before': + { + color: theme.palette.primary.main, + }, }, slider: { flexGrow: 1,
M frontend/containers/WaitingList/index.tsxfrontend/containers/WaitingList/index.tsx

@@ -17,9 +17,14 @@ import useEventStore from '../../stores/useEventStore';

import useAddToEvents from '../../hooks/useAddToEvents'; import PassengersList from '../PassengersList'; import RemoveDialog from '../RemoveDialog'; +import AddPassengerButtons from '../AddPassengerButtons'; import TravelDialog from './TravelDialog'; -const WaitingList = () => { +const WaitingList = ({ + toggleNewPassenger, +}: { + toggleNewPassenger: () => void; +}) => { const classes = useStyles(); const {t} = useTranslation(); const event = useEventStore(s => s.event);

@@ -144,6 +149,8 @@ <Typography variant="overline">

{t('passenger.availability.seats', {count: availability})} </Typography> </div> + <Divider /> + <AddPassengerButtons toggleNewPassenger={toggleNewPassenger} /> <Divider /> <PassengersList passengers={event.waitingList}
M frontend/locales/en.jsonfrontend/locales/en.json

@@ -140,14 +140,25 @@ "removed": "The car has been removed"

}, "passengers": { "empty": "Available seat", - "add": "Add a passenger" + "add": "Add a passenger", + "add_to_car": "Add to car", + "register_to_waiting_list": "Register to waiting list", + "add_someone": "Add someone", + "location": "Meeting place", + "location_helper": "Indicate your preferred departure location", + "location_placeholder": "Meeting place (optionnal)", + "email": "Email", + "email_placeholder": "email", + "name": "Name", + "name_placeholder": "name" }, "errors": { "cant_create": "Unable to create the car", "cant_update": "Unable to modify the car", "cant_remove": "Unable to remove the car", "cant_add_passenger": "Unable to add a passenger", - "cant_remove_passenger": "Unable to remove passenger" + "cant_remove_passenger": "Unable to remove passenger", + "add_someone": "Add someone" } }, "dashboard": {
M frontend/locales/fr.jsonfrontend/locales/fr.json

@@ -140,7 +140,17 @@ "removed": "La voiture a été supprimée"

}, "passengers": { "empty": "Place disponible", - "add": "Ajouter un passager" + "add": "Ajouter un passager", + "add_to_car": "Ajouter à la voiture", + "register_to_waiting_list": "Inscription à la liste d'attente", + "add_someone": "Ajouter quelqu'un", + "location": "Lieu de rencontre", + "location_helper": "Indiquez votre lieu de départ de préférence", + "location_placeholder": "Lieu de rencontre (optionnel)", + "email": "Email", + "email_placeholder": "Email", + "name": "Nom", + "name_placeholder": "Nom" }, "errors": { "cant_create": "Impossible de créer la voiture",