From ec575622c6e1d789607064781ef799442bddfa89 Mon Sep 17 00:00:00 2001 From: Thomas TRIDON Date: Fri, 25 May 2018 16:14:51 +0200 Subject: [PATCH 1/5] I009 Work inprogress * created sub-header (navigation tool) --- src/client/web/components/Event/Preview.js | 11 ++++- src/client/web/components/Event/View.js | 51 ++++++++++++++++++++++ src/client/web/routes.js | 2 + 3 files changed, 63 insertions(+), 1 deletion(-) create mode 100644 src/client/web/components/Event/View.js diff --git a/src/client/web/components/Event/Preview.js b/src/client/web/components/Event/Preview.js index 47195be..08bf068 100644 --- a/src/client/web/components/Event/Preview.js +++ b/src/client/web/components/Event/Preview.js @@ -27,7 +27,15 @@ const style = { }, }; -const Preview = ({ attendeeIds, createdAt, label, image, people, classes }) => { +const Preview = ({ + id, + attendeeIds, + createdAt, + label, + image, + people, + classes, +}) => { return ( { }; Preview.propTypes = { + id: PropTypes.string, attendeeIds: PropTypes.array, createdAt: PropTypes.string, label: PropTypes.string, diff --git a/src/client/web/components/Event/View.js b/src/client/web/components/Event/View.js new file mode 100644 index 0000000..926b752 --- /dev/null +++ b/src/client/web/components/Event/View.js @@ -0,0 +1,51 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import injectSheet from 'react-jss'; +import { compose, withStateHandlers } from 'recompose'; +import BottomNavigation from '@material-ui/core/BottomNavigation'; +import BottomNavigationAction from '@material-ui/core/BottomNavigationAction'; +import RestoreIcon from '@material-ui/icons/Restore'; +import FavoriteIcon from '@material-ui/icons/Favorite'; +import LocationOnIcon from '@material-ui/icons/LocationOn'; + +const styles = { + root: { + display: 'flex', + justifyContent: 'space-around', + margin: 5, + // backgroundColor: '#4054b2', + }, +}; + +const View = ({ classes, tab, setTab }) => { + return ( + setTab(value)} + // showLabels + className={classes.root} + > + } /> + } /> + } /> + + ); +}; + +View.propTypes = { + classes: PropTypes.object, + tab: PropTypes.number, + setTab: PropTypes.func, +}; + +export default compose( + injectSheet(styles), + withStateHandlers( + { + tab: 0, + }, + { + setTab: () => value => ({ tab: value }), + }, + ), +)(View); diff --git a/src/client/web/routes.js b/src/client/web/routes.js index a2b6218..4cfaf87 100644 --- a/src/client/web/routes.js +++ b/src/client/web/routes.js @@ -1,10 +1,12 @@ import { find, prop, values } from 'ramda'; import Events from './pages/Events'; +import View from './components/Event/View'; const routes = { about: { exact: true, path: '/about', + component: View, }, events: { exact: true, From 517e002fc05a4264212d2f12e96cbdeb9aad408e Mon Sep 17 00:00:00 2001 From: Thomas Tridon Date: Tue, 29 May 2018 12:56:09 +0200 Subject: [PATCH 2/5] I009 Save Work * implemente a sub header to navigate between spending - people - stats * try to find a good way to get the Event, and the spendings associated --- src/client/web/actions/event.js | 19 +++++++++++++++++++ src/client/web/components/Event/View.js | 22 ++++++++++++---------- src/client/web/index.js | 2 ++ src/client/web/pages/Event/component.js | 13 +++++++++++++ src/client/web/pages/Event/index.js | 10 ++++++++++ src/client/web/reducers/event.js | 18 ++++++++++++++++++ src/client/web/reducers/index.js | 2 ++ src/client/web/routes.js | 8 ++++++-- src/client/web/selectors/event.js | 1 + src/mock/api.vibes.js | 7 ++++++- src/mock/data.js | 15 +++++++++++++++ src/mock/endpoints.js | 2 ++ 12 files changed, 106 insertions(+), 13 deletions(-) create mode 100644 src/client/web/actions/event.js create mode 100644 src/client/web/pages/Event/component.js create mode 100644 src/client/web/pages/Event/index.js create mode 100644 src/client/web/reducers/event.js create mode 100644 src/client/web/selectors/event.js diff --git a/src/client/web/actions/event.js b/src/client/web/actions/event.js new file mode 100644 index 0000000..fe41f51 --- /dev/null +++ b/src/client/web/actions/event.js @@ -0,0 +1,19 @@ +import { requestJson } from './utils'; + +export const LOAD_SPENDINGS = 'spendings:load'; +export const loadSpendings = () => dispatch => { + requestJson({ + method: 'GET', + url: '/api/spendings', + }) + .then(data => dispatch(spendingsLoaded(data))) + /* eslint-disable no-console */ + .catch(() => alert('spendings:load ERROR')); + /* eslint-enable no-console */ +}; + +export const SPENDINGS_LOADED = 'spendings:loaded'; +export const spendingsLoaded = spendings => ({ + type: SPENDINGS_LOADED, + payload: { spendings }, +}); diff --git a/src/client/web/components/Event/View.js b/src/client/web/components/Event/View.js index 926b752..c10956d 100644 --- a/src/client/web/components/Event/View.js +++ b/src/client/web/components/Event/View.js @@ -19,16 +19,18 @@ const styles = { const View = ({ classes, tab, setTab }) => { return ( - setTab(value)} - // showLabels - className={classes.root} - > - } /> - } /> - } /> - +
+ setTab(value)} + showLabels + className={classes.root} + > + } /> + } /> + } /> + +
); }; diff --git a/src/client/web/index.js b/src/client/web/index.js index d886366..f6c8999 100644 --- a/src/client/web/index.js +++ b/src/client/web/index.js @@ -6,6 +6,7 @@ import { createStore, applyMiddleware } from 'redux'; import createLogger from 'redux-logger'; import thunk from 'redux-thunk'; import reducer from './reducers'; +import { loadSpendings } from './actions/event'; import { loadEvents } from './actions/events'; import { loadPeople } from './actions/people'; import App from './components/App'; @@ -14,6 +15,7 @@ const store = createStore(reducer, applyMiddleware(thunk, createLogger)); store.dispatch(loadEvents()); store.dispatch(loadPeople()); +store.dispatch(loadSpendings()); ReactDOM.render( diff --git a/src/client/web/pages/Event/component.js b/src/client/web/pages/Event/component.js new file mode 100644 index 0000000..f43635f --- /dev/null +++ b/src/client/web/pages/Event/component.js @@ -0,0 +1,13 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import View from '../../components/Event/View'; + +const Event = ({ event }) => { + return ; +}; + +Event.propTypes = { + event: PropTypes.object, +}; + +export default Event; diff --git a/src/client/web/pages/Event/index.js b/src/client/web/pages/Event/index.js new file mode 100644 index 0000000..afbfaaa --- /dev/null +++ b/src/client/web/pages/Event/index.js @@ -0,0 +1,10 @@ +import { connect } from 'react-redux'; +import Event from './component.js'; + +const mapStateToProps = state => { + return { + event: state.event, + }; +}; + +export default connect(mapStateToProps)(Event); diff --git a/src/client/web/reducers/event.js b/src/client/web/reducers/event.js new file mode 100644 index 0000000..cf95cda --- /dev/null +++ b/src/client/web/reducers/event.js @@ -0,0 +1,18 @@ +import { SPENDINGS_LOADED } from '../actions/event'; + +const initialState = { + eventId: '', + spendings: [], +}; + +const event = (state = initialState, action) => { + switch (action.type) { + case SPENDINGS_LOADED: + return { ...state, spendings: action.payload.spendings }; + default: { + return state; + } + } +}; + +export default event; diff --git a/src/client/web/reducers/index.js b/src/client/web/reducers/index.js index ef064c0..aa40595 100644 --- a/src/client/web/reducers/index.js +++ b/src/client/web/reducers/index.js @@ -1,8 +1,10 @@ import { combineReducers } from 'redux'; +import event from './event'; import events from './events'; import people from './people'; const reducer = combineReducers({ + event, events, people, }); diff --git a/src/client/web/routes.js b/src/client/web/routes.js index 4cfaf87..127357c 100644 --- a/src/client/web/routes.js +++ b/src/client/web/routes.js @@ -1,12 +1,11 @@ import { find, prop, values } from 'ramda'; import Events from './pages/Events'; -import View from './components/Event/View'; +import Event from './pages/Event'; const routes = { about: { exact: true, path: '/about', - component: View, }, events: { exact: true, @@ -14,6 +13,11 @@ const routes = { component: Events, default: true, }, + event: { + exact: true, + path: '/event/:id', + component: Event, + }, }; export const defaultRoute = find(prop('default'), values(routes)); diff --git a/src/client/web/selectors/event.js b/src/client/web/selectors/event.js new file mode 100644 index 0000000..1b261f9 --- /dev/null +++ b/src/client/web/selectors/event.js @@ -0,0 +1 @@ +export const getSpendings = state => state.spendings; diff --git a/src/mock/api.vibes.js b/src/mock/api.vibes.js index e53045b..4cb0226 100644 --- a/src/mock/api.vibes.js +++ b/src/mock/api.vibes.js @@ -11,13 +11,14 @@ vibe.default( api: { clientId, clientSecret, - data: { events, people }, + data: { events, people, spendings }, }, }, }, ) => { mock('events:list').reply([200, events]); mock('people:list').reply([200, people]); + mock('spendings:list').reply([200, spendings]); mock('event:add').reply((req, res) => { const id = faker.random.uuid(); const newEvent = { @@ -43,5 +44,9 @@ vibe.default( const { id } = req.params; res.json(find(propEq('id', id), events)); }); + mock('spendings:get').reply((req, res) => { + const { id } = req.params; + res.json(find(propEq('id', id), spendings)); + }); }, ); diff --git a/src/mock/data.js b/src/mock/data.js index 1fb3def..62c2929 100644 --- a/src/mock/data.js +++ b/src/mock/data.js @@ -14,12 +14,26 @@ const Person = () => ({ const people = times(Person, 50); +const Spending = () => ({ + id: faker.random.uuid(), + label: faker.lorem.words(), + attendeeIds: compose(uniq, map(prop('id')))( + times(getRandom(people), random(0, 10)), + ), + createdAt: faker.date.past(), +}); + +const spendings = times(Spending, 50); + const Event = () => ({ id: faker.random.uuid(), label: faker.lorem.words(), attendeeIds: compose(uniq, map(prop('id')))( times(getRandom(people), random(0, 10)), ), + spendingIds: compose(uniq, map(prop('id')))( + times(getRandom(spendings), random(0, 10)), + ), image: faker.image.image(), currency: 'EUR', createdAt: faker.date.past(), @@ -30,4 +44,5 @@ const events = times(Event, 10); module.exports = { people, events, + spendings, }; diff --git a/src/mock/endpoints.js b/src/mock/endpoints.js index 6a6a953..4242ba6 100644 --- a/src/mock/endpoints.js +++ b/src/mock/endpoints.js @@ -5,3 +5,5 @@ endpoint('event:add', { uri: '/api/events', method: 'post' }); endpoint('event:update', { uri: '/api/events/:id', method: 'patch' }); endpoint('event:get', { uri: '/api/events/:id', method: 'get' }); endpoint('people:list', { uri: '/api/people', method: 'get' }); +endpoint('spendings:list', { uri: '/api/spendings', method: 'get' }); +endpoint('spendings:get', { uri: '/api/spendings/:id', method: 'get' }); From 54106818aec9aa5682a2cf0b4f46877f26d06da9 Mon Sep 17 00:00:00 2001 From: Thomas TRIDON Date: Tue, 29 May 2018 19:22:21 +0200 Subject: [PATCH 3/5] I009 Enhance Event View * display all spendings (even not associated spendings) * allow to create, edit and delete a spending --- src/client/web/actions/event.js | 52 ++++++++++- src/client/web/components/Event/Preview.js | 4 +- src/client/web/components/Event/View.js | 53 ------------ src/client/web/components/Spending/Add.js | 83 ++++++++++++++++++ .../web/components/Spending/AddOrEdit.js | 52 +++++++++++ src/client/web/components/Spending/Edit.js | 83 ++++++++++++++++++ src/client/web/components/Spending/Form.js | 43 ++++++++++ src/client/web/components/Spending/Preview.js | 50 +++++++++++ src/client/web/pages/Event/component.js | 86 ++++++++++++++++++- src/client/web/pages/Event/index.js | 67 +++++++++++++-- src/client/web/pages/Events/component.js | 11 ++- src/client/web/pages/Events/index.js | 2 + src/client/web/reducers/event.js | 33 ++++++- src/client/web/selectors/event.js | 6 +- src/mock/api.vibes.js | 39 +++++++-- src/mock/data.js | 7 +- src/mock/endpoints.js | 9 +- 17 files changed, 599 insertions(+), 81 deletions(-) delete mode 100644 src/client/web/components/Event/View.js create mode 100644 src/client/web/components/Spending/Add.js create mode 100644 src/client/web/components/Spending/AddOrEdit.js create mode 100644 src/client/web/components/Spending/Edit.js create mode 100644 src/client/web/components/Spending/Form.js create mode 100644 src/client/web/components/Spending/Preview.js diff --git a/src/client/web/actions/event.js b/src/client/web/actions/event.js index fe41f51..239a21a 100644 --- a/src/client/web/actions/event.js +++ b/src/client/web/actions/event.js @@ -7,9 +7,7 @@ export const loadSpendings = () => dispatch => { url: '/api/spendings', }) .then(data => dispatch(spendingsLoaded(data))) - /* eslint-disable no-console */ .catch(() => alert('spendings:load ERROR')); - /* eslint-enable no-console */ }; export const SPENDINGS_LOADED = 'spendings:loaded'; @@ -17,3 +15,53 @@ export const spendingsLoaded = spendings => ({ type: SPENDINGS_LOADED, payload: { spendings }, }); + +export const ADD_SPENDING = 'spending:add'; +export const addSpending = data => dispatch => { + requestJson({ + method: 'POST', + url: '/api/spendings', + body: data, + }) + .then(data => dispatch(spendingAdded(data))) + .catch(() => alert('spending:add ERROR')); +}; + +export const SPENDING_ADDED = 'spending:added'; +export const spendingAdded = spending => ({ + type: SPENDING_ADDED, + payload: { spending }, +}); + +export const UPDATE_SPENDING = 'spending:update'; +export const updateSpending = data => dispatch => { + requestJson({ + method: 'PATCH', + url: `/api/spendings/${data.id}`, + body: data, + }) + .then(data => dispatch(spendingUpdated(data))) + .catch(() => alert('spendings:update ERROR')); +}; + +export const SPENDING_UPDATED = 'spending:updated'; +export const spendingUpdated = spending => ({ + type: SPENDING_UPDATED, + payload: { spending }, +}); + +export const DELETE_SPENDING = 'spending:deleted'; +export const deleteSpending = id => dispatch => { + requestJson({ + method: 'DELETE', + url: `/api/spendings/${id}`, + }) + .then(data => dispatch(spendingDeleted(data))) + .catch(() => alert('spending:delete ERROR')); +}; + +export const SPENDING_DELETED = 'spending:deleted'; +export const spendingDeleted = ({ id }) => ({ + type: SPENDING_DELETED, + payload: { id }, +}); diff --git a/src/client/web/components/Event/Preview.js b/src/client/web/components/Event/Preview.js index 08bf068..ef4c29a 100644 --- a/src/client/web/components/Event/Preview.js +++ b/src/client/web/components/Event/Preview.js @@ -35,9 +35,10 @@ const Preview = ({ image, people, classes, + history, }) => { return ( - + history.push(`/event/${id}`)}> { - return ( -
- setTab(value)} - showLabels - className={classes.root} - > - } /> - } /> - } /> - -
- ); -}; - -View.propTypes = { - classes: PropTypes.object, - tab: PropTypes.number, - setTab: PropTypes.func, -}; - -export default compose( - injectSheet(styles), - withStateHandlers( - { - tab: 0, - }, - { - setTab: () => value => ({ tab: value }), - }, - ), -)(View); diff --git a/src/client/web/components/Spending/Add.js b/src/client/web/components/Spending/Add.js new file mode 100644 index 0000000..3ca17ad --- /dev/null +++ b/src/client/web/components/Spending/Add.js @@ -0,0 +1,83 @@ +import React, { Fragment } from 'react'; +import PropTypes from 'prop-types'; +import injectSheet from 'react-jss'; +import { Formik } from 'formik'; +import DialogActions from '@material-ui/core/DialogActions'; +import DialogContent from '@material-ui/core/DialogContent'; +import DialogTitle from '@material-ui/core/DialogTitle'; +import Button from 'material-ui/Button'; +import AddOrEdit from './AddOrEdit'; + +const styles = { + appBar: { + position: 'relative', + }, + flex: { + flex: 1, + }, + input: { + margin: 5, + }, + form: { + display: 'grid', + gridTemplateColumns: 'auto', + gridTemplateRows: 'auto', + gridTemplateAreas: "'label' 'currency' 'people'", + }, + formControl: { + margin: 5, + minWidth: 120, + }, +}; + +const Add = ({ addSpending, handleClose, classes }) => { + return ( + { + let errors = {}; + + if (!values.label) { + errors.label = 'Name is required'; + } + + return errors; + }} + onSubmit={values => { + const newSpending = { + ...values, + }; + + addSpending(newSpending); + handleClose(); + }} + render={({ handleSubmit }) => ( + + Fill the form + + + + + + + + + )} + /> + ); +}; + +Add.propTypes = { + addSpending: PropTypes.func, + handleClose: PropTypes.func, + classes: PropTypes.object, +}; + +export default injectSheet(styles)(Add); diff --git a/src/client/web/components/Spending/AddOrEdit.js b/src/client/web/components/Spending/AddOrEdit.js new file mode 100644 index 0000000..6175a0f --- /dev/null +++ b/src/client/web/components/Spending/AddOrEdit.js @@ -0,0 +1,52 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Form } from 'formik'; +import { Field } from 'formik'; +import { InputField, SelectField } from '../../fields'; + +const AddOrEdit = ({ classes }) => { + const currencies = [ + { + value: 'USD', + label: '$', + }, + { + value: 'EUR', + label: '€', + }, + { + value: 'BTC', + label: '฿', + }, + { + value: 'JPY', + label: '¥', + }, + ]; + + return ( +
+ + + + ); +}; + +AddOrEdit.propTypes = { + classes: PropTypes.object, +}; + +export default AddOrEdit; diff --git a/src/client/web/components/Spending/Edit.js b/src/client/web/components/Spending/Edit.js new file mode 100644 index 0000000..a326544 --- /dev/null +++ b/src/client/web/components/Spending/Edit.js @@ -0,0 +1,83 @@ +import React, { Fragment } from 'react'; +import PropTypes from 'prop-types'; +import injectSheet from 'react-jss'; +import { Formik } from 'formik'; +import DialogActions from '@material-ui/core/DialogActions'; +import DialogContent from '@material-ui/core/DialogContent'; +import DialogTitle from '@material-ui/core/DialogTitle'; +import Button from 'material-ui/Button'; +import AddOrEdit from './AddOrEdit'; + +const styles = { + appBar: { + position: 'relative', + }, + flex: { + flex: 1, + }, + input: { + margin: 5, + }, + form: { + display: 'grid', + gridTemplateColumns: 'auto', + gridTemplateRows: 'auto', + gridTemplateAreas: "'label' 'currency' 'people'", + }, + formControl: { + margin: 5, + minWidth: 120, + }, +}; + +const Edit = ({ spending, updateSpending, handleClose, classes }) => { + return ( + { + let errors = {}; + + if (!values.label) { + errors.label = 'Name is required'; + } + + return errors; + }} + onSubmit={values => { + const newSpending = { + ...values, + }; + + updateSpending(newSpending); + handleClose(); + }} + render={({ handleSubmit }) => ( + + Edit the form + + + + + + + + + )} + /> + ); +}; + +Edit.propTypes = { + spending: PropTypes.object, + updateSpending: PropTypes.func, + handleClose: PropTypes.func, + classes: PropTypes.object, +}; + +export default injectSheet(styles)(Edit); diff --git a/src/client/web/components/Spending/Form.js b/src/client/web/components/Spending/Form.js new file mode 100644 index 0000000..76ec6f0 --- /dev/null +++ b/src/client/web/components/Spending/Form.js @@ -0,0 +1,43 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import Dialog from '@material-ui/core/Dialog'; +import Add from './Add'; +import Edit from './Edit'; + +const Form = ({ + isOpen, + spending, + addSpending, + updateSpending, + handleClose, +}) => { + return ( + + {spending ? ( + + ) : ( + + )} + + ); +}; + +Form.propTypes = { + isOpen: PropTypes.bool, + addSpending: PropTypes.func, + updateSpending: PropTypes.func, + handleClose: PropTypes.func, + spending: PropTypes.object, +}; + +export default Form; diff --git a/src/client/web/components/Spending/Preview.js b/src/client/web/components/Spending/Preview.js new file mode 100644 index 0000000..f6bb73c --- /dev/null +++ b/src/client/web/components/Spending/Preview.js @@ -0,0 +1,50 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { format } from 'date-fns'; +import Card, { CardHeader, CardContent } from 'material-ui/Card'; +import IconButton from '@material-ui/core/IconButton'; +import Icon from '@material-ui/core/Icon'; +import DeleteIcon from '@material-ui/icons/Delete'; + +const Preview = ({ + classes, + id, + label, + due, + createdAt, + deleteSpending, + setId, +}) => { + return ( + + + setId(id)}> + edit_icon + + deleteSpending(id)}> + + + + } + subheader={format(createdAt, 'DD MMMM YYYY')} + title={label} + /> + {`${due ? due : 0} $`} + + ); +}; + +Preview.propTypes = { + classes: PropTypes.object, + id: PropTypes.string, + label: PropTypes.string, + due: PropTypes.number, + deleteSpending: PropTypes.func, + setId: PropTypes.func, + createdAt: PropTypes.string, +}; + +export default Preview; diff --git a/src/client/web/pages/Event/component.js b/src/client/web/pages/Event/component.js index f43635f..c7d5e17 100644 --- a/src/client/web/pages/Event/component.js +++ b/src/client/web/pages/Event/component.js @@ -1,13 +1,91 @@ import React from 'react'; import PropTypes from 'prop-types'; -import View from '../../components/Event/View'; +import { map } from 'ramda'; +import Button from 'material-ui/Button'; +import AddIcon from '@material-ui/icons/Add'; +import BottomNavigation from '@material-ui/core/BottomNavigation'; +import BottomNavigationAction from '@material-ui/core/BottomNavigationAction'; +import RestoreIcon from '@material-ui/icons/Restore'; +import FavoriteIcon from '@material-ui/icons/Favorite'; +import LocationOnIcon from '@material-ui/icons/LocationOn'; +import Spending from '../../components/Spending/Preview'; +import Form from '../../components/Spending/Form'; +import { getSpending } from '../../selectors/event'; -const Event = ({ event }) => { - return ; +const Event = ({ + classes, + tab, + setTab, + spendings, + addSpending, + updateSpending, + deleteSpending, + isOpen, + handleClose, + handleOpen, + id, + setId, +}) => { + return ( +
+
+ setTab(value)} + showLabels + className={classes.root} + > + } /> + } /> + } /> + +
+ {map(({ id, ...rest }) => { + return ( + + ); + }, spendings)} +
+ +
+ ); }; Event.propTypes = { - event: PropTypes.object, + history: PropTypes.object, + classes: PropTypes.object, + tab: PropTypes.number, + setTab: PropTypes.func, + spendings: PropTypes.array, + addSpending: PropTypes.func, + updateSpending: PropTypes.func, + deleteSpending: PropTypes.func, + isOpen: PropTypes.bool, + handleOpen: PropTypes.func, + handleClose: PropTypes.func, + id: PropTypes.string, + setId: PropTypes.func, }; export default Event; diff --git a/src/client/web/pages/Event/index.js b/src/client/web/pages/Event/index.js index afbfaaa..cef6fc9 100644 --- a/src/client/web/pages/Event/index.js +++ b/src/client/web/pages/Event/index.js @@ -1,10 +1,67 @@ import { connect } from 'react-redux'; +import { withRouter } from 'react-router'; +import injectSheet from 'react-jss'; +import { compose, withStateHandlers } from 'recompose'; import Event from './component.js'; +import { getSpendings } from '../../selectors/event'; +import { getEvents } from '../../selectors/events'; +import { getPeople } from '../../selectors/people'; -const mapStateToProps = state => { - return { - event: state.event, - }; +import { + addSpending, + updateSpending, + deleteSpending, +} from '../../actions/event'; + +const styles = { + root: { + display: 'flex', + justifyContent: 'space-around', + margin: 5, + // backgroundColor: '#4054b2', + }, + card: { + margin: 10, + }, + container: { + display: 'grid', + gridTemplateColumns: '1fr 1fr', + justifyContent: 'space-between', + }, + icon: { + position: 'fixed', + top: '90vh', + left: '90vw', + }, +}; + +const mapStateToProps = state => ({ + events: getEvents(state), + people: getPeople(state), + spendings: getSpendings(state), +}); + +const mapDispatchToProps = { + addSpending, + updateSpending, + deleteSpending, }; -export default connect(mapStateToProps)(Event); +export default compose( + withRouter, + connect(mapStateToProps, mapDispatchToProps), + injectSheet(styles), + withStateHandlers( + { + tab: 0, + isOpen: false, + id: '', + }, + { + setTab: () => value => ({ tab: value }), + handleOpen: () => () => ({ isOpen: true }), + handleClose: () => () => ({ isOpen: false }), + setId: () => id => ({ id: id, isOpen: true }), + }, + ), +)(Event); diff --git a/src/client/web/pages/Events/component.js b/src/client/web/pages/Events/component.js index 3556261..6fd24f9 100644 --- a/src/client/web/pages/Events/component.js +++ b/src/client/web/pages/Events/component.js @@ -14,13 +14,21 @@ const Events = ({ handleClose, classes, addEvent, + history, }) => { return (
{map( - event => , + event => ( + + ), events, )}
@@ -54,6 +62,7 @@ Events.propTypes = { handleClose: PropTypes.func.isRequired, addEvent: PropTypes.func.isRequired, classes: PropTypes.object, + history: PropTypes.object, }; export default Events; diff --git a/src/client/web/pages/Events/index.js b/src/client/web/pages/Events/index.js index a1a4cb6..2a33e8c 100644 --- a/src/client/web/pages/Events/index.js +++ b/src/client/web/pages/Events/index.js @@ -1,4 +1,5 @@ import { connect } from 'react-redux'; +import { withRouter } from 'react-router'; import { compose, withStateHandlers } from 'recompose'; import injectSheet from 'react-jss'; import { getEvents } from '../../selectors/events'; @@ -29,6 +30,7 @@ const mapDispatchToProps = { }; export default compose( + withRouter, connect(mapStateToProps, mapDispatchToProps), injectSheet(style), withStateHandlers( diff --git a/src/client/web/reducers/event.js b/src/client/web/reducers/event.js index cf95cda..8a73130 100644 --- a/src/client/web/reducers/event.js +++ b/src/client/web/reducers/event.js @@ -1,7 +1,13 @@ -import { SPENDINGS_LOADED } from '../actions/event'; +import { append, findIndex, update, propEq, remove } from 'ramda'; + +import { + SPENDINGS_LOADED, + SPENDING_ADDED, + SPENDING_UPDATED, + SPENDING_DELETED, +} from '../actions/event'; const initialState = { - eventId: '', spendings: [], }; @@ -9,6 +15,29 @@ const event = (state = initialState, action) => { switch (action.type) { case SPENDINGS_LOADED: return { ...state, spendings: action.payload.spendings }; + case SPENDING_ADDED: + return { + ...state, + spendings: append(action.payload.spending, state.spendings), + }; + case SPENDING_UPDATED: + return { + ...state, + spendings: update( + findIndex(propEq('id', action.payload.spending.id), state.spendings), + action.payload.spending, + state.spendings, + ), + }; + case SPENDING_DELETED: + return { + ...state, + spendings: remove( + findIndex(propEq('id', action.payload.id), state.spendings), + 1, + state.spendings, + ), + }; default: { return state; } diff --git a/src/client/web/selectors/event.js b/src/client/web/selectors/event.js index 1b261f9..be9516a 100644 --- a/src/client/web/selectors/event.js +++ b/src/client/web/selectors/event.js @@ -1 +1,5 @@ -export const getSpendings = state => state.spendings; +import { find, propEq } from 'ramda'; + +export const getSpendings = state => state.event.spendings; + +export const getSpending = (id, spendings) => find(propEq('id', id))(spendings); diff --git a/src/mock/api.vibes.js b/src/mock/api.vibes.js index 4cb0226..1ab9874 100644 --- a/src/mock/api.vibes.js +++ b/src/mock/api.vibes.js @@ -1,6 +1,6 @@ const { vibe } = require('farso'); const faker = require('faker'); -const { findIndex, propEq, find, path } = require('ramda'); +const { findIndex, propEq, find, path, remove, filter } = require('ramda'); vibe.default( 'Main', @@ -17,8 +17,6 @@ vibe.default( }, ) => { mock('events:list').reply([200, events]); - mock('people:list').reply([200, people]); - mock('spendings:list').reply([200, spendings]); mock('event:add').reply((req, res) => { const id = faker.random.uuid(); const newEvent = { @@ -44,9 +42,38 @@ vibe.default( const { id } = req.params; res.json(find(propEq('id', id), events)); }); - mock('spendings:get').reply((req, res) => { - const { id } = req.params; - res.json(find(propEq('id', id), spendings)); + + mock('spendings:list').reply((req, res) => + res.send(filter(spending => !spending.deleted, spendings)), + ); + mock('spending:add').reply((req, res) => { + const id = faker.random.uuid(); + const newSpending = { + id, + currency: 'EUR', + createdAt: new Date(), + ...req.body, + }; + spendings.push(newSpending); + res.json(newSpending); }); + mock('spending:update').reply((req, res) => { + const updates = req.body; + const index = findIndex(propEq('id', req.params.id), spendings); + if (index === -1) return res.sendStatus(404); + const updatedSpending = { ...spendings[index], ...updates }; + spendings[index] = updatedSpending; + res.json(updatedSpending); + }); + mock('spending:delete').reply((req, res) => { + const updates = req.body; + const index = findIndex(propEq('id', req.params.id), spendings); + if (index === -1) return res.sendStatus(404); + const updatedSpending = { ...spendings[index], deleted: true }; + spendings[index] = updatedSpending; + res.json(updatedSpending); + }); + + mock('people:list').reply([200, people]); }, ); diff --git a/src/mock/data.js b/src/mock/data.js index 62c2929..d889cf1 100644 --- a/src/mock/data.js +++ b/src/mock/data.js @@ -17,13 +17,12 @@ const people = times(Person, 50); const Spending = () => ({ id: faker.random.uuid(), label: faker.lorem.words(), - attendeeIds: compose(uniq, map(prop('id')))( - times(getRandom(people), random(0, 10)), - ), + due: random(10, 1000), + currency: 'EUR', createdAt: faker.date.past(), }); -const spendings = times(Spending, 50); +const spendings = times(Spending, 5); const Event = () => ({ id: faker.random.uuid(), diff --git a/src/mock/endpoints.js b/src/mock/endpoints.js index 4242ba6..7d09666 100644 --- a/src/mock/endpoints.js +++ b/src/mock/endpoints.js @@ -4,6 +4,11 @@ endpoint('events:list', { uri: '/api/events', method: 'get' }); endpoint('event:add', { uri: '/api/events', method: 'post' }); endpoint('event:update', { uri: '/api/events/:id', method: 'patch' }); endpoint('event:get', { uri: '/api/events/:id', method: 'get' }); -endpoint('people:list', { uri: '/api/people', method: 'get' }); + endpoint('spendings:list', { uri: '/api/spendings', method: 'get' }); -endpoint('spendings:get', { uri: '/api/spendings/:id', method: 'get' }); +endpoint('spending:add', { uri: '/api/spendings', method: 'post' }); +endpoint('spending:update', { uri: '/api/spendings/:id', method: 'patch' }); +endpoint('spending:get', { uri: '/api/spendings/:id', method: 'get' }); +endpoint('spending:delete', { uri: '/api/spendings/:id', method: 'delete' }); + +endpoint('people:list', { uri: '/api/people', method: 'get' }); From 4a05e49e7750d4dc785e626658859b1804f531f2 Mon Sep 17 00:00:00 2001 From: Thomas TRIDON Date: Tue, 29 May 2018 19:27:13 +0200 Subject: [PATCH 4/5] I009 Update Snapshot --- .../components/App/__tests__/__snapshots__/index.js.snap | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/client/web/components/App/__tests__/__snapshots__/index.js.snap b/src/client/web/components/App/__tests__/__snapshots__/index.js.snap index 969099c..650c5c7 100644 --- a/src/client/web/components/App/__tests__/__snapshots__/index.js.snap +++ b/src/client/web/components/App/__tests__/__snapshots__/index.js.snap @@ -16,6 +16,12 @@ exports[`App should match snapshot 1`] = ` key="/events" path="/events" /> + Date: Thu, 31 May 2018 11:25:48 +0200 Subject: [PATCH 5/5] I009 Enhance Spendings Form * the form is completed * remain to link the spending to the event --- src/client/web/components/Event/AddOrEdit.js | 1 + src/client/web/components/Spending/Add.js | 10 ++-- .../web/components/Spending/AddOrEdit.js | 46 ++++++++++++++++++- src/client/web/components/Spending/Edit.js | 1 + src/client/web/components/Spending/Form.js | 4 +- src/client/web/components/Spending/Preview.js | 6 +-- src/client/web/fields/index.js | 6 +-- src/client/web/pages/Event/component.js | 9 +++- src/client/web/pages/Event/index.js | 24 ++++++---- src/client/web/reducers/index.js | 4 +- .../web/reducers/{event.js => spendings.js} | 4 +- src/client/web/selectors/event.js | 5 -- src/client/web/selectors/events.js | 6 +++ src/client/web/selectors/people.js | 7 +++ src/client/web/selectors/spendings.js | 6 +++ src/mock/data.js | 40 +++++++++------- 16 files changed, 130 insertions(+), 49 deletions(-) rename src/client/web/reducers/{event.js => spendings.js} (92%) delete mode 100644 src/client/web/selectors/event.js create mode 100644 src/client/web/selectors/spendings.js diff --git a/src/client/web/components/Event/AddOrEdit.js b/src/client/web/components/Event/AddOrEdit.js index ada7a0b..fccd76c 100644 --- a/src/client/web/components/Event/AddOrEdit.js +++ b/src/client/web/components/Event/AddOrEdit.js @@ -45,6 +45,7 @@ const AddOrEdit = ({ classes }) => { id="attendeeIds" name="attendeeIds" classes={classes} + multiple={true} component={SelectPeople} /> diff --git a/src/client/web/components/Spending/Add.js b/src/client/web/components/Spending/Add.js index 3ca17ad..72226e1 100644 --- a/src/client/web/components/Spending/Add.js +++ b/src/client/web/components/Spending/Add.js @@ -30,18 +30,21 @@ const styles = { }, }; -const Add = ({ addSpending, handleClose, classes }) => { +const Add = ({ eventId, addSpending, handleClose, classes }) => { return ( { let errors = {}; if (!values.label) { - errors.label = 'Name is required'; + errors.label = 'Required field'; } return errors; @@ -50,7 +53,7 @@ const Add = ({ addSpending, handleClose, classes }) => { const newSpending = { ...values, }; - + console.log(newSpending); addSpending(newSpending); handleClose(); }} @@ -75,6 +78,7 @@ const Add = ({ addSpending, handleClose, classes }) => { }; Add.propTypes = { + eventId: PropTypes.string, addSpending: PropTypes.func, handleClose: PropTypes.func, classes: PropTypes.object, diff --git a/src/client/web/components/Spending/AddOrEdit.js b/src/client/web/components/Spending/AddOrEdit.js index 6175a0f..351c0f6 100644 --- a/src/client/web/components/Spending/AddOrEdit.js +++ b/src/client/web/components/Spending/AddOrEdit.js @@ -1,8 +1,10 @@ import React from 'react'; import PropTypes from 'prop-types'; +import { map } from 'ramda'; import { Form } from 'formik'; -import { Field } from 'formik'; -import { InputField, SelectField } from '../../fields'; +import { Field, FieldArray } from 'formik'; +import Button from '@material-ui/core/Button'; +import { InputField, SelectField, SelectPeople } from '../../fields'; const AddOrEdit = ({ classes }) => { const currencies = [ @@ -41,6 +43,46 @@ const AddOrEdit = ({ classes }) => { classes={classes} component={SelectField} /> + + { + const { attendees } = arrayHelper.form.values; + + return ( +
+ + {attendees && + attendees.map((attendee, index) => ( +
+ + + +
+ ))} +
+ ); + }} + /> ); }; diff --git a/src/client/web/components/Spending/Edit.js b/src/client/web/components/Spending/Edit.js index a326544..5b5e7aa 100644 --- a/src/client/web/components/Spending/Edit.js +++ b/src/client/web/components/Spending/Edit.js @@ -31,6 +31,7 @@ const styles = { }; const Edit = ({ spending, updateSpending, handleClose, classes }) => { + console.log(spending); return ( ) : ( - + )} ); }; Form.propTypes = { + id: PropTypes.string, isOpen: PropTypes.bool, addSpending: PropTypes.func, updateSpending: PropTypes.func, diff --git a/src/client/web/components/Spending/Preview.js b/src/client/web/components/Spending/Preview.js index f6bb73c..1fd3aab 100644 --- a/src/client/web/components/Spending/Preview.js +++ b/src/client/web/components/Spending/Preview.js @@ -10,7 +10,7 @@ const Preview = ({ classes, id, label, - due, + amount, createdAt, deleteSpending, setId, @@ -32,7 +32,7 @@ const Preview = ({ subheader={format(createdAt, 'DD MMMM YYYY')} title={label} /> - {`${due ? due : 0} $`} + {`${amount ? amount : 0} $`} ); }; @@ -41,7 +41,7 @@ Preview.propTypes = { classes: PropTypes.object, id: PropTypes.string, label: PropTypes.string, - due: PropTypes.number, + amount: PropTypes.number, deleteSpending: PropTypes.func, setId: PropTypes.func, createdAt: PropTypes.string, diff --git a/src/client/web/fields/index.js b/src/client/web/fields/index.js index 3f8320e..0f97341 100644 --- a/src/client/web/fields/index.js +++ b/src/client/web/fields/index.js @@ -64,14 +64,14 @@ const mapStateToProps = state => ({ }); export const SelectPeople = connect(mapStateToProps)( - ({ people, field, classes }) => { + ({ people, field, classes, multiple }) => { return ( People