all repos — caroster @ e7f15019bac7b476b94a0ccaaa2ed9fd2db2a8e3

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

merge master
Hadrien Froger hadrien@octree.ch
Thu, 02 Jul 2020 15:05:39 +0100
commit

e7f15019bac7b476b94a0ccaaa2ed9fd2db2a8e3

parent

bbf06ea9d86e56e5f856cafb9e2cc99777bfd16f

M DockerfileDockerfile

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

+# Build stage FROM strapi/base:12-alpine ARG NPM_REGISTRY=https://npm-8ee.hidora.com/

@@ -5,8 +6,7 @@ ENV NODE_ENV production

WORKDIR /srv/app RUN apk add --no-cache git -RUN npm set registry $NPM_REGISTRY && \ - npm install -g strapi@latest +RUN npm set registry $NPM_REGISTRY ## Install dependencies COPY . /srv/app/

@@ -22,4 +22,12 @@ npm run build && \

mv build ../public && \ cd .. && rm -rf app -CMD ["strapi", "start"] +# Prod stage +FROM strapi/base:12-alpine + +ENV NODE_ENV production +WORKDIR /srv/app + +COPY --from=0 /srv/app . + +CMD ["npm", "start"]
A app/.prettierrc

@@ -0,0 +1,5 @@

+semi: true +singleQuote: true +bracketSpacing: false +trailingComma: es5 +arrowParens: avoid
M app/src/App.jsapp/src/App.js

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

-import React from "react"; -import { StrapiProvider } from "strapi-react-context"; -import { ThemeProvider } from "@material-ui/core/styles"; -import { MuiPickersUtilsProvider } from "@material-ui/pickers"; -import MomentUtils from "@date-io/moment"; -import { ToastProvider } from "./contexts/Toast"; -import CssBaseline from "@material-ui/core/CssBaseline"; -import Router from "./Router"; -import theme from "./theme"; +import React from 'react'; +import {StrapiProvider} from 'strapi-react-context'; +import {ThemeProvider} from '@material-ui/core/styles'; +import {MuiPickersUtilsProvider} from '@material-ui/pickers'; +import MomentUtils from '@date-io/moment'; +import {ToastProvider} from './contexts/Toast'; +import CssBaseline from '@material-ui/core/CssBaseline'; +import Router from './Router'; +import theme from './theme'; -const models = [{ name: "events" }, { name: "cars" }, { name: "pages" }]; +const models = [{name: 'events'}, {name: 'cars'}, {name: 'pages'}]; const App = () => { return (
M app/src/App.test.jsapp/src/App.test.js

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

import React from 'react'; -import { render } from '@testing-library/react'; +import {render} from '@testing-library/react'; import App from './App'; test('renders learn react link', () => { - const { getByText } = render(<App />); + const {getByText} = render(<App />); const linkElement = getByText(/learn react/i); expect(linkElement).toBeInTheDocument(); });
M app/src/Router.jsapp/src/Router.js

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

-import React from "react"; -import { BrowserRouter, Route, Switch } from "react-router-dom"; +import React from 'react'; +import {BrowserRouter, Route, Switch} from 'react-router-dom'; // Pages -import Home from "./pages/Home"; -import Event from "./pages/Event"; -import NotFound from "./pages/NotFound"; +import Home from './pages/Home'; +import Event from './pages/Event'; +import NotFound from './pages/NotFound'; const Router = () => { return (
A app/src/containers/Car/Header.js

@@ -0,0 +1,60 @@

+import React from 'react'; +import Typography from '@material-ui/core/Typography'; +import IconButton from '@material-ui/core/IconButton'; +import Icon from '@material-ui/core/Icon'; +import moment from 'moment'; +import {makeStyles} from '@material-ui/core/styles'; +import {useTranslation} from 'react-i18next'; + +const Header = ({car, toggleEditing}) => { + const classes = useStyles(); + const {t} = useTranslation(); + return ( + <div className={classes.header}> + <IconButton className={classes.editBtn} onClick={toggleEditing}> + <Icon>edit</Icon> + </IconButton> + {!!car.departure && ( + <Typography variant="overline"> + {moment(car.departure).format('LLLL')} + </Typography> + )} + <Typography variant="h5">{car.name}</Typography> + {!!car.phone_number && ( + <div className={classes.section}> + <Typography variant="subtitle2">{t('car.fields.phone')}</Typography> + <Typography variant="body2">{car.phone_number}</Typography> + </div> + )} + {!!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> + ); +}; + +const useStyles = makeStyles(theme => ({ + header: {padding: theme.spacing(2)}, + editBtn: { + position: 'absolute', + top: 0, + right: 0, + zIndex: theme.zIndex.speedDial, + }, + section: { + marginTop: theme.spacing(2), + }, +})); + +export default Header;
A app/src/containers/Car/HeaderEditing.js

@@ -0,0 +1,191 @@

+import React, {useState, useReducer} from 'react'; +import Typography from '@material-ui/core/Typography'; +import IconButton from '@material-ui/core/IconButton'; +import Icon from '@material-ui/core/Icon'; +import Button from '@material-ui/core/Button'; +import moment from 'moment'; +import {makeStyles} from '@material-ui/core/styles'; +import {DateTimePicker} from '@material-ui/pickers'; +import {useTranslation} from 'react-i18next'; +import TextField from '@material-ui/core/TextField'; +import Slider from '@material-ui/core/Slider'; +import {useStrapi} from 'strapi-react-context'; +import {useToast} from '../../contexts/Toast'; +import {useEvent} from '../../contexts/Event'; +import RemoveDialog from './RemoveDialog'; + +const HeaderEditing = ({car, toggleEditing}) => { + const classes = useStyles(); + const {t} = useTranslation(); + const strapi = useStrapi(); + const {event} = useEvent(); + const {addToast} = useToast(); + const [removing, toggleRemoving] = useReducer(i => !i, false); + + // States + const [name, setName] = useState(car?.name ?? ''); + const [seats, setSeats] = useState(car?.seats ?? 4); + const [meeting, setMeeting] = useState(car?.meeting ?? ''); + const [date, setDate] = useState( + car?.departure ? moment(car.departure) : moment() + ); + const [phone, setPhone] = useState(car?.phone_number ?? ''); + const [details, setDetails] = useState(car?.details ?? ''); + + const onSave = async () => { + try { + // If new seats count is under current passengers count, put excedent in event waiting list + if (!!car.passengers && car.passengers.length > seats) { + const lostPassengers = car.passengers.slice(seats); + if (lostPassengers.length > 0) + await strapi.services.events.update(event.id, { + waiting_list: [...(event.waiting_list ?? []), ...lostPassengers], + }); + } + // Update car + await strapi.services.cars.update(car.id, { + name, + seats, + meeting, + departure: date.toISOString(), + phone_number: phone, + details, + passengers: car.passengers ? car.passengers.slice(0, seats) : [], + }); + toggleEditing(); + } catch (error) { + console.error(error); + addToast('car.errors.cant_update'); + } + }; + + const onRemove = async () => { + try { + // Put passengers in event waiting list (if any) + if (Array.isArray(car?.passengers) && car.passengers.length > 0) + await strapi.services.events.update(event.id, { + waiting_list: [...(event.waiting_list ?? []), ...car.passengers], + }); + // Remove car + await strapi.services.cars.remove(car.id); + addToast(t('car.actions.removed')); + toggleEditing(); + } catch (error) { + console.error(error); + addToast('car.errors.cant_remove'); + } + }; + + return ( + <div className={classes.header}> + <IconButton className={classes.editBtn} onClick={onSave}> + <Icon>done</Icon> + </IconButton> + <DateTimePicker + label={t('event.creation.date')} + value={date} + onChange={setDate} + className={classes.textField} + fullWidth + format="LLLL" + disablePast + id="UpdateCarDateTime" + name="date" + /> + <TextField + className={classes.textField} + label={t('car.creation.name')} + fullWidth + autoFocus + margin="dense" + value={name} + onChange={e => setName(e.target.value)} + id="UpdateCarName" + name="name" + /> + <TextField + className={classes.textField} + label={t('car.creation.phone')} + fullWidth + autoFocus + margin="dense" + value={phone} + onChange={e => setPhone(e.target.value)} + id="UpdateCarPhone" + name="phone" + /> + <TextField + className={classes.textField} + label={t('car.creation.meeting')} + fullWidth + margin="dense" + multiline + rows={2} + value={meeting} + onChange={e => setMeeting(e.target.value)} + id="UpdateCarMeeting" + name="meeting" + /> + <TextField + className={classes.textField} + label={t('car.creation.notes')} + fullWidth + margin="dense" + multiline + rows={2} + value={details} + onChange={e => setDetails(e.target.value)} + id="UpdateCarDetails" + name="details" + /> + <div className={classes.slider}> + <Typography variant="caption">{t('car.creation.seats')}</Typography> + <Slider + value={seats} + onChange={(e, value) => setSeats(value)} + step={1} + marks={[1, 2, 3, 4, 5, 6, 7, 8].map(value => ({ + value, + label: value, + }))} + min={1} + max={8} + valueLabelDisplay="auto" + /> + </div> + <div className={classes.actions}> + <Button color="secondary" variant="outlined" onClick={toggleRemoving}> + {t('car.actions.remove')} + </Button> + </div> + <RemoveDialog + open={removing} + toggle={toggleRemoving} + onRemove={onRemove} + /> + </div> + ); +}; + +const useStyles = makeStyles(theme => ({ + header: {padding: theme.spacing(2)}, + editBtn: { + position: 'absolute', + top: 0, + right: 0, + zIndex: theme.zIndex.speedDial, + }, + section: { + marginTop: theme.spacing(2), + }, + slider: { + marginTop: theme.spacing(2), + }, + actions: { + display: 'flex', + justifyContent: 'center', + margin: theme.spacing(2, 0), + }, +})); + +export default HeaderEditing;
A app/src/containers/Car/RemoveDialog.js

@@ -0,0 +1,37 @@

+import React 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 Slide from '@material-ui/core/Slide'; +import Button from '@material-ui/core/Button'; +import {useTranslation} from 'react-i18next'; + +const Transition = React.forwardRef(function Transition(props, ref) { + return <Slide direction="up" ref={ref} {...props} />; +}); + +const RemoveDialog = ({open, toggle, onRemove}) => { + const {t} = useTranslation(); + + return ( + <Dialog open={open} TransitionComponent={Transition} onClose={toggle}> + <DialogContent> + <DialogContentText>{t('car.actions.remove_alert')}</DialogContentText> + </DialogContent> + <DialogActions> + <Button onClick={toggle}>{t('generic.cancel')}</Button> + <Button + onClick={() => { + onRemove(); + toggle(); + }} + > + {t('generic.confirm')} + </Button> + </DialogActions> + </Dialog> + ); +}; + +export default RemoveDialog;
A app/src/containers/Car/index.js

@@ -0,0 +1,70 @@

+import React, {useReducer} from 'react'; +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 PassengersList from '../PassengersList'; +import {useToast} from '../../contexts/Toast'; +import Header from './Header'; +import HeaderEditing from './HeaderEditing'; + +const Car = ({car}) => { + const classes = useStyles(); + const {t} = useTranslation(); + const {addToast} = useToast(); + const strapi = useStrapi(); + const [isEditing, toggleEditing] = useReducer(i => !i, false); + + 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 idx => { + if (!car?.passengers) return false; + try { + return await strapi.services.cars.update(car.id, { + passengers: car.passengers.filter((_, i) => i !== idx), + }); + } catch (error) { + console.error(error); + addToast(t('car.errors.cant_remove_passenger')); + return false; + } + }; + + return ( + <Paper className={classes.root}> + {isEditing ? ( + <HeaderEditing car={car} toggleEditing={toggleEditing} /> + ) : ( + <Header car={car} toggleEditing={toggleEditing} /> + )} + <Divider /> + <PassengersList + passengers={car.passengers} + places={car.seats} + addPassenger={addPassenger} + removePassenger={removePassenger} + /> + </Paper> + ); +}; + +const useStyles = makeStyles(theme => ({ + root: { + position: 'relative', + }, +})); + +export default Car;
D app/src/containers/CarColumns/Car.js

@@ -1,88 +0,0 @@

-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 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> - <div className={classes.header}> - {!!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")} - </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), - }, -})); - -export default Car;
A app/src/containers/CarColumns/WaitingList.js

@@ -0,0 +1,58 @@

+import React from 'react'; +import Typography from '@material-ui/core/Typography'; +import Paper from '@material-ui/core/Paper'; +import {makeStyles} from '@material-ui/core/styles'; +import {useTranslation} from 'react-i18next'; +import {useStrapi} from 'strapi-react-context'; +import {useEvent} from '../../contexts/Event'; +import PassengersList from '../PassengersList'; +import Divider from '@material-ui/core/Divider'; + +const WaitingList = ({car}) => { + const {t} = useTranslation(); + const {event} = useEvent(); + const strapi = useStrapi(); + const classes = useStyles(); + + const addPassenger = async passenger => { + try { + await strapi.services.events.update(event.id, { + waiting_list: [...(event.waiting_list || []), passenger], + }); + } catch (error) { + console.error(error); + } + }; + + const removePassenger = async idx => { + try { + await strapi.services.events.update(event.id, { + waiting_list: event.waiting_list.filter((_, i) => i !== idx), + }); + } catch (error) { + console.error(error); + } + }; + + return ( + <Paper> + <div className={classes.header}> + <Typography variant="h5">{t('passenger.title')}</Typography> + </div> + <Divider /> + <PassengersList + hideEmpty + passengers={event.waiting_list} + places={50} + addPassenger={addPassenger} + removePassenger={removePassenger} + /> + </Paper> + ); +}; + +const useStyles = makeStyles(theme => ({ + header: {padding: theme.spacing(2)}, +})); + +export default WaitingList;
M app/src/containers/CarColumns/index.jsapp/src/containers/CarColumns/index.js

@@ -1,11 +1,12 @@

-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"; +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'; +import WaitingList from './WaitingList'; const settings = { dots: false,

@@ -17,11 +18,19 @@ arrows: false,

lazyLoad: true, swipeToSlide: true, swipe: true, + adaptiveHeight: true, responsive: [ { breakpoint: 600, settings: { slidesToShow: 1, + initialSlide: 1, + }, + }, + { + breakpoint: 900, + settings: { + slidesToShow: 2, }, }, {

@@ -30,6 +39,12 @@ settings: {

slidesToShow: 3, }, }, + { + breakpoint: 1400, + settings: { + slidesToShow: 4, + }, + }, ], };

@@ -40,24 +55,27 @@ if (dateA === dateB) return new Date(a.createdAt) - new Date(b.createdAt);

else return dateA - dateB; }; -const CarColumns = ({ ...props }) => { +const CarColumns = ({...props}) => { const classes = useStyles(); const strapi = useStrapi(); - const { event } = useEvent(); + const {event} = useEvent(); const cars = useMemo( () => strapi.stores.cars - ?.filter((car) => car?.event?.id === event?.id) + ?.filter(car => car?.event?.id === event?.id) .sort(sortCars), [strapi.stores.cars, event] ); return ( - <div className={classes.root}> + <div> <Slider {...settings}> + <Container maxWidth="sm" className={classes.slide}> + <WaitingList /> + </Container> {cars && - cars.map((car) => ( + cars.map(car => ( <Container key={car.id} maxWidth="sm" className={classes.slide}> <Car car={car} {...props} /> </Container>

@@ -70,13 +88,11 @@ </div>

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

@@ -1,104 +1,127 @@

-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"; +import React, {useState, useEffect, useReducer, useMemo} 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) => +const isValidEmail = email => // eslint-disable-next-line /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/.test( email ); -const Step1 = ({ nextStep, event, addToEvent, ...props }) => { +const Step1 = ({ + nextStep, + previousStep, + createEvent, + event, + addToEvent, + ...props +}) => { const classes = useStyles(); - const { t } = useTranslation(); + const {t} = useTranslation(); // States - const [name, setName] = useState(event.name ?? ""); - const [email, setEmail] = useState(event.email ?? ""); + 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 [showTos, toggleTos] = useReducer(i => !i, false); const debouncedEmail = useDebounce(email, 400); + const canSubmit = useMemo( + () => name.length > 0 && email.length > 0 && emailIsValid && tos, + [name, email, emailIsValid, tos] + ); useEffect(() => { setEmailIsValid(isValidEmail(debouncedEmail)); }, [debouncedEmail]); const onNext = () => { - addToEvent({ name, email }); + addToEvent({name, email}); 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" - /> - <div className={classes.tos}> - <Checkbox - name="tos" - id="NewEventTos" - checked={tos} - onChange={(e) => setTos(e.target.checked)} + <form onSubmit={onNext}> + <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" /> - <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> + <div className={classes.tos}> + <Checkbox + name="tos" + id="NewEventTos" + checked={tos} + onChange={e => setTos(e.target.checked)} + /> + <Typography + component="a" + role="button" + variant="caption" + onClick={toggleTos} + tabIndex="0" + onKeyPress={({charCode}) => { + if (charCode && (charCode === 32 || charCode === 13)) + toggleTos(); + }} + > + {t('event.creation.tos')} + </Typography> + </div> + <Button + className={classes.button} + type="submit" + variant="contained" + color="secondary" + fullWidth + disabled={!canSubmit} + aria-disabled={!canSubmit} + > + {t('event.creation.next')} + </Button> + </form> </Paper> <TosDialog open={showTos} toggle={toggleTos} /> </> ); }; -const useStyles = makeStyles((theme) => ({ +const useStyles = makeStyles(theme => ({ textField: {}, button: { marginTop: theme.spacing(2), }, tos: { - cursor: "pointer", + cursor: 'pointer', marginTop: theme.spacing(2), - display: "flex", - alignItems: "center", - marginLeft: "-11px", + display: 'flex', + alignItems: 'center', + marginLeft: '-11px', }, }));
M app/src/containers/NewCarDialog/index.jsapp/src/containers/NewCarDialog/index.js

@@ -1,49 +1,50 @@

-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"; +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) => ({ +const marks = [1, 2, 3, 4, 5, 6, 7, 8].map(value => ({ value, label: value, })); -const NewCarDialog = ({ open, toggle }) => { +const NewCarDialog = ({open, toggle}) => { const strapi = useStrapi(); - const { t } = useTranslation(); + const {t} = useTranslation(); const classes = useStyles(); - const { addToast } = useToast(); - const { event } = useEvent(); + const {addToast} = useToast(); + const {event} = useEvent(); // States - const [name, setName] = useState(""); + const [name, setName] = useState(''); const [seats, setSeats] = useState(4); - const [meeting, setMeeting] = useState(""); + const [meeting, setMeeting] = useState(''); const [date, setDate] = useState(moment()); - const [phone, setPhone] = useState(""); - const [details, setDetails] = useState(""); + const [phone, setPhone] = useState(''); + const [details, setDetails] = useState(''); const canCreate = !!name && !!seats; - const onCreate = async () => { + const onCreate = async event => { + if (event.preventDefault) event.preventDefault(); try { await strapi.services.cars.create({ name,

@@ -54,14 +55,13 @@ phone_number: phone,

details, event: event.id, }); - addToast(t("car.creation.created")); + addToast(t('car.creation.created')); toggle(); - return true; } catch (error) { console.error(error); - addToast(t("car.errors.cant_create")); - return false; + addToast(t('car.errors.cant_create')); } + return false; }; return (

@@ -72,90 +72,99 @@ 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> + <form onSubmit={onCreate}> + <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 + 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 id="NewCarCancel" onClick={toggle} tabIndex={-1}> + {t('generic.cancel')} + </Button> + <Button + variant="contained" + type="submit" + disabled={!canCreate} + aria-disabled={!canCreate} + id="NewCarSubmit" + > + {t('generic.create')} + </Button> + </DialogActions> + </form> </Dialog> ); }; -const useStyles = makeStyles((theme) => ({ - textField: { marginBottom: theme.spacing(2) }, +const useStyles = makeStyles(theme => ({ + textField: {marginBottom: theme.spacing(2)}, })); export default NewCarDialog;
M app/src/containers/PassengersList/Passenger.jsapp/src/containers/PassengersList/Passenger.js

@@ -1,37 +1,33 @@

-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"; +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 Passenger = ({passenger, removePassenger}) => { const classes = useStyles(); - const { t } = useTranslation(); + 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)} - > + <IconButton edge="end" size="small" onClick={removePassenger}> <Icon>close</Icon> </IconButton> </div> ); - else return <div className={classes.item}>{t("car.passengers.empty")}</div>; + else return <div className={classes.item}>{t('car.passengers.empty')}</div>; }; -const useStyles = makeStyles((theme) => ({ +const useStyles = makeStyles(theme => ({ item: { padding: theme.spacing(1, 2), - display: "flex", - alignItems: "center", - height: "46px", + display: 'flex', + alignItems: 'center', + height: '46px', }, name: { flexGrow: 1,
M app/src/containers/PassengersList/index.jsapp/src/containers/PassengersList/index.js

@@ -1,9 +1,10 @@

-import React from "react"; -import { makeStyles } from "@material-ui/core/styles"; -import Passenger from "./Passenger"; -import Input from "./Input"; +import React from 'react'; +import {makeStyles} from '@material-ui/core/styles'; +import Passenger from './Passenger'; +import Input from './Input'; const PassengersList = ({ + hideEmpty, passengers, places = 0, addPassenger,

@@ -11,29 +12,35 @@ removePassenger,

}) => { const classes = useStyles(); - const emptyList = Array.apply(null, Array(places)); - const list = Array.isArray(passengers) - ? emptyList.map((u, index) => passengers[index]) - : emptyList; + let list = passengers; + + if (!hideEmpty) { + const emptyList = [...Array(places)]; + 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 && list.map((passenger, index) => ( <Passenger key={index} passenger={passenger} - removePassenger={removePassenger} + removePassenger={() => removePassenger(index)} /> ))} </div> ); }; -const useStyles = makeStyles((theme) => ({ - container: { padding: theme.spacing(1, 0) }, +const useStyles = makeStyles(theme => ({ + container: {padding: theme.spacing(1, 0)}, })); export default PassengersList;
M app/src/i18n.jsapp/src/i18n.js

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

-import i18n from "i18next"; -import { initReactI18next } from "react-i18next"; -import translationFr from "./locales/fr.json"; +import i18n from 'i18next'; +import {initReactI18next} from 'react-i18next'; +import translationFr from './locales/fr.json'; const resources = { fr: {

@@ -12,7 +12,7 @@ i18n

.use(initReactI18next) // passes i18n down to react-i18next .init({ resources, - lng: "fr", + lng: 'fr', interpolation: { escapeValue: false, // react already safes from xss },
M app/src/index.jsapp/src/index.js

@@ -1,16 +1,16 @@

-import React from "react"; -import ReactDOM from "react-dom"; -import "fontsource-roboto"; -import "./i18n"; -import "moment/locale/fr-ch"; -import App from "./App"; -import * as serviceWorker from "./serviceWorker"; +import React from 'react'; +import ReactDOM from 'react-dom'; +import 'fontsource-roboto'; +import './i18n'; +import 'moment/locale/fr-ch'; +import App from './App'; +import * as serviceWorker from './serviceWorker'; ReactDOM.render( <React.StrictMode> <App /> </React.StrictMode>, - document.getElementById("root") + document.getElementById('root') ); // If you want your app to work offline and load faster, you can change
M app/src/locales/fr.jsonapp/src/locales/fr.json

@@ -3,7 +3,9 @@ "generic": {

"loading": "Chargement...", "close": "Fermer", "create": "Créer", - "cancel": "Annuler" + "cancel": "Annuler", + "remove": "Supprimer", + "confirm": "Confirmer" }, "event": { "fields": {

@@ -34,7 +36,8 @@ },

"car": { "fields": { "meeting_point": "Lieu de rencontre", - "details": "Notes" + "details": "Notes", + "phone": "Contact" }, "creation": { "title": "Ajouter une voiture",

@@ -45,6 +48,11 @@ "phone": "Numéro de téléphone",

"notes": "Infos complémentaires", "created": "La voiture a été créée" }, + "actions": { + "remove": "Supprimer la voiture", + "remove_alert": "Voulez-vous vraiment supprimer cette voiture et ajouter les inscrits à la liste d'attente ?", + "removed": "La voiture a été supprimée" + }, "passengers": { "empty": "Place disponible", "add": "Ajouter un passager"

@@ -52,8 +60,12 @@ },

"errors": { "cant_create": "Impossible de créer la voiture", "cant_update": "Impossible de modifier la voiture", + "cant_remove": "Impossile de supprimer la voiture", "cant_add_passenger": "Impossible d'ajouter un passager", "cant_remove_passenger": "Impossible de supprimer le passager" } + }, + "passenger": { + "title": "Liste d'attente" } }
M app/src/pages/Event.jsapp/src/pages/Event.js

@@ -1,29 +1,30 @@

-import React, { useState, useReducer, useEffect } from "react"; -import { useTranslation } from "react-i18next"; -import AppBar from "@material-ui/core/AppBar"; -import TextField from "../components/TextField"; -import Toolbar from "@material-ui/core/Toolbar"; -import Container from "@material-ui/core/Container"; -import Typography from "@material-ui/core/Typography"; -import IconButton from "@material-ui/core/IconButton"; -import Icon from "@material-ui/core/Icon"; -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"; +import React, {useState, useReducer, useEffect} from 'react'; +import {useTranslation} from 'react-i18next'; +import AppBar from '@material-ui/core/AppBar'; +import TextField from '../components/TextField'; +import Toolbar from '@material-ui/core/Toolbar'; +import Container from '@material-ui/core/Container'; +import Typography from '@material-ui/core/Typography'; +import IconButton from '@material-ui/core/IconButton'; +import Icon from '@material-ui/core/Icon'; +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'; +import Loading from '../pages/Loading'; const Event = () => { - const { t } = useTranslation(); - const { addToast } = useToast(); + const {t} = useTranslation(); + 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 [detailsOpen, toggleDetails] = useReducer(i => !i, false); + const classes = useStyles({detailsOpen}); + const [openNewCar, toggleNewCar] = useReducer(i => !i, false); const { event, isEditing,

@@ -37,32 +38,32 @@ useEffect(() => {

if (!detailsOpen) setIsEditing(false); }, [detailsOpen]); // eslint-disable-line react-hooks/exhaustive-deps - const onEventSave = async (e) => { + const onEventSave = async e => { try { await updateEvent(); setIsEditing(false); } catch (error) { console.error(error); - addToast(t("event.errors.cant_update")); + addToast(t('event.errors.cant_update')); } }; - if (!event) return <div>{t("generic.loading")}</div>; + if (!event) return <Loading />; return ( <Layout> <AppBar position="static" className={classes.appbar} - id={(isEditing && "EditEvent") || (detailsOpen && "Details") || "Menu"} + id={(isEditing && 'EditEvent') || (detailsOpen && 'Details') || 'Menu'} > <Toolbar> {isEditing ? ( <TextField light value={editingEvent.name ?? event.name} - onChange={(e) => - setEditingEvent({ ...editingEvent, name: e.target.value }) + onChange={e => + setEditingEvent({...editingEvent, name: e.target.value}) } id="EditEventName" name="name"

@@ -80,7 +81,7 @@ {!detailsOpen && (

<IconButton edge="end" id="MenuMoreInfo" - onClick={(e) => setAnchorEl(e.currentTarget)} + onClick={e => setAnchorEl(e.currentTarget)} > <Icon className={classes.barIcon}>more_vert</Icon> </IconButton>

@@ -89,7 +90,7 @@ {detailsOpen && !isEditing && (

<IconButton edge="end" id="DetailsEditBtn" - onClick={(e) => setIsEditing(true)} + onClick={e => setIsEditing(true)} > <Icon className={classes.barIcon}>edit</Icon> </IconButton>

@@ -105,20 +106,20 @@ setAnchorEl={setAnchorEl}

actions={[ { label: detailsOpen - ? t("event.actions.hide_details") - : t("event.actions.show_details"), + ? t('event.actions.hide_details') + : t('event.actions.show_details'), onClick: toggleDetails, - id: "DetailsTab", + id: 'DetailsTab', }, { - label: t("event.actions.add_car"), + label: t('event.actions.add_car'), onClick: toggleNewCar, - id: "NewCarTab", + id: 'NewCarTab', }, { - label: t("event.actions.invite"), + label: t('event.actions.invite'), onClick: () => {}, - id: "InviteTab", + id: 'InviteTab', }, ]} />

@@ -134,23 +135,23 @@ </Layout>

); }; -const useStyles = makeStyles((theme) => ({ - container: { padding: theme.spacing(2) }, - appbar: ({ detailsOpen }) => ({ - transition: "height 0.3s ease", - overflow: "hidden", - height: detailsOpen ? "100vh" : theme.mixins.toolbar.minHeight, +const useStyles = makeStyles(theme => ({ + container: {padding: theme.spacing(2)}, + appbar: ({detailsOpen}) => ({ + transition: 'height 0.3s ease', + overflow: 'hidden', + height: detailsOpen ? '100vh' : theme.mixins.toolbar.minHeight, zIndex: theme.zIndex.appBar, }), name: { flexGrow: 1, }, barIcon: { - color: "white", + color: 'white', }, })); -export default (props) => ( +export default props => ( <EventProvider {...props}> <Event {...props} /> </EventProvider>
A app/src/pages/Loading.js

@@ -0,0 +1,24 @@

+import React from 'react'; +import CircularProgress from '@material-ui/core/CircularProgress'; +import Container from '@material-ui/core/Container'; +import {makeStyles} from '@material-ui/core/styles'; + +const Loading = () => { + const classes = useStyles(); + return ( + <Container className={classes.container}> + <CircularProgress /> + </Container> + ); +}; + +const useStyles = makeStyles(theme => ({ + container: { + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + height: '100vh', + }, +})); + +export default Loading;
M app/src/serviceWorker.jsapp/src/serviceWorker.js

@@ -101,7 +101,7 @@

function checkValidServiceWorker(swUrl, config) { // Check if the service worker can be found. If it can't reload the page. fetch(swUrl, { - headers: { 'Service-Worker': 'script' }, + headers: {'Service-Worker': 'script'}, }) .then(response => { // Ensure service worker exists, and that we really are getting a JS file.
M app/src/theme.jsapp/src/theme.js

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

-import { createMuiTheme } from "@material-ui/core/styles"; -import grey from "@material-ui/core/colors/grey"; -import blue from "@material-ui/core/colors/blue"; +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: {
M config/functions/bootstrap.jsconfig/functions/bootstrap.js

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

-'use strict'; +"use strict"; + +const permissions = require("../permissions.json"); /** * An asynchronous bootstrap function that runs before

@@ -10,4 +12,61 @@ *

* See more details here: https://strapi.io/documentation/v3.x/concepts/configurations.html#bootstrap */ -module.exports = () => {}; +module.exports = async () => { + /** + * Set permissions + */ + + // For each role, set permissions + const roles = Object.keys(permissions.roles); + await Promise.all( + roles.map(async (roleType) => { + // Get role entity in Strapi db + const role = await strapi.query("role", "users-permissions").findOne({ + type: roleType, + }); + // If role doesn't exist, skip + if (!role) return []; + + // Enable or create permissions for each roles, controllers and actions + const perms = permissions.roles[roleType]; + return perms.map(({ type, controllers }) => + controllers.map(({ name: controller, actions }) => + actions.map(async (action) => { + const existingPerm = await strapi + .query("permission", "users-permissions") + .findOne({ + role: role.id, + type, + controller, + action, + }); + if (!!existingPerm) { + if (existingPerm.enabled) return false; // If permission already enabled, skip + strapi.log.info( + `Enable permission ${type}.${controller}.${action} for role ${roleType}.` + ); + return strapi + .query("permission", "users-permissions") + .update( + { role: role.id, type, controller, action }, + { enabled: true } + ); + } else { + strapi.log.info( + `Create permission ${type}.${controller}.${action} for role ${roleType}.` + ); + return strapi.query("permission", "users-permissions").create({ + role: role.id, + type, + controller, + action, + enabled: true, + }); + } + }) + ) + ); + }) + ); +};
A config/permissions.json

@@ -0,0 +1,23 @@

+{ + "roles": { + "public": [ + { + "type": "application", + "controllers": [ + { + "name": "car", + "actions": ["create", "delete", "find", "findone", "update"] + }, + { + "name": "event", + "actions": ["create", "findone", "update"] + }, + { + "name": "page", + "actions": ["find", "findone"] + } + ] + } + ] + } +}
M e2e/test/pages/_page.jse2e/test/pages/_page.js

@@ -57,6 +57,7 @@ if (typeof value === 'undefined') value = selector;

const element = await $(field); await element.addValue(cast(value)); } + /** * * @param {string} selector