all repos — caroster @ 7fe5ad7f5e032f23e13738c67c6b9314110684ac

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

✨ Set event view #5 #12
Tim Izzo tim@octree.ch
Tue, 30 Jun 2020 14:04:54 +0000
commit

7fe5ad7f5e032f23e13738c67c6b9314110684ac

parent

3356b91f06973f52b8c12ba294de3a76705d3162

A .npmrc

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

+registry=https://npm-8ee.hidora.com
M api/car/documentation/1.0.0/car.jsonapi/car/documentation/1.0.0/car.json

@@ -569,8 +569,17 @@ "type": "array",

"items": { "type": "string" } + }, + "position": { + "type": "object" + }, + "waiting_list": { + "type": "object" } } + }, + "passengers": { + "type": "object" } } },

@@ -600,6 +609,9 @@ "type": "string"

}, "event": { "type": "string" + }, + "passengers": { + "type": "object" } } }
M api/car/models/car.settings.jsonapi/car/models/car.settings.json

@@ -32,6 +32,9 @@ },

"event": { "model": "event", "via": "cars" + }, + "passengers": { + "type": "json" } } }
M api/event/documentation/1.0.0/event.jsonapi/event/documentation/1.0.0/event.json

@@ -568,9 +568,18 @@ "type": "string"

}, "event": { "type": "string" + }, + "passengers": { + "type": "object" } } } + }, + "position": { + "type": "object" + }, + "waiting_list": { + "type": "object" } } },

@@ -597,6 +606,12 @@ "type": "array",

"items": { "type": "string" } + }, + "position": { + "type": "object" + }, + "waiting_list": { + "type": "object" } } }
M api/event/models/event.jsapi/event/models/event.js

@@ -1,8 +1,50 @@

-'use strict'; +"use strict"; +const axios = require("axios"); /** * Read the documentation (https://strapi.io/documentation/v3.x/concepts/models.html#lifecycle-hooks) * to customize this model */ -module.exports = {}; +module.exports = { + lifecycles: { + async beforeCreate(event) { + if (!!event.address) { + const query = encodeURI(event.address); + try { + const { data } = await axios.get( + ` https://nominatim.openstreetmap.org/search?format=json&q=${query}` + ); + if (Array.isArray(data) && data.length > 0) { + const [entity] = data; + event.position = [entity.lat, entity.lon]; + } else + strapi.log.info( + `No location from Nominatim API for ${event.address}` + ); + } catch (error) { + strapi.log.error(error); + } + } + }, + async beforeUpdate(params, event) { + if (!!event.address) { + const query = encodeURI(event.address); + try { + const { data } = await axios.get( + ` https://nominatim.openstreetmap.org/search?format=json&q=${query}` + ); + if (Array.isArray(data) && data.length > 0) { + const [entity] = data; + event.position = [entity.lat, entity.lon]; + } else + strapi.log.info( + `No location from Nominatim API for ${event.address}` + ); + } catch (error) { + strapi.log.error(error); + } + } + }, + }, +};
M api/event/models/event.settings.jsonapi/event/models/event.settings.json

@@ -26,6 +26,12 @@ },

"cars": { "via": "event", "collection": "car" + }, + "position": { + "type": "json" + }, + "waiting_list": { + "type": "json" } } }
A app/.npmrc

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

+registry=https://npm-8ee.hidora.com
M app/package-lock.jsonapp/package-lock.json

@@ -3566,6 +3566,11 @@ }

} } }, + "classnames": { + "version": "2.2.6", + "resolved": "https://npm-8ee.hidora.com/classnames/-/classnames-2.2.6.tgz", + "integrity": "sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q==" + }, "clean-css": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.2.3.tgz",

@@ -4852,6 +4857,11 @@ "safe-buffer": "~5.1.0"

} } } + }, + "enquire.js": { + "version": "2.1.6", + "resolved": "https://npm-8ee.hidora.com/enquire.js/-/enquire.js-2.1.6.tgz", + "integrity": "sha1-PoeAybi4NQhMP2DhZtvDwqPImBQ=" }, "entities": { "version": "2.0.3",

@@ -7845,6 +7855,14 @@ "json-stringify-safe": {

"version": "5.0.1", "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" + }, + "json2mq": { + "version": "0.2.0", + "resolved": "https://npm-8ee.hidora.com/json2mq/-/json2mq-0.2.0.tgz", + "integrity": "sha1-tje9O6nqvhIsg+lyBIOusQ0skEo=", + "requires": { + "string-convert": "^0.2.0" + } }, "json3": { "version": "3.3.3",

@@ -8010,6 +8028,11 @@ "requires": {

"invert-kv": "^2.0.0" } }, + "leaflet": { + "version": "1.6.0", + "resolved": "https://npm-8ee.hidora.com/leaflet/-/leaflet-1.6.0.tgz", + "integrity": "sha512-CPkhyqWUKZKFJ6K8umN5/D2wrJ2+/8UIpXppY7QDnUZW5bZL5+SEI2J7GBpwh4LIupOKqbNSQXgqmrEJopHVNQ==" + }, "left-pad": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/left-pad/-/left-pad-1.3.0.tgz",

@@ -8142,6 +8165,11 @@ "lodash._reinterpolate": {

"version": "3.0.0", "resolved": "https://registry.npmjs.org/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz", "integrity": "sha1-DM8tiRZq8Ds2Y8eWU4t1rG4RTZ0=" + }, + "lodash.debounce": { + "version": "4.0.8", + "resolved": "https://npm-8ee.hidora.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha1-gteb/zCmfEAF/9XiUVMArZyk168=" }, "lodash.memoize": { "version": "4.1.2",

@@ -10862,6 +10890,17 @@ "version": "16.13.1",

"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, + "react-leaflet": { + "version": "2.7.0", + "resolved": "https://npm-8ee.hidora.com/react-leaflet/-/react-leaflet-2.7.0.tgz", + "integrity": "sha512-pMf5eRyWU8RH9HohM2i0NZymcWHraJA1m6iMFYu94/01PAaBJpOyxORZJmN6cV9dBzkVWaLjAAHTNmxbwIpcfw==", + "requires": { + "@babel/runtime": "^7.9.2", + "fast-deep-equal": "^3.1.1", + "hoist-non-react-statics": "^3.3.2", + "warning": "^4.0.3" + } + }, "react-router": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/react-router/-/react-router-5.2.0.tgz",

@@ -10966,6 +11005,18 @@ "webpack": "4.42.0",

"webpack-dev-server": "3.10.3", "webpack-manifest-plugin": "2.2.0", "workbox-webpack-plugin": "4.3.1" + } + }, + "react-slick": { + "version": "0.26.1", + "resolved": "https://npm-8ee.hidora.com/react-slick/-/react-slick-0.26.1.tgz", + "integrity": "sha512-IQVRSkikG2w5bkz+m9Ing5zZIuM9cI+qJyXG2o6PXHKj8GFcsMCJoTBADwyLSsVT8dHcZ8MZ0dsxq0i0CKIq+Q==", + "requires": { + "classnames": "^2.2.5", + "enquire.js": "^2.1.6", + "json2mq": "^0.2.0", + "lodash.debounce": "^4.0.8", + "resize-observer-polyfill": "^1.5.0" } }, "react-transition-group": {

@@ -11261,6 +11312,11 @@ "version": "1.0.0",

"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=" }, + "resize-observer-polyfill": { + "version": "1.5.1", + "resolved": "https://npm-8ee.hidora.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz", + "integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==" + }, "resolve": { "version": "1.15.0", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.15.0.tgz",

@@ -12269,6 +12325,11 @@ "version": "1.1.0",

"resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz", "integrity": "sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM=" }, + "string-convert": { + "version": "0.2.1", + "resolved": "https://npm-8ee.hidora.com/string-convert/-/string-convert-0.2.1.tgz", + "integrity": "sha1-aYLMMEn7tM2F+LJFaLnZvznu/5c=" + }, "string-length": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/string-length/-/string-length-2.0.0.tgz",

@@ -13182,6 +13243,14 @@ "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.7.tgz",

"integrity": "sha1-L3+bj9ENZ3JisYqITijRlhjgKPs=", "requires": { "makeerror": "1.0.x" + } + }, + "warning": { + "version": "4.0.3", + "resolved": "https://npm-8ee.hidora.com/warning/-/warning-4.0.3.tgz", + "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==", + "requires": { + "loose-envify": "^1.0.0" } }, "watchpack": {
M app/package.jsonapp/package.json

@@ -12,12 +12,15 @@ "@testing-library/react": "^9.5.0",

"@testing-library/user-event": "^7.2.1", "fontsource-roboto": "^2.1.4", "i18next": "^19.5.1", + "leaflet": "^1.6.0", "moment": "^2.27.0", "react": "^16.13.1", "react-dom": "^16.13.1", "react-i18next": "^11.7.0", + "react-leaflet": "^2.7.0", "react-router-dom": "^5.2.0", "react-scripts": "3.4.1", + "react-slick": "^0.26.1", "strapi-react-context": "^0.2.3" }, "scripts": {
M app/public/index.htmlapp/public/index.html

@@ -13,11 +13,26 @@ name="description"

content="Web site created using create-react-app" /> <link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" /> + <link + rel="stylesheet" + href="https://fonts.googleapis.com/icon?family=Material+Icons" + /> <!-- manifest.json provides metadata used when your web app is installed on a user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/ --> <link rel="manifest" href="%PUBLIC_URL%/manifest.json" /> + <link + rel="stylesheet" + type="text/css" + charset="UTF-8" + href="https://cdnjs.cloudflare.com/ajax/libs/slick-carousel/1.6.0/slick.min.css" + /> + <link + rel="stylesheet" + type="text/css" + href="https://cdnjs.cloudflare.com/ajax/libs/slick-carousel/1.6.0/slick-theme.min.css" + /> <!-- Notice the use of %PUBLIC_URL% in the tags above. It will be replaced with the URL of the `public` folder during the build.
A app/src/components/Map/index.js

@@ -0,0 +1,25 @@

+import React from "react"; +import "leaflet/dist/leaflet.css"; +import { Map as LeafletMap, Marker, TileLayer } from "react-leaflet"; +import L from "leaflet"; +delete L.Icon.Default.prototype._getIconUrl; + +L.Icon.Default.mergeOptions({ + iconRetinaUrl: require("leaflet/dist/images/marker-icon-2x.png"), + iconUrl: require("leaflet/dist/images/marker-icon.png"), + shadowUrl: require("leaflet/dist/images/marker-shadow.png"), +}); + +const Map = ({ width = "100%", height = "20rem", position }) => { + return ( + <LeafletMap center={position} zoom={13} style={{ width, height }}> + <TileLayer + url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" + attribution='&copy; <a href="http://osm.org/copyright">OpenStreetMap</a> contributors' + /> + <Marker position={position}></Marker> + </LeafletMap> + ); +}; + +export default Map;
A app/src/components/Paper/index.js

@@ -0,0 +1,18 @@

+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} /> + ); +}; + +export default Paper;
A app/src/components/TextField/index.js

@@ -0,0 +1,35 @@

+import React from "react"; +import TextFieldMUI from "@material-ui/core/TextField"; +import { makeStyles } from "@material-ui/core/styles"; + +const TextField = ({ className, light, ...props }) => { + const classes = useStyles(); + return ( + <TextFieldMUI + className={`${classes.input} ${className} ${light ? "light" : ""}`} + fullWidth + margin="dense" + {...props} + /> + ); +}; + +const useStyles = makeStyles((theme) => ({ + input: { + "&.light .MuiFormLabel-root": { + color: "white", + }, + "&.light .MuiInputBase-input": { color: "white" }, + "&.light .MuiInput-underline::before": { + borderColor: "white", + }, + "&.light .MuiInput-underline:hover:not(.Mui-disabled)::before": { + borderColor: "white", + }, + "&.light .MuiInput-underline::after": { + transform: "scaleX(0)", + }, + }, +})); + +export default TextField;
A app/src/containers/CarColumns/AddCar.js

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

+import React from "react"; + +const AddCar = () => { + return <div>Add car</div>; +}; + +export default AddCar;
A app/src/containers/CarColumns/Car.js

@@ -0,0 +1,46 @@

+import React from "react"; +import Typography from "@material-ui/core/Typography"; +import { makeStyles } from "@material-ui/core/styles"; +import { useTranslation } from "react-i18next"; +import moment from "moment"; +import Paper from "../../components/Paper"; + +const Car = ({ car }) => { + const classes = useStyles(); + const { t } = useTranslation(); + + if (!car) return null; + + 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")} + </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> + )} + </Paper> + ); +}; + +const useStyles = makeStyles((theme) => ({ + section: { + marginTop: theme.spacing(2), + }, +})); + +export default Car;
A app/src/containers/CarColumns/index.js

@@ -0,0 +1,54 @@

+import React 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"; + +const settings = { + dots: false, + infinite: false, + speed: 500, + slidesToShow: 5, + slidesToScroll: 1, + arrows: false, + lazyLoad: true, + swipeToSlide: true, + swipe: true, + responsive: [ + { + breakpoint: 600, + settings: { + slidesToShow: 1, + }, + }, + ], +}; + +const CarColumns = ({ cars = [] }) => { + const classes = useStyles(); + return ( + <div> + <Slider {...settings}> + {cars.map((car) => ( + <Container key={car.id} maxWidth="sm" className={classes.slide}> + <Car car={car} /> + </Container> + ))} + <Container maxWidth="sm" className={classes.slide}> + <AddCar /> + </Container> + </Slider> + </div> + ); +}; + +const useStyles = makeStyles((theme) => ({ + slide: { + height: `calc(100vh - ${theme.mixins.toolbar.minHeight}px)`, + outline: "none", + padding: theme.spacing(2), + }, +})); + +export default CarColumns;
M app/src/containers/CreateEvent/Step1.jsapp/src/containers/CreateEvent/Step1.js

@@ -1,17 +1,18 @@

import React, { useState, useEffect } from "react"; -import Paper from "@material-ui/core/Paper"; import { makeStyles } from "@material-ui/core/styles"; import TextField from "@material-ui/core/TextField"; import Button from "@material-ui/core/Button"; import { useTranslation } from "react-i18next"; import useDebounce from "../../hooks/useDebounce"; +import Paper from "../../components/Paper"; 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 }) => { +const Step1 = ({ nextStep, event, addToEvent, ...props }) => { const classes = useStyles(); const { t } = useTranslation();

@@ -20,9 +21,10 @@ const [name, setName] = useState(event.name ?? "");

const [email, setEmail] = useState(event.email ?? ""); const [emailIsValid, setEmailIsValid] = useState(false); + const debouncedEmail = useDebounce(email, 400); useEffect(() => { - setEmailIsValid(isValidEmail(email)); - }, [useDebounce(email, 400)]); + setEmailIsValid(isValidEmail(debouncedEmail)); + }, [debouncedEmail]); const onNext = () => { addToEvent({ name, email });

@@ -30,7 +32,7 @@ nextStep();

}; return ( - <Paper className={classes.container}> + <Paper {...props}> <TextField className={classes.textField} label={t("event.creation.event_name")}

@@ -39,6 +41,8 @@ autoFocus

margin="dense" value={name} onChange={(e) => setName(e.target.value)} + id="NewEventName" + name="name" /> <TextField className={classes.textField}

@@ -47,6 +51,9 @@ fullWidth

margin="dense" value={email} onChange={(e) => setEmail(e.target.value)} + id="NewEventEmail" + name="email" + type="email" /> <Button className={classes.button}

@@ -63,9 +70,6 @@ );

}; const useStyles = makeStyles((theme) => ({ - container: { - padding: theme.spacing(2), - }, textField: {}, button: { marginTop: theme.spacing(2),
M app/src/containers/CreateEvent/Step2.jsapp/src/containers/CreateEvent/Step2.js

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

import React, { useState } from "react"; -import Paper from "@material-ui/core/Paper"; import { makeStyles } from "@material-ui/core/styles"; import TextField from "@material-ui/core/TextField"; import Button from "@material-ui/core/Button";

@@ -8,8 +7,9 @@ import { DatePicker } from "@material-ui/pickers";

import moment from "moment"; import { useHistory } from "react-router-dom"; import { useToast } from "../../contexts/Toast"; +import Paper from "../../components/Paper"; -const Step2 = ({ event, addToEvent, createEvent }) => { +const Step2 = ({ event, addToEvent, createEvent, ...props }) => { const classes = useStyles(); const { t } = useTranslation(); const history = useHistory();

@@ -28,7 +28,7 @@ else history.push(`/e/${result.id}`);

}; return ( - <Paper className={classes.container}> + <Paper {...props}> <DatePicker label={t("event.creation.date")} value={date}

@@ -37,6 +37,8 @@ className={classes.textField}

fullWidth format="DD.MM.YYYY" disablePast + id="NewEventDate" + name="date" /> <TextField className={classes.textField}

@@ -47,6 +49,8 @@ multiline

rows={4} value={address} onChange={(e) => setAddress(e.target.value)} + id="NewEventAddress" + name="address" /> <Button className={classes.button}

@@ -54,6 +58,7 @@ variant="contained"

color="secondary" fullWidth onClick={onCreate} + id="NewEventSubmit" > {t("event.creation.create")} </Button>

@@ -62,9 +67,6 @@ );

}; const useStyles = makeStyles((theme) => ({ - container: { - padding: theme.spacing(2), - }, textField: {}, button: { marginTop: theme.spacing(2),
M app/src/containers/CreateEvent/index.jsapp/src/containers/CreateEvent/index.js

@@ -32,6 +32,7 @@ addToEvent={addToEvent}

createEvent={createEvent} nextStep={() => setStep(step + 1)} previousStep={() => setStep(step - 1)} + id="NewEvent" /> ); };
A app/src/containers/EventDetails/index.js

@@ -0,0 +1,89 @@

+import React from "react"; +import Typography from "@material-ui/core/Typography"; +import TextField from "../../components/TextField"; +import moment from "moment"; +import { useEvent } from "../../contexts/Event"; +import { useTranslation } from "react-i18next"; +import { makeStyles } from "@material-ui/core"; +import Button from "@material-ui/core/Button"; +import { DatePicker } from "@material-ui/pickers"; +import Map from "../../components/Map"; + +const EventDetails = ({ toggleDetails }) => { + const { t } = useTranslation(); + const classes = useStyles(); + const { event, isEditing, setEditingEvent, editingEvent } = useEvent(); + + if (!event) return null; + + return ( + <div> + <div className={classes.section}> + <Typography variant="h6">{t("event.fields.starts_on")}</Typography> + {isEditing ? ( + <DatePicker + value={ + editingEvent.date ? moment(editingEvent.date) : moment(event.date) + } + onChange={(date) => + setEditingEvent({ ...editingEvent, date: date.toISOString() }) + } + className={classes.textField} + fullWidth + format="DD.MM.YYYY" + disablePast + id="UpdateEventDate" + name="date" + TextFieldComponent={(p) => <TextField light {...p} />} + /> + ) : ( + <Typography variant="body1">{event.date}</Typography> + )} + </div> + <div className={classes.section}> + <Typography variant="h6">{t("event.fields.address")}</Typography> + {isEditing ? ( + <TextField + light + multiline + rows={4} + value={editingEvent.address ?? event.address} + onChange={(e) => + setEditingEvent({ ...editingEvent, address: e.target.value }) + } + id="UpdateEventAddress" + name="address" + /> + ) : ( + <Typography variant="body1">{event.address}</Typography> + )} + </div> + <div className={classes.actions}> + <Button onClick={toggleDetails} variant="contained"> + {t("event.actions.find_car")} + </Button> + </div> + {event.position && ( + <div className={classes.map}> + <Map position={event.position} /> + </div> + )} + </div> + ); +}; + +const useStyles = makeStyles((theme) => ({ + section: { + marginBottom: theme.spacing(2), + }, + actions: { + display: "flex", + justifyContent: "center", + marginTop: theme.spacing(4), + }, + map: { + marginTop: theme.spacing(4), + }, +})); + +export default EventDetails;
A app/src/containers/EventMenu/index.js

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

+import React from "react"; +import Menu from "@material-ui/core/Menu"; +import MenuItem from "@material-ui/core/MenuItem"; + +const EventMenu = ({ anchorEl, setAnchorEl, actions = [] }) => { + return ( + <Menu + anchorEl={anchorEl} + anchorOrigin={{ + vertical: "top", + horizontal: "right", + }} + keepMounted + transformOrigin={{ + vertical: "top", + horizontal: "right", + }} + open={!!anchorEl} + onClose={() => setAnchorEl(null)} + > + {actions && + actions.map((action, idx) => ( + <MenuItem + onClick={() => { + action.onClick(); + setAnchorEl(null); + }} + key={idx} + > + {action.label} + </MenuItem> + ))} + </Menu> + ); +}; + +export default EventMenu;
A app/src/contexts/Event.js

@@ -0,0 +1,52 @@

+import React, { + createContext, + useContext, + useEffect, + useMemo, + useState, +} from "react"; +import { useStrapi } from "strapi-react-context"; + +const EventContext = createContext(); +export default EventContext; +export const useEvent = () => useContext(EventContext); + +export const EventProvider = ({ match, children }) => { + const strapi = useStrapi(); + const { eventId } = match.params; + const [isEditing, setIsEditing] = useState(false); + const [editingEvent, setEditingEvent] = useState({}); + + // Fetch event data if not already done + useEffect(() => { + if (!strapi.stores.events?.find(({ id }) => eventId === id)) + strapi.services.events.findOne(eventId); + }, [eventId, strapi.stores.events, strapi.services.events]); + + // Retrieve event data + const event = useMemo( + () => strapi.stores.events?.find((e) => e.id === eventId), + [eventId, strapi.stores.events] + ); + + const updateEvent = async () => { + const result = await strapi.services.events.update(event.id, editingEvent); + setEditingEvent({}); + return result; + }; + + return ( + <EventContext.Provider + value={{ + event, + isEditing, + setIsEditing, + editingEvent, + setEditingEvent, + updateEvent, + }} + > + {children} + </EventContext.Provider> + ); +};
M app/src/locales/fr.jsonapp/src/locales/fr.json

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

{ + "generic": { "loading": "Chargement..." }, "event": { + "fields": { + "starts_on": "Commence le", + "address": "Adresse" + }, "creation": { "event_name": "Nom de l'événement", "creator_email": "Votre e-mail",

@@ -8,8 +13,22 @@ "address": "Adresse de l'événement",

"next": "Suivant", "create": "Créer" }, + "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" + }, "errors": { - "cant_create": "Impossible de créer l'événement" + "cant_create": "Impossible de créer l'événement", + "cant_update": "Impossible de modifier l'événement" + } + }, + "car": { + "fields": { + "meeting_point": "Lieu de rencontre", + "details": "Notes" } } }
M app/src/pages/Event.jsapp/src/pages/Event.js

@@ -1,7 +1,129 @@

-import React from "react"; +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 { useEvent, EventProvider } from "../contexts/Event"; +import CarColumns from "../containers/CarColumns"; +import { useToast } from "../contexts/Toast"; const Event = () => { - return <>Event</>; + const { t } = useTranslation(); + const { addToast } = useToast(); + const [anchorEl, setAnchorEl] = useState(null); + const [detailsOpen, toggleDetails] = useReducer((i) => !i, false); + const classes = useStyles({ detailsOpen }); + const { + event, + isEditing, + setIsEditing, + editingEvent, + setEditingEvent, + updateEvent, + } = useEvent(); + + useEffect(() => { + if (!detailsOpen) setIsEditing(false); + }, [detailsOpen]); + + const onEventSave = async (e) => { + try { + await updateEvent(); + setIsEditing(false); + } catch (error) { + console.error(error); + addToast(t("event.errors.cant_update")); + } + }; + + if (!event) return <div>{t("generic.loading")}</div>; + + return ( + <Layout> + <AppBar position="static" className={classes.appbar}> + <Toolbar> + {isEditing ? ( + <TextField + light + value={editingEvent.name ?? event.name} + onChange={(e) => + setEditingEvent({ ...editingEvent, name: e.target.value }) + } + id="NewEventName" + name="name" + /> + ) : ( + <Typography variant="h6" className={classes.name}> + {event.name} + </Typography> + )} + {!detailsOpen && ( + <IconButton + edge="end" + onClick={(e) => setAnchorEl(e.currentTarget)} + > + <Icon className={classes.barIcon}>more_vert</Icon> + </IconButton> + )} + {detailsOpen && !isEditing && ( + <IconButton edge="end" onClick={(e) => setIsEditing(true)}> + <Icon className={classes.barIcon}>edit</Icon> + </IconButton> + )} + {detailsOpen && isEditing && ( + <IconButton edge="end" onClick={onEventSave}> + <Icon className={classes.barIcon}>done</Icon> + </IconButton> + )} + <EventMenu + anchorEl={anchorEl} + setAnchorEl={setAnchorEl} + actions={[ + { + label: detailsOpen + ? t("event.actions.hide_details") + : t("event.actions.show_details"), + onClick: toggleDetails, + }, + { label: t("event.actions.add_car"), onClick: () => {} }, + { label: t("event.actions.invite"), onClick: () => {} }, + ]} + /> + </Toolbar> + <Container className={classes.container} maxWidth="sm"> + <EventDetails toggleDetails={toggleDetails} /> + </Container> + </AppBar> + <CarColumns cars={event.cars} /> + </Layout> + ); }; -export default Event; +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, + }), + name: { + flexGrow: 1, + }, + barIcon: { + color: "white", + }, +})); + +export default (props) => ( + <EventProvider {...props}> + <Event {...props} /> + </EventProvider> +);
M extensions/documentation/documentation/1.0.0/full_documentation.jsonextensions/documentation/documentation/1.0.0/full_documentation.json

@@ -14,7 +14,7 @@ "license": {

"name": "Apache 2.0", "url": "https://www.apache.org/licenses/LICENSE-2.0.html" }, - "x-generation-date": "06/29/2020 2:11:59 PM" + "x-generation-date": "06/30/2020 3:56:41 PM" }, "x-strapi-config": { "path": "/documentation",

@@ -1637,8 +1637,17 @@ "type": "array",

"items": { "type": "string" } + }, + "position": { + "type": "object" + }, + "waiting_list": { + "type": "object" } } + }, + "passengers": { + "type": "object" } } },

@@ -1668,6 +1677,9 @@ "type": "string"

}, "event": { "type": "string" + }, + "passengers": { + "type": "object" } } },

@@ -1725,9 +1737,18 @@ "type": "string"

}, "event": { "type": "string" + }, + "passengers": { + "type": "object" } } } + }, + "position": { + "type": "object" + }, + "waiting_list": { + "type": "object" } } },

@@ -1754,6 +1775,12 @@ "type": "array",

"items": { "type": "string" } + }, + "position": { + "type": "object" + }, + "waiting_list": { + "type": "object" } } },
M package-lock.jsonpackage-lock.json

@@ -2017,7 +2017,7 @@ "integrity": "sha512-3YDiu347mtVtjpyV3u5kVqQLP242c06zwDOgpeRnybmXlYYsLbtTrUBUm8i8srONt+FWobl5aibnU1030PeeuA=="

}, "axios": { "version": "0.19.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.19.2.tgz", + "resolved": "https://npm-8ee.hidora.com/axios/-/axios-0.19.2.tgz", "integrity": "sha512-fjgm5MvRHLhx+osE2xoekY70AhARk3a6hkN+3Io1jc00jtquGvxYlKlsFUhmUET0V5te6CcZI7lcv2Ym61mjHA==", "requires": { "follow-redirects": "1.5.10"
M package.jsonpackage.json

@@ -15,6 +15,7 @@ "babel-eslint": "^10.1.0",

"eslint": "^7.3.1" }, "dependencies": { + "axios": "^0.19.2", "strapi": "3.0.5", "strapi-admin": "3.0.5", "strapi-connector-mongoose": "3.0.5",