all repos — caroster @ 6eb6aa7b98abe986421fe3c6d3416861c9d7f3f5

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

✨ Edit and remove a car
Tim Izzo tim@octree.ch
Wed, 01 Jul 2020 17:54:46 +0200
commit

6eb6aa7b98abe986421fe3c6d3416861c9d7f3f5

parent

6973ca7323f2dbbd8d73e9777cb89acc0bf406a6

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 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> - <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;
M app/src/containers/CarColumns/index.jsapp/src/containers/CarColumns/index.js

@@ -2,7 +2,7 @@ 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 Car from '../Car'; import AddCar from './AddCar'; import {useEvent} from '../../contexts/Event'; import {useStrapi} from 'strapi-react-context';

@@ -18,6 +18,7 @@ arrows: false,

lazyLoad: true, swipeToSlide: true, swipe: true, + adaptiveHeight: true, responsive: [ { breakpoint: 600,

@@ -27,9 +28,21 @@ initialSlide: 1,

}, }, { + breakpoint: 900, + settings: { + slidesToShow: 2, + }, + }, + { breakpoint: 1200, settings: { slidesToShow: 3, + }, + }, + { + breakpoint: 1400, + settings: { + slidesToShow: 4, }, }, ],

@@ -80,7 +93,6 @@ slide: {

height: `calc(100vh - ${theme.mixins.toolbar.minHeight}px)`, outline: 'none', padding: theme.spacing(2), - marginBottom: theme.spacing(4), }, }));
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,6 +60,7 @@ },

"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" }
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="sticky" 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;