From 8f9d1c366354d61f88f14476c4a9263666f6d122 Mon Sep 17 00:00:00 2001 From: Drew Larson Date: Sat, 4 Mar 2017 20:55:06 -0600 Subject: [PATCH] Load data with redux --- bikeshop_project/assets/js/bikes/actions.js | 7 + .../js/bikes/components/BikeForm/index.jsx | 319 +++++++++++------- .../js/bikes/components/BikeModal/index.jsx | 4 +- .../js/bikes/components/BikeTable/index.jsx | 133 ++++---- .../assets/js/bikes/components/Size/index.jsx | 38 ++- .../js/bikes/components/Source/index.jsx | 1 + bikeshop_project/assets/js/bikes/index.jsx | 27 +- bikeshop_project/assets/js/bikes/reducers.js | 27 ++ bikeshop_project/assets/js/bikes/sagas.js | 27 ++ .../assets/js/bikes/schema/index.js | 4 + .../assets/js/bikes/services/index.js | 28 ++ bikeshop_project/package.json | 6 + bikeshop_project/webpack.dev.config.js | 5 +- 13 files changed, 420 insertions(+), 206 deletions(-) create mode 100644 bikeshop_project/assets/js/bikes/actions.js create mode 100644 bikeshop_project/assets/js/bikes/reducers.js create mode 100644 bikeshop_project/assets/js/bikes/sagas.js create mode 100644 bikeshop_project/assets/js/bikes/schema/index.js create mode 100644 bikeshop_project/assets/js/bikes/services/index.js diff --git a/bikeshop_project/assets/js/bikes/actions.js b/bikeshop_project/assets/js/bikes/actions.js new file mode 100644 index 0000000..174e7b2 --- /dev/null +++ b/bikeshop_project/assets/js/bikes/actions.js @@ -0,0 +1,7 @@ +import { createAction } from 'redux-actions'; + +export const fetchBikes = createAction('fetch bikes'); +export const setBikes = createAction('set bikes'); +export const setBikesIsFetching = createAction('set bikes.isFetching'); +export const setBikesFetched = createAction('set bikes.fetched'); +export const setBike = createAction('set bike'); diff --git a/bikeshop_project/assets/js/bikes/components/BikeForm/index.jsx b/bikeshop_project/assets/js/bikes/components/BikeForm/index.jsx index d71aec0..60535ec 100644 --- a/bikeshop_project/assets/js/bikes/components/BikeForm/index.jsx +++ b/bikeshop_project/assets/js/bikes/components/BikeForm/index.jsx @@ -1,8 +1,12 @@ import React, { PropTypes } from 'react'; +import { Field, reduxForm } from 'redux-form'; +import { connect } from 'react-redux'; import Checkbox from 'material-ui/Checkbox'; import Cookies from 'js-cookie'; import FlatButton from 'material-ui/FlatButton'; +import MenuItem from 'material-ui/MenuItem'; import RaisedButton from 'material-ui/RaisedButton'; +import SelectField from 'material-ui/SelectField'; import TextField from 'material-ui/TextField'; import fetch from 'isomorphic-fetch'; import moment from 'moment-timezone'; @@ -21,12 +25,68 @@ const styles = { }, }; -class BikeForm extends React.Component { - static propTypes = { - bike: PropTypes.object, - editing: PropTypes.bool, - handleClose: PropTypes.func, +const sources = ['COS_BIKE_DIVERSION_PILOT', 'UOFS', 'DROP_OFF']; + +const friendly = (s) => { + switch (s) { + case 'COS_BIKE_DIVERSION_PILOT': + return 'City of Saskatoon Bike Diversion Pilot'; + case 'UOFS': + return 'University of Saskatchewan'; + case 'DROP_OFF': + return 'Drop Off'; + default: + return undefined; } +}; + +const sourceMenuItems = sources.map(s => + , +); + +const renderTextField = ({ input, meta: { touched, error }, ...custom }) => ( + +); + +const renderCheckbox = ({ input, meta, label, ...custom }) => ( + +); + +const renderSelectField = ({ input, label, meta: { touched, error }, children, ...custom }) => ( + input.onChange(value)} + children={children} + {...custom} + /> +); + +const validate = (values) => { + console.log(values); + const errors = {}; + const requiredFields = ['make', 'colour', 'size', 'serial_number', 'donation_source']; + + requiredFields.forEach((field) => { + if (!values[field]) { + errors[field] = 'Required'; + } + }); + return errors; +}; + +const handleSubmit = data => false; + +class BikeForm extends React.Component { constructor({ bike, editing = false }) { super(); if (editing) { @@ -40,19 +100,19 @@ class BikeForm extends React.Component { } } - handleChange = (event, value) => { + handleChange(event, value) { this.setState({ bike: { ...this.state.bike, [event.target.name]: value } }); } - handleSizeChange = (event, index, value) => { + handleSizeChange(event, index, value) { this.setState({ bike: { ...this.state.bike, size: value } }); } - handleSourceChange = (event, index, value) => { + handleSourceChange(event, index, value) { this.setState({ bike: { ...this.state.bike, source: value } }); } - handleCpicCheck = () => { + handleCpicCheck() { const id = this.state.bike.id; const serialNumber = this.state.bike.serial_number; const data = JSON.stringify({ serial_number: serialNumber }); @@ -107,6 +167,7 @@ class BikeForm extends React.Component { cpic_searched_at, created_at, stolen, + checked, } = this.props.bike; const editing = this.props.editing; const createdAtFormatted = (moment(created_at).isValid()) ? moment(created_at).tz(timezone).fromNow() : ''; @@ -116,147 +177,163 @@ class BikeForm extends React.Component { return (
-
-
- -
-
- -
-
- -
-
- -
-
-
-
- -
- {editing && -
- +
+
+
- } -
- {editing && -
-
- +
-
- +
+
+ +
- } - - {editing && -
+
- -
-
-
-
- -
-
- } -
-
- + {editing && +
+ +
+ }
{editing && -
-
- +
+ +
+
+ +
+
+ } + + {editing && +
+
+ +
+
+
+
+ +
} -
-
-
- - +
+
+ + {sourceMenuItems} + +
+ {editing && +
+
+ +
+
+ }
-
+
+
+ + +
+
+
); } } +BikeForm = reduxForm({ + form: 'BikeForm', // a unique identifier for this form + validate, +})(BikeForm); + +BikeForm = connect( + state => ({ + initialValues: state.bike, // pull initial values from account reducer + }), +)(BikeForm); + BikeForm.propTypes = { editing: PropTypes.bool, handleClose: PropTypes.func, } + export default BikeForm; + diff --git a/bikeshop_project/assets/js/bikes/components/BikeModal/index.jsx b/bikeshop_project/assets/js/bikes/components/BikeModal/index.jsx index 53342c3..80cce25 100644 --- a/bikeshop_project/assets/js/bikes/components/BikeModal/index.jsx +++ b/bikeshop_project/assets/js/bikes/components/BikeModal/index.jsx @@ -58,8 +58,10 @@ class BikeModal extends React.Component { } } -export default BikeModal.propTypes = { +BikeModal.propTypes = { open: PropTypes.bool, bike: PropTypes.object, editing: PropTypes.bool, }; + +export default BikeModal; diff --git a/bikeshop_project/assets/js/bikes/components/BikeTable/index.jsx b/bikeshop_project/assets/js/bikes/components/BikeTable/index.jsx index 58342a9..563f23e 100644 --- a/bikeshop_project/assets/js/bikes/components/BikeTable/index.jsx +++ b/bikeshop_project/assets/js/bikes/components/BikeTable/index.jsx @@ -1,27 +1,30 @@ +import { connect } from 'react-redux' import { Table, TableBody, TableHeader, TableHeaderColumn, TableRow, TableRowColumn } from 'material-ui/Table'; -import Cookies from 'js-cookie'; -import fetch from 'isomorphic-fetch'; import FlatButton from 'material-ui/FlatButton'; import FloatingActionButton from 'material-ui/FloatingActionButton'; import ContentAdd from 'material-ui/svg-icons/content/add'; import React from 'react'; import { friendlySize } from '../Size'; import BikeModal from '../BikeModal'; +import { fetchBikes, setBike } from '../../actions'; -function checkStatus(response) { - if (response.status >= 200 && response.status < 300) { - return response; - } - const error = new Error(response.statusText); - error.response = response; - throw error; -} - -function parseJSON(response) { - return response.json(); +const renderBikes = (bikes) => { + console.log(bikes); + const bikeRows = bikes.map(bike => ( + + {friendlySize(bike.size)} + {bike.colour} + {bike.make} + {bike.serial_number} + {bike.state} + {bike.claimed_by} + + + )); + return bikeRows; } -export default class BikeTable extends React.Component { +class BikeTableComponent extends React.Component { constructor(props) { super(props); @@ -38,10 +41,9 @@ export default class BikeTable extends React.Component { } componentDidMount() { - this.getBikes(); + this.props.fetchBikes(); } - getBikes = () => { handleEditBike(bike) { this.setState({ ...this.state, @@ -65,54 +67,57 @@ export default class BikeTable extends React.Component { } render() { - const bikeRows = this.state.bikes.map(bike => ( - - {friendlySize(bike.size)} - {bike.colour} - {bike.make} - {bike.serial_number} - {bike.state} - {bike.claimed_by} - - - )); - - return ( -
-
-

Bikes

- - - - - - - Size - Colour - Make - Serial number - State - Claimed by - - - - - {bikeRows.length ? - bikeRows : - - {'No bikes found.'} - - } - -
- + if (this.props.bikes.fetched) { + const bikeRows = renderBikes(Object.values(this.props.bikes.entities['bikes'] || [])); + return ( +
+
+

Bikes

+ + + + + + + Size + Colour + Make + Serial number + State + Claimed by + + + + + {bikeRows.length ? + bikeRows : + + {'No bikes found.'} + + } + +
+
-
- ); + ); + } + + return null; } } + +const mapStateToProps = state => ({ + bikes: state.bikes.bikes, +}); + +const mapDispatchToProps = dispatch => ({ + fetchBikes: () => { + dispatch(fetchBikes()); + }, + setBike: (id) => { + dispatch(setBike(id)); + }, +}); + +const BikeTable = connect(mapStateToProps, mapDispatchToProps)(BikeTableComponent); +export default BikeTable; \ No newline at end of file diff --git a/bikeshop_project/assets/js/bikes/components/Size/index.jsx b/bikeshop_project/assets/js/bikes/components/Size/index.jsx index cbd3ce1..cd9772b 100644 --- a/bikeshop_project/assets/js/bikes/components/Size/index.jsx +++ b/bikeshop_project/assets/js/bikes/components/Size/index.jsx @@ -1,6 +1,7 @@ import React, { PropTypes } from 'react'; -import SelectField from 'material-ui/SelectField'; +import { Field } from 'redux-form'; import MenuItem from 'material-ui/MenuItem'; +import SelectField from 'material-ui/SelectField'; const sizes = ['C', 'S', 'M', 'L', 'XL']; @@ -23,26 +24,33 @@ export const friendlySize = (size) => { const styles = { float: 'left', -} +}; + +const renderSelectField = ({ input, label, meta: { touched, error }, children, ...custom }) => ( + input.onChange(value)} + children={children} + {...custom} + /> +); -const Size = ({ size, onChange }) => { +const Size = () => { const items = sizes.map(s => , ); return ( -
- - - {items} - -
+ + + {items} + ); }; diff --git a/bikeshop_project/assets/js/bikes/components/Source/index.jsx b/bikeshop_project/assets/js/bikes/components/Source/index.jsx index 5698b17..b16598f 100644 --- a/bikeshop_project/assets/js/bikes/components/Source/index.jsx +++ b/bikeshop_project/assets/js/bikes/components/Source/index.jsx @@ -25,6 +25,7 @@ const Source = ({ source, onChange }) => { return (
- + ); } } -ReactDOM.render(, document.getElementById('root')); +ReactDOM.render(, document.getElementById('root')); diff --git a/bikeshop_project/assets/js/bikes/reducers.js b/bikeshop_project/assets/js/bikes/reducers.js new file mode 100644 index 0000000..07182ca --- /dev/null +++ b/bikeshop_project/assets/js/bikes/reducers.js @@ -0,0 +1,27 @@ +import { setBike, setBikes, setBikesIsFetching, setBikesFetched } from './actions'; +import { handleActions } from 'redux-actions'; + +export default handleActions({ + [setBikes]: (state, action) => ({ + ...state, + bikes: action.payload, + }), + [setBikesIsFetching]: (state, action) => ({ + ...state, + bikes: { + ...state.bikes, + isFetching: action.payload, + }, + }), + [setBikesFetched]: (state, action) => ({ + ...state, + bikes: { + ...state.bikes, + fetched: action.payload + } + }), + [setBike]: (state, action) => ({ + ...state, + ...action.payload, + }), +}, { bikes: [], bike: undefined }); diff --git a/bikeshop_project/assets/js/bikes/sagas.js b/bikeshop_project/assets/js/bikes/sagas.js new file mode 100644 index 0000000..3f35b29 --- /dev/null +++ b/bikeshop_project/assets/js/bikes/sagas.js @@ -0,0 +1,27 @@ +import { call, put, takeEvery, takeLatest } from 'redux-saga/effects'; +import { fetchBikes as fetchBikesAction, setBikes, setBikesIsFetching, setBikesFetched } from './actions'; +import { normalize } from 'normalizr'; +import * as schema from './schema'; +import Api from './services'; + + +// worker Saga: will be fired on USER_FETCH_REQUESTED actions +function* fetchBikes(action) { + try { + yield put({ type: setBikesIsFetching.toString(), payload: true }); + const bikes = yield call(Api.fetchBikes); + yield put({ type: setBikes.toString(), payload: normalize(bikes, schema.bikes) }); + yield put({ type: setBikesFetched, payload: true }); + } catch (e) { + yield put({ type: 'BIKES_FETCH_FAILED', message: e.message }); + throw e; + } finally { + yield put({ type: setBikesIsFetching.toString(), payload: false }); + } +} + +function* watchFetchBikes() { + yield takeEvery(fetchBikesAction.toString(), fetchBikes); +} + +export default watchFetchBikes; diff --git a/bikeshop_project/assets/js/bikes/schema/index.js b/bikeshop_project/assets/js/bikes/schema/index.js new file mode 100644 index 0000000..4eaf74e --- /dev/null +++ b/bikeshop_project/assets/js/bikes/schema/index.js @@ -0,0 +1,4 @@ +import { normalize, schema } from 'normalizr'; + +const bike = new schema.Entity('bikes'); +export const bikes = new schema.Array(bike); diff --git a/bikeshop_project/assets/js/bikes/services/index.js b/bikeshop_project/assets/js/bikes/services/index.js new file mode 100644 index 0000000..27d22d9 --- /dev/null +++ b/bikeshop_project/assets/js/bikes/services/index.js @@ -0,0 +1,28 @@ +import fetch from 'isomorphic-fetch'; + +const checkStatus = (response) => { + if (response.status >= 200 && response.status < 300) { + return response; + } + const error = new Error(response.statusText); + error.response = response; + throw error; +}; + +const parseJson = response => response.json(); + +const Api = { + fetchBikes() { + return fetch('/api/v1/bikes/', { + credentials: 'same-origin', + }) + .then(checkStatus) + .then(parseJson) + .then(data => data) + .catch((error) => { + console.log('request failed', error); + }); + }, +}; + +export default Api; diff --git a/bikeshop_project/package.json b/bikeshop_project/package.json index ef82e81..6bdceb9 100644 --- a/bikeshop_project/package.json +++ b/bikeshop_project/package.json @@ -18,10 +18,16 @@ "material-ui": "^0.18.1", "moment": "^2.13.0", "node-sass": "^3.4.2", + "normalizr": "^3.2.2", "react": "^15.4.1", "react-addons-css-transition-group": "^15.4.2", "react-dom": "^15.4.1", + "react-redux": "^5.0.2", "react-tap-event-plugin": "^2.0.1", + "redux": "^3.6.0", + "redux-actions": "^1.2.2", + "redux-form": "^6.5.0", + "redux-saga": "^0.14.3", "webpack": "1.13.1", "webpack-bundle-tracker": "0.0.93" }, diff --git a/bikeshop_project/webpack.dev.config.js b/bikeshop_project/webpack.dev.config.js index ef87749..8983d7f 100644 --- a/bikeshop_project/webpack.dev.config.js +++ b/bikeshop_project/webpack.dev.config.js @@ -7,8 +7,7 @@ const config = require('./webpack.base.config.js'); // Use webpack dev server config.entry = { webpack: [ - 'webpack-dev-server/client?http://localhost:3000', - 'webpack/hot/only-dev-server', + 'webpack-dev-server/client?http://workstand.docker:3000', ], signin: './assets/js/index', members: './assets/js/members/index', @@ -16,7 +15,7 @@ config.entry = { }; // override django's STATIC_URL for webpack bundles -config.output.publicPath = 'http://localhost:3000/assets/bundles/'; +config.output.publicPath = 'http://workstand.docker:3000/assets/bundles/'; config.devtool = 'eval-source-map';