all repos — caroster @ 6fc46298959978b218a66da7352e0fae5f470e4c

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

binding for random car value, NewCar page setup. Related #5
Hadrien Froger hadrien@octree.ch
Wed, 01 Jul 2020 14:35:25 +0100
commit

6fc46298959978b218a66da7352e0fae5f470e4c

parent

e3f8c146e8fd76e38f0100b8d59720069ebdde4e

M app/package-lock.jsonapp/package-lock.json

@@ -8313,6 +8313,11 @@ "requires": {

"object-visit": "^1.0.0" } }, + "marked": { + "version": "1.1.0", + "resolved": "https://npm-8ee.hidora.com/marked/-/marked-1.1.0.tgz", + "integrity": "sha512-EkE7RW6KcXfMHy2PA7Jg0YJE1l8UPEZE8k45tylzmZM30/r1M1MUXWQfJlrSbsTeh7m/XTwHbWUENvAJZpp1YA==" + }, "md5.js": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz",
M app/package.jsonapp/package.json

@@ -13,6 +13,7 @@ "@testing-library/user-event": "^7.2.1",

"fontsource-roboto": "^2.1.4", "i18next": "^19.5.1", "leaflet": "^1.6.0", + "marked": "^1.1.0", "moment": "^2.27.0", "react": "^16.13.1", "react-dom": "^16.13.1",
M app/src/App.jsapp/src/App.js

@@ -8,7 +8,7 @@ import CssBaseline from "@material-ui/core/CssBaseline";

import Router from "./Router"; import theme from "./theme"; -const models = [{ name: "events" }, { name: "cars" }]; +const models = [{ name: "events" }, { name: "cars" }, { name: "pages" }]; const App = () => { return (
M app/src/components/Paper/index.jsapp/src/components/Paper/index.js

@@ -2,17 +2,17 @@ import React from "react";

import PaperMUI from "@material-ui/core/Paper"; import { makeStyles } from "@material-ui/core/styles"; -const useStyles = makeStyles((theme) => ({ - root: { - padding: theme.spacing(2), - }, -})); - const Paper = ({ className, ...props }) => { const classes = useStyles(); return ( <PaperMUI classes={{ root: classes.root, parent: className }} {...props} /> ); }; + +const useStyles = makeStyles((theme) => ({ + root: { + padding: theme.spacing(2), + }, +})); export default Paper;
M app/src/containers/CarColumns/AddCar.jsapp/src/containers/CarColumns/AddCar.js

@@ -1,7 +1,26 @@

import React from "react"; +import Button from "@material-ui/core/Button"; +import Container from "@material-ui/core/Container"; +import { makeStyles } from "@material-ui/core"; +import { useTranslation } from "react-i18next"; -const AddCar = () => { - return <div>Add car</div>; +const AddCar = ({ toggleNewCar }) => { + const classes = useStyles(); + const { t } = useTranslation(); + return ( + <Container maxWidth="sm" className={classes.container}> + <Button variant="contained" onClick={toggleNewCar}> + {t("car.creation.title")} + </Button> + </Container> + ); }; + +const useStyles = makeStyles((theme) => ({ + container: { + display: "flex", + justifyContent: "center", + }, +})); export default AddCar;
M app/src/containers/CarColumns/Car.jsapp/src/containers/CarColumns/Car.js

@@ -1,43 +1,85 @@

import React from "react"; import Typography from "@material-ui/core/Typography"; import { makeStyles } from "@material-ui/core/styles"; +import Divider from "@material-ui/core/Divider"; +import Paper from "@material-ui/core/Paper"; import { useTranslation } from "react-i18next"; +import { useStrapi } from "strapi-react-context"; import moment from "moment"; -import Paper from "../../components/Paper"; +import PassengersList from "../PassengersList"; +import { useToast } from "../../contexts/Toast"; const Car = ({ car }) => { const classes = useStyles(); const { t } = useTranslation(); + const { addToast } = useToast(); + const strapi = useStrapi(); if (!car) return null; + const addPassenger = async (passenger) => { + try { + await strapi.services.cars.update(car.id, { + passengers: [...(car.passengers || []), passenger], + }); + } catch (error) { + console.error(error); + addToast(t("car.errors.cant_add_passenger")); + } + }; + + const removePassenger = async (passenger) => { + if (!car?.passengers) return false; + try { + return await strapi.services.cars.update(car.id, { + passengers: car.passengers.filter((pssngr) => passenger !== pssngr), + }); + } catch (error) { + console.error(error); + addToast(t("car.errors.cant_remove_passenger")); + return false; + } + }; + return ( <Paper> - {!!car.departure && ( - <Typography variant="overline"> - {moment(car.departure).format("LLLL")} - </Typography> - )} - <Typography variant="h5">{car.name}</Typography> - {!!car.meeting && ( - <div className={classes.section}> - <Typography variant="subtitle2"> - {t("car.fields.meeting_point")} + <div className={classes.header}> + {!!car.departure && ( + <Typography variant="overline"> + {moment(car.departure).format("LLLL")} </Typography> - <Typography variant="body2">{car.meeting}</Typography> - </div> - )} - {!!car.details && ( - <div className={classes.section}> - <Typography variant="subtitle2">{t("car.fields.details")}</Typography> - <Typography variant="body2">{car.details}</Typography> - </div> - )} + )} + <Typography variant="h5">{car.name}</Typography> + {!!car.meeting && ( + <div className={classes.section}> + <Typography variant="subtitle2"> + {t("car.fields.meeting_point")} + </Typography> + <Typography variant="body2">{car.meeting}</Typography> + </div> + )} + {!!car.details && ( + <div className={classes.section}> + <Typography variant="subtitle2"> + {t("car.fields.details")} + </Typography> + <Typography variant="body2">{car.details}</Typography> + </div> + )} + </div> + <Divider /> + <PassengersList + passengers={car.passengers} + places={car.seats} + addPassenger={addPassenger} + removePassenger={removePassenger} + /> </Paper> ); }; const useStyles = makeStyles((theme) => ({ + header: { padding: theme.spacing(2) }, section: { marginTop: theme.spacing(2), },
M app/src/containers/CarColumns/index.jsapp/src/containers/CarColumns/index.js

@@ -1,9 +1,11 @@

-import React from "react"; +import React, { useMemo } from "react"; import Slider from "react-slick"; import Container from "@material-ui/core/Container"; import { makeStyles } from "@material-ui/core/styles"; import Car from "./Car"; import AddCar from "./AddCar"; +import { useEvent } from "../../contexts/Event"; +import { useStrapi } from "strapi-react-context"; const settings = { dots: false,

@@ -22,21 +24,46 @@ settings: {

slidesToShow: 1, }, }, + { + breakpoint: 1200, + settings: { + slidesToShow: 3, + }, + }, ], }; -const CarColumns = ({ cars = [] }) => { +const sortCars = (a, b) => { + const dateA = new Date(a.departure).getTime(); + const dateB = new Date(b.departure).getTime(); + if (dateA === dateB) return new Date(a.createdAt) - new Date(b.createdAt); + else return dateA - dateB; +}; + +const CarColumns = ({ ...props }) => { const classes = useStyles(); + const strapi = useStrapi(); + const { event } = useEvent(); + + const cars = useMemo( + () => + strapi.stores.cars + ?.filter((car) => car?.event?.id === event?.id) + .sort(sortCars), + [strapi.stores.cars, event] + ); + return ( - <div> + <div className={classes.root}> <Slider {...settings}> - {cars.map((car) => ( - <Container key={car.id} maxWidth="sm" className={classes.slide}> - <Car car={car} /> - </Container> - ))} + {cars && + cars.map((car) => ( + <Container key={car.id} maxWidth="sm" className={classes.slide}> + <Car car={car} {...props} /> + </Container> + ))} <Container maxWidth="sm" className={classes.slide}> - <AddCar /> + <AddCar {...props} /> </Container> </Slider> </div>

@@ -44,10 +71,12 @@ );

}; const useStyles = makeStyles((theme) => ({ + root: {}, slide: { height: `calc(100vh - ${theme.mixins.toolbar.minHeight}px)`, outline: "none", padding: theme.spacing(2), + marginBottom: theme.spacing(4), }, }));
M app/src/containers/CreateEvent/Step1.jsapp/src/containers/CreateEvent/Step1.js

@@ -1,10 +1,13 @@

-import React, { useState, useEffect } from "react"; +import React, { useState, useEffect, useReducer } from "react"; import { makeStyles } from "@material-ui/core/styles"; import TextField from "@material-ui/core/TextField"; import Button from "@material-ui/core/Button"; +import Typography from "@material-ui/core/Typography"; +import Checkbox from "@material-ui/core/Checkbox"; import { useTranslation } from "react-i18next"; import useDebounce from "../../hooks/useDebounce"; import Paper from "../../components/Paper"; +import TosDialog from "../TosDialog"; const isValidEmail = (email) => // eslint-disable-next-line

@@ -20,6 +23,8 @@ // States

const [name, setName] = useState(event.name ?? ""); const [email, setEmail] = useState(event.email ?? ""); const [emailIsValid, setEmailIsValid] = useState(false); + const [tos, setTos] = useState(false); + const [showTos, toggleTos] = useReducer((i) => !i, false); const debouncedEmail = useDebounce(email, 400); useEffect(() => {

@@ -32,40 +37,54 @@ nextStep();

}; return ( - <Paper {...props}> - <TextField - className={classes.textField} - label={t("event.creation.event_name")} - fullWidth - autoFocus - margin="dense" - value={name} - onChange={(e) => setName(e.target.value)} - id="NewEventName" - name="name" - /> - <TextField - className={classes.textField} - label={t("event.creation.creator_email")} - fullWidth - margin="dense" - value={email} - onChange={(e) => setEmail(e.target.value)} - id="NewEventEmail" - name="email" - type="email" - /> - <Button - className={classes.button} - variant="contained" - color="secondary" - fullWidth - onClick={onNext} - disabled={!name || !email || !emailIsValid} - > - {t("event.creation.next")} - </Button> - </Paper> + <> + <Paper {...props}> + <TextField + className={classes.textField} + label={t("event.creation.event_name")} + fullWidth + autoFocus + margin="dense" + value={name} + onChange={(e) => setName(e.target.value)} + id="NewEventName" + name="name" + /> + <TextField + className={classes.textField} + label={t("event.creation.creator_email")} + fullWidth + margin="dense" + value={email} + onChange={(e) => setEmail(e.target.value)} + id="NewEventEmail" + name="email" + type="email" + /> + <div className={classes.tos}> + <Checkbox + name="tos" + id="NewEventTos" + checked={tos} + onChange={(e) => setTos(e.target.checked)} + /> + <Typography variant="caption" onClick={toggleTos}> + {t("event.creation.tos")} + </Typography> + </div> + <Button + className={classes.button} + variant="contained" + color="secondary" + fullWidth + onClick={onNext} + disabled={!name || !email || !emailIsValid || !tos} + > + {t("event.creation.next")} + </Button> + </Paper> + <TosDialog open={showTos} toggle={toggleTos} /> + </> ); };

@@ -73,6 +92,13 @@ const useStyles = makeStyles((theme) => ({

textField: {}, button: { marginTop: theme.spacing(2), + }, + tos: { + cursor: "pointer", + marginTop: theme.spacing(2), + display: "flex", + alignItems: "center", + marginLeft: "-11px", }, }));
M app/src/containers/CreateEvent/Step2.jsapp/src/containers/CreateEvent/Step2.js

@@ -60,7 +60,7 @@ fullWidth

onClick={onCreate} id="NewEventSubmit" > - {t("event.creation.create")} + {t("generic.create")} </Button> </Paper> );
A app/src/containers/EventFab/index.js

@@ -0,0 +1,98 @@

+import React, { useReducer } from "react"; +import Icon from "@material-ui/core/Icon"; +import Fab from "@material-ui/core/Fab"; +import { makeStyles } from "@material-ui/core/styles"; +import { useTranslation } from "react-i18next"; +import { useEvent } from "../../contexts/Event"; +import { useToast } from "../../contexts/Toast"; + +const EventFab = ({ toggleNewCar }) => { + const { t } = useTranslation(); + const [open, toggleOpen] = useReducer((i) => !i, false); + const classes = useStyles({ open }); + const { event } = useEvent(); + const { addToast } = useToast(); + + const onShare = async () => { + if (!event) return null; + // If navigator as share capability + if (!!navigator.share) { + const shareData = { + title: `Caroster ${event.name}`, + url: `${window.location.href}`, + }; + return await navigator.share(shareData); + } + // Else copy URL in clipboard + else if (!!navigator.clipboard) { + await navigator.clipboard.writeText(window.location.href); + addToast(t("event.actions.copied")); + return true; + } + }; + + return ( + <> + <div className={classes.fabContainer}> + <Fab color="primary" aria-label="more" onClick={toggleOpen}> + <Icon>add</Icon> + </Fab> + </div> + <div className={classes.actionContainer} onClick={toggleOpen}> + <Fab + color="primary" + aria-label="share" + variant="extended" + className={classes.extendedFab} + onClick={onShare} + > + <Icon className={classes.extendedIcon}>share</Icon> + Partager + </Fab> + <Fab + color="secondary" + aria-label="add-car" + variant="extended" + className={classes.extendedFab} + onClick={toggleNewCar} + > + <Icon className={classes.extendedIcon}>directions_car</Icon> + Ajouter + </Fab> + </div> + </> + ); +}; + +const useStyles = makeStyles((theme) => ({ + fabContainer: ({ open }) => ({ + position: "fixed", + bottom: open ? "-4rem" : theme.spacing(2), + right: theme.spacing(2), + transition: "all 0.3s ease", + transform: open ? "rotate(45deg)" : "", + zIndex: theme.zIndex.speedDial, + }), + actionContainer: ({ open }) => ({ + position: "fixed", + bottom: open ? 0 : "-100vh", + left: 0, + right: 0, + display: "flex", + flexDirection: "column", + justifyContent: "flex-end", + transition: "all 0.3s ease", + height: "100vh", + }), + extendedIcon: { + marginRight: theme.spacing(2), + }, + extendedFab: { + width: "11rem", + marginLeft: "auto", + marginRight: "auto", + marginBottom: theme.spacing(2), + }, +})); + +export default EventFab;
A app/src/containers/NewCarDialog/index.js

@@ -0,0 +1,161 @@

+import React, { useState } from "react"; +import Dialog from "@material-ui/core/Dialog"; +import DialogActions from "@material-ui/core/DialogActions"; +import DialogContent from "@material-ui/core/DialogContent"; +import DialogContentText from "@material-ui/core/DialogContentText"; +import DialogTitle from "@material-ui/core/DialogTitle"; +import Button from "@material-ui/core/Button"; +import Slide from "@material-ui/core/Slide"; +import moment from "moment"; +import { useStrapi } from "strapi-react-context"; +import { useTranslation } from "react-i18next"; +import TextField from "@material-ui/core/TextField"; +import Slider from "@material-ui/core/Slider"; +import Typography from "@material-ui/core/Typography"; +import { makeStyles } from "@material-ui/core/styles"; +import { DateTimePicker } from "@material-ui/pickers"; +import { useToast } from "../../contexts/Toast"; +import { useEvent } from "../../contexts/Event"; + +const Transition = React.forwardRef(function Transition(props, ref) { + return <Slide direction="up" ref={ref} {...props} />; +}); + +const marks = [1, 2, 3, 4, 5, 6, 7, 8].map((value) => ({ + value, + label: value, +})); + +const NewCarDialog = ({ open, toggle }) => { + const strapi = useStrapi(); + const { t } = useTranslation(); + const classes = useStyles(); + const { addToast } = useToast(); + const { event } = useEvent(); + + // States + const [name, setName] = useState(""); + const [seats, setSeats] = useState(4); + const [meeting, setMeeting] = useState(""); + const [date, setDate] = useState(moment()); + const [phone, setPhone] = useState(""); + const [details, setDetails] = useState(""); + + const canCreate = !!name && !!seats; + + const onCreate = async () => { + try { + await strapi.services.cars.create({ + name, + seats, + meeting, + departure: date.toISOString(), + phone_number: phone, + details, + event: event.id, + }); + addToast(t("car.creation.created")); + toggle(); + return true; + } catch (error) { + console.error(error); + addToast(t("car.errors.cant_create")); + return false; + } + }; + + return ( + <Dialog + open={open} + TransitionComponent={Transition} + onClose={toggle} + fullWidth + maxWidth="sm" + > + <DialogTitle>{t("car.creation.title")}</DialogTitle> + <DialogContent> + <DialogContentText> + <TextField + className={classes.textField} + label={t("car.creation.name")} + fullWidth + autoFocus + margin="dense" + value={name} + onChange={(e) => setName(e.target.value)} + id="NewCarName" + name="name" + /> + <Typography variant="caption">{t("car.creation.seats")}</Typography> + <Slider + value={seats} + onChange={(e, value) => setSeats(value)} + step={1} + marks={marks} + min={1} + max={marks.length} + valueLabelDisplay="auto" + /> + <TextField + className={classes.textField} + label={t("car.creation.meeting")} + fullWidth + margin="dense" + multiline + rows={2} + value={meeting} + onChange={(e) => setMeeting(e.target.value)} + id="NewCarMeeting" + name="meeting" + /> + <DateTimePicker + label={t("event.creation.date")} + value={date} + onChange={setDate} + className={classes.textField} + fullWidth + format="LLLL" + disablePast + id="NewCarDateTime" + name="date" + /> + <TextField + className={classes.textField} + label={t("car.creation.phone")} + fullWidth + autoFocus + margin="dense" + value={phone} + onChange={(e) => setPhone(e.target.value)} + id="NewCarPhone" + name="phone" + /> + <TextField + className={classes.textField} + label={t("car.creation.notes")} + fullWidth + margin="dense" + multiline + rows={2} + value={details} + onChange={(e) => setDetails(e.target.value)} + id="NewCarDetails" + name="details" + /> + </DialogContentText> + </DialogContent> + <DialogActions> + <Button onClick={toggle}>{t("generic.cancel")}</Button> + <Button variant="contained" onClick={onCreate} disabled={!canCreate}> + {t("generic.create")} + </Button> + </DialogActions> + </Dialog> + ); +}; + +const useStyles = makeStyles((theme) => ({ + textField: { marginBottom: theme.spacing(2) }, +})); + +export default NewCarDialog;
A app/src/containers/PassengersList/Input.js

@@ -0,0 +1,45 @@

+import React, { useState } from "react"; +import TextField from "@material-ui/core/TextField"; +import { useTranslation } from "react-i18next"; +import { makeStyles } from "@material-ui/core/styles"; +import Divider from "@material-ui/core/Divider"; + +const Input = ({ addPassenger }) => { + const classes = useStyles(); + const [name, setName] = useState(""); + const { t } = useTranslation(); + + const onSave = () => { + if (!!name) { + addPassenger(name); + setName(""); + } + }; + + const onKeyDown = (e) => { + if (e.keyCode === 13) onSave(); + }; + + return ( + <> + <div className={classes.container}> + <TextField + value={name} + onChange={(e) => setName(e.target.value)} + onKeyDown={onKeyDown} + fullWidth + label={t("car.passengers.add")} + id="NewPassenger" + name="passenger" + /> + </div> + <Divider /> + </> + ); +}; + +const useStyles = makeStyles((theme) => ({ + container: { padding: theme.spacing(0, 2, 2) }, +})); + +export default Input;
A app/src/containers/PassengersList/Passenger.js

@@ -0,0 +1,41 @@

+import React from "react"; +import { makeStyles } from "@material-ui/core/styles"; +import Typography from "@material-ui/core/Typography"; +import IconButton from "@material-ui/core/IconButton"; +import Icon from "@material-ui/core/Icon"; +import { useTranslation } from "react-i18next"; + +const Passenger = ({ passenger, removePassenger }) => { + const classes = useStyles(); + const { t } = useTranslation(); + if (!!passenger) + return ( + <div className={classes.item}> + <Typography variant="body2" className={classes.name}> + {passenger} + </Typography> + <IconButton + edge="end" + size="small" + onClick={() => removePassenger(passenger)} + > + <Icon>close</Icon> + </IconButton> + </div> + ); + else return <div className={classes.item}>{t("car.passengers.empty")}</div>; +}; + +const useStyles = makeStyles((theme) => ({ + item: { + padding: theme.spacing(1, 2), + display: "flex", + alignItems: "center", + height: "46px", + }, + name: { + flexGrow: 1, + }, +})); + +export default Passenger;
A app/src/containers/PassengersList/index.js

@@ -0,0 +1,39 @@

+import React from "react"; +import { makeStyles } from "@material-ui/core/styles"; +import Passenger from "./Passenger"; +import Input from "./Input"; + +const PassengersList = ({ + passengers, + places = 0, + addPassenger, + removePassenger, +}) => { + const classes = useStyles(); + + const emptyList = Array.apply(null, Array(places)); + const list = Array.isArray(passengers) + ? emptyList.map((u, index) => passengers[index]) + : emptyList; + const emptyPlaces = !!passengers ? places - passengers.length : places; + + return ( + <div className={classes.container}> + {emptyPlaces > 0 && <Input addPassenger={addPassenger} />} + {!!places && + list.map((passenger, index) => ( + <Passenger + key={index} + passenger={passenger} + removePassenger={removePassenger} + /> + ))} + </div> + ); +}; + +const useStyles = makeStyles((theme) => ({ + container: { padding: theme.spacing(1, 0) }, +})); + +export default PassengersList;
A app/src/containers/TosDialog/index.js

@@ -0,0 +1,49 @@

+import React, { useEffect } from "react"; +import Dialog from "@material-ui/core/Dialog"; +import DialogActions from "@material-ui/core/DialogActions"; +import DialogContent from "@material-ui/core/DialogContent"; +import DialogContentText from "@material-ui/core/DialogContentText"; +import DialogTitle from "@material-ui/core/DialogTitle"; +import Button from "@material-ui/core/Button"; +import Slide from "@material-ui/core/Slide"; +import { useStrapi } from "strapi-react-context"; +import marked from "marked"; +import { useTranslation } from "react-i18next"; + +const Transition = React.forwardRef(function Transition(props, ref) { + return <Slide direction="up" ref={ref} {...props} />; +}); + +const TosDialog = ({ open, toggle }) => { + const strapi = useStrapi(); + const { t } = useTranslation(); + const page = strapi.stores?.pages?.find(({ type }) => type === "tos"); + + useEffect(() => { + strapi.services.pages.find({ type: "tos" }); + }, [strapi.services.pages]); + + return ( + <Dialog + open={open} + TransitionComponent={Transition} + onClose={toggle} + fullWidth + maxWidth="sm" + > + <DialogTitle>{page?.name}</DialogTitle> + <DialogContent> + {page && ( + <DialogContentText + dangerouslySetInnerHTML={{ __html: marked(page.content) }} + /> + )} + </DialogContent> + <DialogActions> + <Button onClick={toggle}>{t("generic.close")}</Button> + </DialogActions> + </Dialog> + ); +}; + +export default TosDialog;
M app/src/contexts/Event.jsapp/src/contexts/Event.js

@@ -23,6 +23,11 @@ if (!strapi.stores.events?.find(({ id }) => eventId === id))

strapi.services.events.findOne(eventId); }, [eventId, strapi.stores.events, strapi.services.events]); + // Fetch event cars on load + useEffect(() => { + strapi.services.cars.find({ event: eventId }); + }, [eventId]); // eslint-disable-line react-hooks/exhaustive-deps + // Retrieve event data const event = useMemo( () => strapi.stores.events?.find((e) => e.id === eventId),
M app/src/locales/fr.jsonapp/src/locales/fr.json

@@ -1,5 +1,10 @@

{ - "generic": { "loading": "Chargement..." }, + "generic": { + "loading": "Chargement...", + "close": "Fermer", + "create": "Créer", + "cancel": "Annuler" + }, "event": { "fields": { "starts_on": "Commence le",

@@ -11,14 +16,15 @@ "creator_email": "Votre e-mail",

"date": "Date de l'événement", "address": "Adresse de l'événement", "next": "Suivant", - "create": "Créer" + "tos": "J'accepte les conditions générales d'utilisation" }, "actions": { "show_details": "Afficher les détails", "hide_details": "Cacher les détails", "add_car": "Ajouter une voiture", "invite": "Inviter", - "find_car": "Trouver une voiture" + "find_car": "Trouver une voiture", + "copied": "Le lien a été copié dans votre presse-papier" }, "errors": { "cant_create": "Impossible de créer l'événement",

@@ -29,6 +35,25 @@ "car": {

"fields": { "meeting_point": "Lieu de rencontre", "details": "Notes" + }, + "creation": { + "title": "Ajouter une voiture", + "name": "Nom de la voiture", + "seats": "Nombre de places", + "meeting": "Lieu de rencontre", + "phone": "Numéro de téléphone", + "notes": "Infos complémentaires", + "created": "La voiture a été créée" + }, + "passengers": { + "empty": "Place disponible", + "add": "Ajouter un passager" + }, + "errors": { + "cant_create": "Impossible de créer la voiture", + "cant_update": "Impossible de modifier la voiture", + "cant_add_passenger": "Impossible d'ajouter un passager", + "cant_remove_passenger": "Impossible de supprimer le passager" } } }
M app/src/pages/Event.jsapp/src/pages/Event.js

@@ -11,9 +11,11 @@ import { makeStyles } from "@material-ui/core/styles";

import Layout from "../layouts/Default"; import EventMenu from "../containers/EventMenu"; import EventDetails from "../containers/EventDetails"; +import EventFab from "../containers/EventFab"; import { useEvent, EventProvider } from "../contexts/Event"; import CarColumns from "../containers/CarColumns"; import { useToast } from "../contexts/Toast"; +import NewCarDialog from "../containers/NewCarDialog"; const Event = () => { const { t } = useTranslation();

@@ -21,6 +23,7 @@ const { addToast } = useToast();

const [anchorEl, setAnchorEl] = useState(null); const [detailsOpen, toggleDetails] = useReducer((i) => !i, false); const classes = useStyles({ detailsOpen }); + const [openNewCar, toggleNewCar] = useReducer((i) => !i, false); const { event, isEditing,

@@ -32,7 +35,7 @@ } = useEvent();

useEffect(() => { if (!detailsOpen) setIsEditing(false); - }, [detailsOpen]); + }, [detailsOpen]); // eslint-disable-line react-hooks/exhaustive-deps const onEventSave = async (e) => { try {

@@ -51,7 +54,7 @@ <Layout>

<AppBar position="static" className={classes.appbar} - id={isEditing ? "EditEvent" : detailsOpen ? "Details" : "Menu"} + id={(isEditing && "EditEvent") || (detailsOpen && "Details") || "Menu"} > <Toolbar> {isEditing ? (

@@ -109,7 +112,7 @@ id: "DetailsTab",

}, { label: t("event.actions.add_car"), - onClick: () => {}, + onClick: toggleNewCar, id: "NewCarTab", }, {

@@ -124,7 +127,9 @@ <Container className={classes.container} maxWidth="sm">

<EventDetails toggleDetails={toggleDetails} /> </Container> </AppBar> - <CarColumns cars={event.cars} /> + <CarColumns toggleNewCar={toggleNewCar} /> + <EventFab toggleNewCar={toggleNewCar} /> + <NewCarDialog open={openNewCar} toggle={toggleNewCar} /> </Layout> ); };

@@ -135,6 +140,7 @@ appbar: ({ detailsOpen }) => ({

transition: "height 0.3s ease", overflow: "hidden", height: detailsOpen ? "100vh" : theme.mixins.toolbar.minHeight, + zIndex: theme.zIndex.appBar, }), name: { flexGrow: 1,
M app/src/theme.jsapp/src/theme.js

@@ -1,10 +1,13 @@

import { createMuiTheme } from "@material-ui/core/styles"; +import grey from "@material-ui/core/colors/grey"; import blue from "@material-ui/core/colors/blue"; export default createMuiTheme({ palette: { + primary: grey, + secondary: blue, background: { - default: blue[400], + default: grey[50], }, }, });
M e2e/conf/chrome.jse2e/conf/chrome.js

@@ -51,7 +51,7 @@ strict: true,

tags: [], timeout: 100000, ignoreUndefinedDefinitions: false, - tagExpression: '', + tagExpression: 'not @skip', }, specs: ['test/features/**/*.feature'], maximizeWindow: true,
A e2e/test/features/Cars/NewCar.feature

@@ -0,0 +1,15 @@

+@GIT.2 +Feature: Car Creation + Background: + Given I am vistor + And I have created an event + Scenario: I can create a car + When I am on the event page + And I go to the new car page + And I type my car name + And I pick my car seats + And I type my car meeting + And I type my car event date + And I type my phone + And I type my car details + And I submit the form
M e2e/test/features/Events/NewEvent.featuree2e/test/features/Events/NewEvent.feature

@@ -6,6 +6,7 @@ Scenario: I can create an event

When I am on the homepage And I type my event name And I type my email + And I click accept the Tos And I submit the form And I type my event date And I type my event address
M e2e/test/features/Events/SeeEvent.featuree2e/test/features/Events/SeeEvent.feature

@@ -1,4 +1,4 @@

-@GIT.5 +@GIT.5 and @skip Feature: Event Creation Background: Given I am vistor
M e2e/test/pages/_page.jse2e/test/pages/_page.js

@@ -65,7 +65,6 @@ * @return {Promise<void>}

*/ async click(selector) { const field = this.field(selector); - await $(field).waitForDisplayed(); const element = await $(field); await element.click(); }
M e2e/test/pages/eventPage.jse2e/test/pages/eventPage.js

@@ -1,5 +1,6 @@

import {Page} from './_page'; import {EventDetails} from './eventDetails'; +import {NewCar} from './newCar'; class _EventPage extends Page { /** *

@@ -43,6 +44,11 @@ const tab = await $(this.field('detail tab'));

await tab.click(); global.SCENE.scene = EventDetails; await EventDetails.waitForDisplayed(); + return; + case 'new car': + const tab = await $(this.field('new car tab')); + await tab.click(); + global.SCENE.scene = NewCar; } }
A e2e/test/pages/newCar.js

@@ -0,0 +1,47 @@

+import {Page} from './_page'; +class _NewCar extends Page { + /** + * + * @param {string} selector + * @return {string} + */ + field(selector) { + switch (selector) { + case 'form': + return '#NewCar'; + case 'name': + case 'my car name': + return '#NewCarName'; + case 'seats': + case 'my car seats': + return '.MuiSlider-marked .MuiSlider-markLabel[aria-hidden=true]'; + case 'meeting': + case 'my car meeting': + return '#NewCarMeeting'; + case 'event date': + case 'my car event date': + return '#NewCarDateTime'; + case 'phone': + case 'my phone': + return '#NewCarPhone'; + case 'car details': + case 'my car details': + return '#NewCarDetails'; + case 'submit': + return 'button.MuiButton-root.MuiButton-contained'; + } + throw new Error('Unknown selector ' + selector); + } + + get name() { + return 'NewCar'; + } + + async submit() { + await new Promise(resolve => { + setTimeout(resolve, 1000); + }); + await super.submit(); + } +} +export const NewCar = new _NewCar('/');
M e2e/test/pages/newEvent.jse2e/test/pages/newEvent.js

@@ -13,6 +13,9 @@ return '#NewEvent';

case 'event name': case 'my event name': return '#NewEventName'; + case 'tos': + case 'accept the Tos': + return '#NewEventTos'; case 'email': case 'my email': return '#NewEventEmail';
M e2e/test/pages/utils/cast.jse2e/test/pages/utils/cast.js

@@ -4,19 +4,37 @@ * @param {string} value

* @return {string|undefined} */ export const cast = function (value) { + const num = x => (x < 10 ? `0${x}` : `${x}`); + let month; + let day; + let year; switch (value) { case 'my email': return global.SCENE.actor.email; case 'my event address': return global.SCENE.event.address; case 'my event date': - const month = global.SCENE.event.date.getMonth(); - const day = global.SCENE.event.date.getDay(); - const year = global.SCENE.event.date.getYear(); - const num = x => (x < 10 ? `0${x}` : `${x}`); + month = global.SCENE.event.date.getMonth(); + day = global.SCENE.event.date.getDate(); + year = global.SCENE.event.date.getYear(); return `${num(day)}.${num(month)}.${year}`; case 'my event name': return global.SCENE.event.name; + case 'my car name': + return global.SCENE.car.name; + case 'my car seats': + return '' + global.SCENE.car.seats; + case 'my car meeting': + return '' + global.SCENE.car.meeting; + case 'my car event date': + month = global.SCENE.car.meeting_date.getMonth(); + day = global.SCENE.car.meeting_date.getDate(); + year = global.SCENE.car.meeting_date.getYear(); + return `${num(day)}.${num(month)}.${year}`; + case 'my phone': + return global.SCENE.actor.phone; + case 'my car infos': + return global.SCENE.actor.phone; } return value;
M e2e/test/pages/utils/scene.jse2e/test/pages/utils/scene.js

@@ -1,11 +1,28 @@

/* eslint-disable class-methods-use-this */ const faker = require('faker'); const axios = require('axios'); + +function randomPhone() { + return `${faker.random.arrayElement([ + '76', + '78', + '79', + '77', + ])}${faker.random.number({ + min: 100, + max: 999, + })}${faker.random.number({ + min: 10, + max: 99, + })}${faker.random.number({min: 10, max: 99})}`; +} + class _Scene { constructor() { this.scene = undefined; this.actor = this.randomActor(); this.event = this.randomEvent(); + this.car = this.randomCar(); } /**

@@ -14,9 +31,12 @@ */

randomActor() { return { email: faker.internet.email(), + phone: faker.random.arrayElement(['+41', '0']) + randomPhone(), }; } - + /** + * @return {Object} + */ randomEvent() { return { address: `${faker.address.streetAddress()}, ${faker.address.zipCode()} ${faker.address.city()}`,

@@ -24,6 +44,20 @@ date: faker.date.future(),

name: `${faker.hacker.noun()} ${faker.hacker.verb()} ${faker.hacker.adjective()}`, }; } + + /** + * @return {Object} + */ + randomCar() { + return { + name: `${faker.hacker.verb()} ${faker.commerce.productName()}`, + seats: faker.random.number({min: 1, max: 8}), + meeting: `${faker.address.streetAddress()}, ${faker.address.zipCode()} ${faker.address.city()}`, + meeting_date: faker.date.future(), + details: faker.lorem.paragraph(), + }; + } + /** * Create an event and set an event id. * @return {string} the new event id
M e2e/test/steps/when.steps.jse2e/test/steps/when.steps.js

@@ -16,6 +16,10 @@ When(/^I type (.+)$/, {}, async field => {

await global.SCENE.scene.type(field); }); +When(/^I click (.+)$/, {}, async field => { + await global.SCENE.scene.click(field); +}); + When(/^I submit the form$/, {}, async () => { await global.SCENE.scene.submit(); });

@@ -24,6 +28,6 @@ When(/^I write (.+) in (.+) field$/, {}, async (field, value) => {

await global.SCENE.scene.type(field, value); }); -When(/^I go to the event detail page$/, {}, async (field, value) => { +When(/^I go to the event detail page$/, async () => { await global.SCENE.scene.openTab('details'); });