From 8105af5b95a4b80ee0e38897e22d1b3eec0d3d87 Mon Sep 17 00:00:00 2001 From: Nathan Harrenstein Date: Mon, 28 Aug 2017 14:59:17 -0700 Subject: [PATCH 01/18] Configure project. --- lab-nathan/.babelrc | 4 ++ lab-nathan/.dev.env | 1 + lab-nathan/.eslintrc.json | 8 ++++ lab-nathan/.gitignore | 3 ++ lab-nathan/package.json | 43 ++++++++++++++++++ lab-nathan/webpack.config.js | 86 ++++++++++++++++++++++++++++++++++++ 6 files changed, 145 insertions(+) create mode 100644 lab-nathan/.babelrc create mode 100644 lab-nathan/.dev.env create mode 100644 lab-nathan/.eslintrc.json create mode 100644 lab-nathan/.gitignore create mode 100644 lab-nathan/package.json create mode 100644 lab-nathan/webpack.config.js diff --git a/lab-nathan/.babelrc b/lab-nathan/.babelrc new file mode 100644 index 0000000..cf6ae40 --- /dev/null +++ b/lab-nathan/.babelrc @@ -0,0 +1,4 @@ +{ + "presets": ["es2015", "react"], + "plugins": ["transform-object-rest-spread"] +} \ No newline at end of file diff --git a/lab-nathan/.dev.env b/lab-nathan/.dev.env new file mode 100644 index 0000000..dd660b8 --- /dev/null +++ b/lab-nathan/.dev.env @@ -0,0 +1 @@ +NODE_ENV='dev' \ No newline at end of file diff --git a/lab-nathan/.eslintrc.json b/lab-nathan/.eslintrc.json new file mode 100644 index 0000000..5a87f8a --- /dev/null +++ b/lab-nathan/.eslintrc.json @@ -0,0 +1,8 @@ +{ + "extends": ["eslint:recommended", "plugin:react/recommended"], + "parser": "babel-eslint", + "env": { + "browser": true, + "node": true + } +} \ No newline at end of file diff --git a/lab-nathan/.gitignore b/lab-nathan/.gitignore new file mode 100644 index 0000000..9c19dce --- /dev/null +++ b/lab-nathan/.gitignore @@ -0,0 +1,3 @@ +node_modules +build +.vscode \ No newline at end of file diff --git a/lab-nathan/package.json b/lab-nathan/package.json new file mode 100644 index 0000000..c3c8988 --- /dev/null +++ b/lab-nathan/package.json @@ -0,0 +1,43 @@ +{ + "name": "lab-nathan", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "babel-core": "^6.26.0", + "babel-loader": "^7.1.2", + "babel-plugin-transform-object-rest-spread": "^6.26.0", + "babel-preset-es2015": "^6.24.1", + "babel-preset-react": "^6.24.1", + "clean-webpack-plugin": "^0.1.16", + "css-loader": "^0.28.5", + "dotenv": "^4.0.0", + "extract-text-webpack-plugin": "^3.0.0", + "file-loader": "^0.11.2", + "html-webpack-plugin": "^2.30.1", + "node-sass": "^4.5.3", + "react": "^15.6.1", + "react-dom": "^15.6.1", + "react-redux": "^5.0.6", + "react-test-renderer": "^15.6.1", + "redux": "^3.7.2", + "sass-loader": "^6.0.6", + "superagent": "^3.6.0", + "uglifyjs-webpack-plugin": "^0.4.6", + "url-loader": "^0.5.9", + "uuid": "^3.1.0", + "webpack": "^3.5.5", + "webpack-dev-server": "^2.7.1" + }, + "devDependencies": { + "babel-eslint": "^7.2.3", + "eslint": "^4.5.0", + "eslint-plugin-react": "^7.3.0" + } +} diff --git a/lab-nathan/webpack.config.js b/lab-nathan/webpack.config.js new file mode 100644 index 0000000..25929e9 --- /dev/null +++ b/lab-nathan/webpack.config.js @@ -0,0 +1,86 @@ +'use strict'; + +const dotenv = require('dotenv'); +dotenv.config({ path: `${__dirname}/.dev.env` }); + +const production = process.env.NODE_ENV === 'production'; +const { DefinePlugin, EnvironmentPlugin } = require('webpack'); +const HtmlPlugin = require('html-webpack-plugin'); +const ExtractPlugin = require('extract-text-webpack-plugin'); +const CleanPlugin = require('clean-webpack-plugin'); +const UglifyPlugin = require('uglifyjs-webpack-plugin'); + +let plugins = [ + new EnvironmentPlugin(['NODE_ENV']), + new ExtractPlugin('bundle-[hash].css'), + new HtmlPlugin({ template: `${__dirname}/src/index.html` }), + new DefinePlugin({ __DEBUG__: JSON.stringify(!production) }) +] + +if (production) { + plugins = plugins.concate([ new CleanPlugin(), new UglifyPlugin() ]); +} + +module.exports = { + plugins, + entry: `${__dirname}/src/main.js`, + devServer: { + historyApiFallback: true + }, + devtool: production ? undefined : 'eval', + output: { + path: `${__dirname}/build`, + filename: 'bundle-[hash].js', + publicPath: process.env.CDN_URL + }, + module: { + rules: [ + { + test: /\.js$/, + exclude: /node_modules/, + loader: 'babel-loader' + }, + { + test: /\.scss$/, + loader: ExtractPlugin.extract(['css-loader','sass-loader']) + }, + { + test: /\.(woff|woff2|ttf|eot|glyph|\.svg)$/, + use: [ + { + loader: 'url-loader', + options: { + limit: 10000, + name: 'font/[name].[ext]' + } + } + ] + }, + { + test: /\.(jpg|jpeg|gif|png|tiff|svg)$/, + exclude: /\.glyph.svg/, + use: [ + { + loader: 'url-loader', + options: { + limit: 6000, + name: 'image/[name].[ext]' + }, + } + ] + }, + { + test: /\.(mp3|aac|aiff|wav|flac|m4a|mp4|ogg|ape)$/, + exclude: /\.glyph.svg/, + use: [ + { + loader: 'file-loader', + options: { + name: 'audio/[name].[ext]' + } + } + ] + } + ] + } +}; \ No newline at end of file From dfdd76c28b8df9aab0a87e43d9a0246c62e5763c Mon Sep 17 00:00:00 2001 From: Nathan Harrenstein Date: Mon, 28 Aug 2017 16:24:04 -0700 Subject: [PATCH 02/18] Add App component. --- lab-nathan/src/components/app/app.js | 32 +++++++++++++++++++ .../components/category-form/category-form.js | 0 .../components/category-item/category-item.js | 0 .../src/components/dashboard/dashboard.js | 0 lab-nathan/src/index.html | 9 ++++++ lab-nathan/src/main.js | 5 +++ 6 files changed, 46 insertions(+) create mode 100644 lab-nathan/src/components/app/app.js create mode 100644 lab-nathan/src/components/category-form/category-form.js create mode 100644 lab-nathan/src/components/category-item/category-item.js create mode 100644 lab-nathan/src/components/dashboard/dashboard.js create mode 100644 lab-nathan/src/index.html create mode 100644 lab-nathan/src/main.js diff --git a/lab-nathan/src/components/app/app.js b/lab-nathan/src/components/app/app.js new file mode 100644 index 0000000..a0feb0d --- /dev/null +++ b/lab-nathan/src/components/app/app.js @@ -0,0 +1,32 @@ +import React from 'react'; +import { BrowserRouter, Route } from 'react-router-dom'; +import { createStore } from 'redux'; +import { Provider } from 'react-redux'; +import reducer from './reducers'; +import Dashboard from '../dashboard/dashboard.js'; + +const store = createStore(reducer); + +class App extends React.Component { + componentDidMount() { + store.subscribe(() => { + console.log('__STATE__', store.getState()); + }); + + store.dispatch({ type: null }); + } + + render() { + return ( + +
+ + + +
+
+ ); + } +} + +export default App; \ No newline at end of file diff --git a/lab-nathan/src/components/category-form/category-form.js b/lab-nathan/src/components/category-form/category-form.js new file mode 100644 index 0000000..e69de29 diff --git a/lab-nathan/src/components/category-item/category-item.js b/lab-nathan/src/components/category-item/category-item.js new file mode 100644 index 0000000..e69de29 diff --git a/lab-nathan/src/components/dashboard/dashboard.js b/lab-nathan/src/components/dashboard/dashboard.js new file mode 100644 index 0000000..e69de29 diff --git a/lab-nathan/src/index.html b/lab-nathan/src/index.html new file mode 100644 index 0000000..709b2d0 --- /dev/null +++ b/lab-nathan/src/index.html @@ -0,0 +1,9 @@ + + + + Budget Tracker + + +
+ + \ No newline at end of file diff --git a/lab-nathan/src/main.js b/lab-nathan/src/main.js new file mode 100644 index 0000000..cf45095 --- /dev/null +++ b/lab-nathan/src/main.js @@ -0,0 +1,5 @@ +import React from 'react'; +import ReactDom from 'react-dom'; +import App from './components/app/app.js'; + +ReactDom.render(, document.getElementById('root')); \ No newline at end of file From 579280965dcb90da4a9167849a91ff8aa6413a67 Mon Sep 17 00:00:00 2001 From: Nathan Harrenstein Date: Mon, 28 Aug 2017 16:55:59 -0700 Subject: [PATCH 03/18] Add CategoryForm, CategoryItem, Dashboard components. --- lab-nathan/.eslintrc.json | 1 + lab-nathan/package.json | 1 + .../components/category-form/category-form.js | 30 +++++++++++++++++++ .../components/category-item/category-item.js | 25 ++++++++++++++++ .../src/components/dashboard/dashboard.js | 18 +++++++++++ 5 files changed, 75 insertions(+) diff --git a/lab-nathan/.eslintrc.json b/lab-nathan/.eslintrc.json index 5a87f8a..df7b26d 100644 --- a/lab-nathan/.eslintrc.json +++ b/lab-nathan/.eslintrc.json @@ -5,4 +5,5 @@ "browser": true, "node": true } + } \ No newline at end of file diff --git a/lab-nathan/package.json b/lab-nathan/package.json index c3c8988..89d39c5 100644 --- a/lab-nathan/package.json +++ b/lab-nathan/package.json @@ -22,6 +22,7 @@ "file-loader": "^0.11.2", "html-webpack-plugin": "^2.30.1", "node-sass": "^4.5.3", + "prop-types": "^15.5.10", "react": "^15.6.1", "react-dom": "^15.6.1", "react-redux": "^5.0.6", diff --git a/lab-nathan/src/components/category-form/category-form.js b/lab-nathan/src/components/category-form/category-form.js index e69de29..419e684 100644 --- a/lab-nathan/src/components/category-form/category-form.js +++ b/lab-nathan/src/components/category-form/category-form.js @@ -0,0 +1,30 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +class CategoryForm extends React.Component { + constructor(props) { + super(props); + } + + render() { + return ( +
+ + + +
+ ); + } +} + +CategoryForm.propTypes = { + buttonText: PropTypes.string, +}; + +export default CategoryForm; \ No newline at end of file diff --git a/lab-nathan/src/components/category-item/category-item.js b/lab-nathan/src/components/category-item/category-item.js index e69de29..e270a3c 100644 --- a/lab-nathan/src/components/category-item/category-item.js +++ b/lab-nathan/src/components/category-item/category-item.js @@ -0,0 +1,25 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +class CategoryItem extends React.Component { + constructor(props) { + super(props); + } + + render() { + return ( +
+

{this.props.category.name}

+

{this.props.category.budget}

+ +
+ ); + } +} + +CategoryItem.propTypes = { + category: PropTypes.object, +}; + +export default CategoryItem; + diff --git a/lab-nathan/src/components/dashboard/dashboard.js b/lab-nathan/src/components/dashboard/dashboard.js index e69de29..e3ff083 100644 --- a/lab-nathan/src/components/dashboard/dashboard.js +++ b/lab-nathan/src/components/dashboard/dashboard.js @@ -0,0 +1,18 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import CategoryForm from '../category-form/category-form.js'; + +class DashboardContainer extends React.Component { + render() { + return ( +
+

Dashboard

+ +
+ ); + } +} + +export default DashboardContainer; \ No newline at end of file From d2ede9e3464c74c79360bcef8dbd141663e4a81d Mon Sep 17 00:00:00 2001 From: Nathan Harrenstein Date: Mon, 28 Aug 2017 17:31:42 -0700 Subject: [PATCH 04/18] Fix bugs and complete stateless components. --- lab-nathan/package.json | 4 +++- lab-nathan/src/components/app/app.js | 18 +++++++++--------- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/lab-nathan/package.json b/lab-nathan/package.json index 89d39c5..1f039fe 100644 --- a/lab-nathan/package.json +++ b/lab-nathan/package.json @@ -4,7 +4,8 @@ "description": "", "main": "index.js", "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" + "build": "webpack", + "watch": "webpack-dev-server --inline --hot" }, "keywords": [], "author": "", @@ -26,6 +27,7 @@ "react": "^15.6.1", "react-dom": "^15.6.1", "react-redux": "^5.0.6", + "react-router-dom": "^4.2.2", "react-test-renderer": "^15.6.1", "redux": "^3.7.2", "sass-loader": "^6.0.6", diff --git a/lab-nathan/src/components/app/app.js b/lab-nathan/src/components/app/app.js index a0feb0d..b6f659e 100644 --- a/lab-nathan/src/components/app/app.js +++ b/lab-nathan/src/components/app/app.js @@ -2,23 +2,23 @@ import React from 'react'; import { BrowserRouter, Route } from 'react-router-dom'; import { createStore } from 'redux'; import { Provider } from 'react-redux'; -import reducer from './reducers'; +// import reducer from '../../reducers'; import Dashboard from '../dashboard/dashboard.js'; -const store = createStore(reducer); +// const store = createStore(reducer); class App extends React.Component { - componentDidMount() { - store.subscribe(() => { - console.log('__STATE__', store.getState()); - }); + // componentDidMount() { + // store.subscribe(() => { + // console.log('__STATE__', store.getState()); + // }); - store.dispatch({ type: null }); - } + // store.dispatch({ type: null }); + // } render() { return ( - +
From 2875af88188e41939d258fccf5c8ad0457610fc7 Mon Sep 17 00:00:00 2001 From: Nathan Harrenstein Date: Mon, 28 Aug 2017 18:13:59 -0700 Subject: [PATCH 05/18] Create redux store. --- lab-nathan/src/components/app/app.js | 18 +++++++++--------- lab-nathan/src/reducers/category.js | 14 ++++++++++++++ 2 files changed, 23 insertions(+), 9 deletions(-) create mode 100644 lab-nathan/src/reducers/category.js diff --git a/lab-nathan/src/components/app/app.js b/lab-nathan/src/components/app/app.js index b6f659e..715d5ec 100644 --- a/lab-nathan/src/components/app/app.js +++ b/lab-nathan/src/components/app/app.js @@ -2,23 +2,23 @@ import React from 'react'; import { BrowserRouter, Route } from 'react-router-dom'; import { createStore } from 'redux'; import { Provider } from 'react-redux'; -// import reducer from '../../reducers'; +import reducer from '../../reducers/category.js'; import Dashboard from '../dashboard/dashboard.js'; -// const store = createStore(reducer); +const store = createStore(reducer); class App extends React.Component { - // componentDidMount() { - // store.subscribe(() => { - // console.log('__STATE__', store.getState()); - // }); + componentDidMount() { + store.subscribe(() => { + console.log('__STATE__', store.getState()); + }); - // store.dispatch({ type: null }); - // } + store.dispatch({ type: null }); + } render() { return ( - +
diff --git a/lab-nathan/src/reducers/category.js b/lab-nathan/src/reducers/category.js new file mode 100644 index 0000000..33fbfb3 --- /dev/null +++ b/lab-nathan/src/reducers/category.js @@ -0,0 +1,14 @@ +export default (state = [], action) => { + switch (action.type) { + case 'CATEGORY_CREATE': + return [...state, action.payload]; + case 'CATEGORY_UPDATE': + return state.map(category => category.id === payload.id ? payload : category); + case 'CATEGORY_DELETE': + return state.filter(category => category.id !== payload.id); + case 'CATEGORY_RESET': + return []; + default: + return state; + } +}; \ No newline at end of file From 63eac7656d3f4af6df9b7606a77b88793445b621 Mon Sep 17 00:00:00 2001 From: Nathan Harrenstein Date: Mon, 28 Aug 2017 18:16:17 -0700 Subject: [PATCH 06/18] Add category actions. --- lab-nathan/src/actions/category-actions.js | 24 ++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 lab-nathan/src/actions/category-actions.js diff --git a/lab-nathan/src/actions/category-actions.js b/lab-nathan/src/actions/category-actions.js new file mode 100644 index 0000000..fed0ad6 --- /dev/null +++ b/lab-nathan/src/actions/category-actions.js @@ -0,0 +1,24 @@ +import uuid from 'uuid/v1'; + +export const categoryCreate = (category) => { + category.id = uuid(); + category.timestamp = new Date(); + return { + type: 'CATEGORY_CREATE', + payload: category + } +}; + +export const categoryUpdate = (category) => ({ + type: 'CATEGORY_UPDATE', + payload: category +}); + +export const categoryDelete = (category) => ({ + type: 'CATEGORY_DELETE', + payload: category +}); + +export const categoryReset = () => ({ + type: 'CATEGORY_RESET' +}); From 951f2abee47902adc26c43f21738988327517466 Mon Sep 17 00:00:00 2001 From: Nathan Harrenstein Date: Mon, 28 Aug 2017 23:22:13 -0700 Subject: [PATCH 07/18] Make CategoryItems display. --- .../components/category-form/category-form.js | 31 ++++++++++++++-- .../src/components/dashboard/dashboard.js | 35 ++++++++++++++++++- lab-nathan/src/reducers/category.js | 4 +-- 3 files changed, 64 insertions(+), 6 deletions(-) diff --git a/lab-nathan/src/components/category-form/category-form.js b/lab-nathan/src/components/category-form/category-form.js index 419e684..e42d66a 100644 --- a/lab-nathan/src/components/category-form/category-form.js +++ b/lab-nathan/src/components/category-form/category-form.js @@ -4,19 +4,43 @@ import PropTypes from 'prop-types'; class CategoryForm extends React.Component { constructor(props) { super(props); + + this.state = { + name: props.category ? props.category.name : '', + budget: props.category ? props.category.budget : '' + }; + + this.handleNameChange = this.handleNameChange.bind(this); + this.handleBudgetChange = this.handleBudgetChange.bind(this); + this.handleSubmit = this.handleSubmit.bind(this); + } + + handleNameChange(e) { + this.setState({ name: e.target.value }); + } + + handleBudgetChange(e) { + this.setState({ budget: e.target.value }); + } + + handleSubmit(e) { + e.preventDefault(); + this.props.onComplete(Object.assign({}, this.state)); } render() { return ( -
+ + placeholder='name' + onChange={this.handleNameChange} /> + placeholder='budget' + onChange={this.handleBudgetChange} />
); @@ -25,6 +49,7 @@ class CategoryForm extends React.Component { CategoryForm.propTypes = { buttonText: PropTypes.string, + onComplete: PropTypes.func }; export default CategoryForm; \ No newline at end of file diff --git a/lab-nathan/src/components/dashboard/dashboard.js b/lab-nathan/src/components/dashboard/dashboard.js index e3ff083..c11fa7b 100644 --- a/lab-nathan/src/components/dashboard/dashboard.js +++ b/lab-nathan/src/components/dashboard/dashboard.js @@ -1,5 +1,14 @@ import React from 'react'; import { connect } from 'react-redux'; +import PropTypes from 'prop-types'; +import CategoryItem from '../category-item/category-item.js'; + +import { + categoryCreate, + categoryUpdate, + categoryDelete +} from '../../actions/category-actions.js'; + import CategoryForm from '../category-form/category-form.js'; class DashboardContainer extends React.Component { @@ -9,10 +18,34 @@ class DashboardContainer extends React.Component {

Dashboard

+ + {this.props.categories.map((item, index) => + + )} ); } } -export default DashboardContainer; \ No newline at end of file +DashboardContainer.propTypes = { + categories: PropTypes.array, + categoryCreate: PropTypes.func, + categoryUpdate: PropTypes.func, + categoryDelete: PropTypes.func, +}; + +const mapStateToProps = (state) => ({ + categories: state +}); + +const mapDispatchToProps = (dispatch) => { + return { + categoryCreate: (category) => dispatch(categoryCreate(category)), + categoryUpdate: (category) => dispatch(categoryUpdate(category)), + categoryDelete: (category) => dispatch(categoryDelete(category)) + } +} + +export default connect(mapStateToProps, mapDispatchToProps)(DashboardContainer); \ No newline at end of file diff --git a/lab-nathan/src/reducers/category.js b/lab-nathan/src/reducers/category.js index 33fbfb3..5b5568b 100644 --- a/lab-nathan/src/reducers/category.js +++ b/lab-nathan/src/reducers/category.js @@ -3,9 +3,9 @@ export default (state = [], action) => { case 'CATEGORY_CREATE': return [...state, action.payload]; case 'CATEGORY_UPDATE': - return state.map(category => category.id === payload.id ? payload : category); + return state.map(category => category.id === action.payload.id ? action.payload : category); case 'CATEGORY_DELETE': - return state.filter(category => category.id !== payload.id); + return state.filter(category => category.id !== action.payload.id); case 'CATEGORY_RESET': return []; default: From 67777d47f069c04945d9bb4f49bf97d157aae349 Mon Sep 17 00:00:00 2001 From: Nathan Harrenstein Date: Mon, 28 Aug 2017 23:43:10 -0700 Subject: [PATCH 08/18] Add update and delete functionality to CategoryItem. --- .../components/category-form/category-form.js | 13 +++++++++++-- .../components/category-item/category-item.js | 16 +++++++++++++++- lab-nathan/src/components/dashboard/dashboard.js | 6 +++++- 3 files changed, 31 insertions(+), 4 deletions(-) diff --git a/lab-nathan/src/components/category-form/category-form.js b/lab-nathan/src/components/category-form/category-form.js index e42d66a..4394707 100644 --- a/lab-nathan/src/components/category-form/category-form.js +++ b/lab-nathan/src/components/category-form/category-form.js @@ -25,7 +25,15 @@ class CategoryForm extends React.Component { handleSubmit(e) { e.preventDefault(); - this.props.onComplete(Object.assign({}, this.state)); + + let category = Object.assign({}, this.state); + + if (this.props.category) { + category.id = this.props.category.id; + category.timestamp = this.props.category.timestamp; + } + + this.props.onComplete(category); } render() { @@ -49,7 +57,8 @@ class CategoryForm extends React.Component { CategoryForm.propTypes = { buttonText: PropTypes.string, - onComplete: PropTypes.func + onComplete: PropTypes.func, + category: PropTypes.object }; export default CategoryForm; \ No newline at end of file diff --git a/lab-nathan/src/components/category-item/category-item.js b/lab-nathan/src/components/category-item/category-item.js index e270a3c..352e0ee 100644 --- a/lab-nathan/src/components/category-item/category-item.js +++ b/lab-nathan/src/components/category-item/category-item.js @@ -1,9 +1,17 @@ import React from 'react'; import PropTypes from 'prop-types'; +import CategoryForm from '../category-form/category-form.js'; class CategoryItem extends React.Component { constructor(props) { super(props); + + this.handleSubmit = this.handleSubmit.bind(this); + } + + handleSubmit(e) { + e.preventDefault(); + this.props.categoryDelete(this.props.category); } render() { @@ -11,7 +19,11 @@ class CategoryItem extends React.Component {

{this.props.category.name}

{this.props.category.budget}

- + +
); } @@ -19,6 +31,8 @@ class CategoryItem extends React.Component { CategoryItem.propTypes = { category: PropTypes.object, + categoryUpdate: PropTypes.func, + categoryDelete: PropTypes.func, }; export default CategoryItem; diff --git a/lab-nathan/src/components/dashboard/dashboard.js b/lab-nathan/src/components/dashboard/dashboard.js index c11fa7b..6c5d552 100644 --- a/lab-nathan/src/components/dashboard/dashboard.js +++ b/lab-nathan/src/components/dashboard/dashboard.js @@ -22,7 +22,11 @@ class DashboardContainer extends React.Component { /> {this.props.categories.map((item, index) => - + )} ); From 1ad734087e3c989717c4c91f22b26d30769d736c Mon Sep 17 00:00:00 2001 From: Nathan Harrenstein Date: Tue, 29 Aug 2017 14:34:18 -0700 Subject: [PATCH 09/18] Add expense reducer. --- lab-nathan/src/reducers/category-reducer.js | 16 ++++++++++++ lab-nathan/src/reducers/category.js | 14 ---------- lab-nathan/src/reducers/expense-reducer.js | 29 +++++++++++++++++++++ lab-nathan/src/reducers/reducers.js | 8 ++++++ 4 files changed, 53 insertions(+), 14 deletions(-) create mode 100644 lab-nathan/src/reducers/category-reducer.js delete mode 100644 lab-nathan/src/reducers/category.js create mode 100644 lab-nathan/src/reducers/expense-reducer.js create mode 100644 lab-nathan/src/reducers/reducers.js diff --git a/lab-nathan/src/reducers/category-reducer.js b/lab-nathan/src/reducers/category-reducer.js new file mode 100644 index 0000000..db1d2b1 --- /dev/null +++ b/lab-nathan/src/reducers/category-reducer.js @@ -0,0 +1,16 @@ +const categoryReducer = function(categories = [], action) { + switch (action.type) { + case 'CATEGORY_CREATE': + return [...categories, action.payload]; + case 'CATEGORY_UPDATE': + return categories.map(category => category.id === action.payload.id ? action.payload : category); + case 'CATEGORY_DELETE': + return categories.filter(category => category.id !== action.payload.id); + case 'CATEGORY_RESET': + return []; + default: + return categories; + } +} + +export default categoryReducer; \ No newline at end of file diff --git a/lab-nathan/src/reducers/category.js b/lab-nathan/src/reducers/category.js deleted file mode 100644 index 5b5568b..0000000 --- a/lab-nathan/src/reducers/category.js +++ /dev/null @@ -1,14 +0,0 @@ -export default (state = [], action) => { - switch (action.type) { - case 'CATEGORY_CREATE': - return [...state, action.payload]; - case 'CATEGORY_UPDATE': - return state.map(category => category.id === action.payload.id ? action.payload : category); - case 'CATEGORY_DELETE': - return state.filter(category => category.id !== action.payload.id); - case 'CATEGORY_RESET': - return []; - default: - return state; - } -}; \ No newline at end of file diff --git a/lab-nathan/src/reducers/expense-reducer.js b/lab-nathan/src/reducers/expense-reducer.js new file mode 100644 index 0000000..d5bf8a0 --- /dev/null +++ b/lab-nathan/src/reducers/expense-reducer.js @@ -0,0 +1,29 @@ +const expenseReducer = function(expenses = {}, action) { + let {type, payload} = action; + + switch (type) { + case 'CATEGORY_CREATE': + return {...expenses, [payload.id]: []}; + case 'CATEGORY_DELETE': + return {...expenses, [payload.id]: undefined}; + case 'EXPENSE_CREATE': { + let { categoryId } = payload; + let categoryExpenses = expenses[categoryId]; + return {...expenses, [categoryId]: [...categoryExpenses, payload] }; + } + case 'EXPENSE_UPDATE': { + let { categoryId } = payload; + let categoryExpenses = expenses[categoryId]; + return {...expenses, [categoryId]: categoryExpenses.map(expense => expense.id === payload.id ? payload : expense) }; + } + case 'EXPENSE_DELETE': { + let { categoryId } = payload; + let categoryExpenses = expenses[categoryId]; + return {...expenses, [categoryId]: categoryExpenses.filter(expense => expense.id !== payload.id) }; + } + default: + return expenses; + } +} + +export default expenseReducer; \ No newline at end of file diff --git a/lab-nathan/src/reducers/reducers.js b/lab-nathan/src/reducers/reducers.js new file mode 100644 index 0000000..2307a40 --- /dev/null +++ b/lab-nathan/src/reducers/reducers.js @@ -0,0 +1,8 @@ +import { combineReducers } from 'redux'; +import categoryReducer from './category-reducer.js'; +import expenseReducer from './expense-reducer.js'; + +export default combineReducers({ + categories: categoryReducer, + expenses: expenseReducer +}); \ No newline at end of file From f1c1ae6dce357a33d3226f66869e64b1d7097eff Mon Sep 17 00:00:00 2001 From: Nathan Harrenstein Date: Tue, 29 Aug 2017 14:52:51 -0700 Subject: [PATCH 10/18] Add expense actions. --- lab-nathan/src/actions/expense-actions.js | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 lab-nathan/src/actions/expense-actions.js diff --git a/lab-nathan/src/actions/expense-actions.js b/lab-nathan/src/actions/expense-actions.js new file mode 100644 index 0000000..6582567 --- /dev/null +++ b/lab-nathan/src/actions/expense-actions.js @@ -0,0 +1,20 @@ +import uuid from 'uuid/v1'; + +export const expenseCreate = (expense) => { + expense.id = uuid(); + expense.timestamp = new Date(); + return { + type: 'EXPENSE_CREATE', + payload: expense + } +}; + +export const expenseUpdate = (expense) => ({ + type: 'EXPENSE_UPDATE', + payload: expense +}); + +export const expenseDelete = (expense) => ({ + type: 'EXPENSE_DELETE', + payload: expense +}); From d2d61fa5d8e83ee1292ccf2749716e0b71ae9f8a Mon Sep 17 00:00:00 2001 From: Nathan Harrenstein Date: Tue, 29 Aug 2017 18:40:09 -0700 Subject: [PATCH 11/18] Enables creation of expense items. --- lab-nathan/package.json | 1 + lab-nathan/src/components/app/app.js | 4 +- .../components/category-item/category-item.js | 43 +++++++++++- .../src/components/dashboard/dashboard.js | 20 ++---- .../components/expense-form/expense-form.js | 70 +++++++++++++++++++ .../components/expense-item/expense-item.js | 56 +++++++++++++++ 6 files changed, 174 insertions(+), 20 deletions(-) create mode 100644 lab-nathan/src/components/expense-form/expense-form.js create mode 100644 lab-nathan/src/components/expense-item/expense-item.js diff --git a/lab-nathan/package.json b/lab-nathan/package.json index 1f039fe..800d092 100644 --- a/lab-nathan/package.json +++ b/lab-nathan/package.json @@ -22,6 +22,7 @@ "extract-text-webpack-plugin": "^3.0.0", "file-loader": "^0.11.2", "html-webpack-plugin": "^2.30.1", + "jest": "^20.0.4", "node-sass": "^4.5.3", "prop-types": "^15.5.10", "react": "^15.6.1", diff --git a/lab-nathan/src/components/app/app.js b/lab-nathan/src/components/app/app.js index 715d5ec..73a9a95 100644 --- a/lab-nathan/src/components/app/app.js +++ b/lab-nathan/src/components/app/app.js @@ -2,10 +2,10 @@ import React from 'react'; import { BrowserRouter, Route } from 'react-router-dom'; import { createStore } from 'redux'; import { Provider } from 'react-redux'; -import reducer from '../../reducers/category.js'; +import reducers from '../../reducers/reducers.js'; import Dashboard from '../dashboard/dashboard.js'; -const store = createStore(reducer); +const store = createStore(reducers); class App extends React.Component { componentDidMount() { diff --git a/lab-nathan/src/components/category-item/category-item.js b/lab-nathan/src/components/category-item/category-item.js index 352e0ee..777f6ef 100644 --- a/lab-nathan/src/components/category-item/category-item.js +++ b/lab-nathan/src/components/category-item/category-item.js @@ -1,6 +1,18 @@ import React from 'react'; import PropTypes from 'prop-types'; import CategoryForm from '../category-form/category-form.js'; +import ExpenseForm from '../expense-form/expense-form.js'; +import ExpenseItem from '../expense-item/expense-item.js'; +import { connect } from 'react-redux'; + +import { + expenseCreate, +} from '../../actions/expense-actions.js'; + +import { + categoryUpdate, + categoryDelete +} from '../../actions/category-actions.js'; class CategoryItem extends React.Component { constructor(props) { @@ -17,13 +29,24 @@ class CategoryItem extends React.Component { render() { return (
-

{this.props.category.name}

-

{this.props.category.budget}

+

Category: {this.props.category.name}

+

Budget: {this.props.category.budget}

+ + + {this.props.expenses[this.props.category.id].map((expense) => + + )}
); } @@ -33,7 +56,21 @@ CategoryItem.propTypes = { category: PropTypes.object, categoryUpdate: PropTypes.func, categoryDelete: PropTypes.func, + expenseCreate: PropTypes.func, + expenses: PropTypes.object }; -export default CategoryItem; +const mapStateToProps = (state) => ({ + expenses: state.expenses +}); + +const mapDispatchToProps = (dispatch) => { + return { + categoryUpdate: (category) => dispatch(categoryUpdate(category)), + categoryDelete: (category) => dispatch(categoryDelete(category)), + expenseCreate: (category) => dispatch(expenseCreate(category)) + } +} + +export default connect(mapStateToProps, mapDispatchToProps)(CategoryItem); diff --git a/lab-nathan/src/components/dashboard/dashboard.js b/lab-nathan/src/components/dashboard/dashboard.js index 6c5d552..23b61fc 100644 --- a/lab-nathan/src/components/dashboard/dashboard.js +++ b/lab-nathan/src/components/dashboard/dashboard.js @@ -3,11 +3,7 @@ import { connect } from 'react-redux'; import PropTypes from 'prop-types'; import CategoryItem from '../category-item/category-item.js'; -import { - categoryCreate, - categoryUpdate, - categoryDelete -} from '../../actions/category-actions.js'; +import { categoryCreate } from '../../actions/category-actions.js'; import CategoryForm from '../category-form/category-form.js'; @@ -15,7 +11,7 @@ class DashboardContainer extends React.Component { render() { return (
-

Dashboard

+

Dashboard

+ category={item} /> )}
); @@ -35,20 +29,16 @@ class DashboardContainer extends React.Component { DashboardContainer.propTypes = { categories: PropTypes.array, - categoryCreate: PropTypes.func, - categoryUpdate: PropTypes.func, - categoryDelete: PropTypes.func, + categoryCreate: PropTypes.func }; const mapStateToProps = (state) => ({ - categories: state + categories: state.categories }); const mapDispatchToProps = (dispatch) => { return { categoryCreate: (category) => dispatch(categoryCreate(category)), - categoryUpdate: (category) => dispatch(categoryUpdate(category)), - categoryDelete: (category) => dispatch(categoryDelete(category)) } } diff --git a/lab-nathan/src/components/expense-form/expense-form.js b/lab-nathan/src/components/expense-form/expense-form.js new file mode 100644 index 0000000..885a7e7 --- /dev/null +++ b/lab-nathan/src/components/expense-form/expense-form.js @@ -0,0 +1,70 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +class ExpenseForm extends React.Component { + constructor(props) { + super(props); + + this.state = { + name: props.expense ? props.expense.name : '', + price: props.expense ? props.expense.price : '', + categoryId: props.expense ? props.expense.categoryId : '', + }; + + this.handleNameChange = this.handleNameChange.bind(this); + this.handleBudgetChange = this.handleBudgetChange.bind(this); + this.handleSubmit = this.handleSubmit.bind(this); + } + + handleNameChange(e) { + this.setState({ name: e.target.value }); + } + + handleBudgetChange(e) { + this.setState({ price: e.target.value }); + } + + handleSubmit(e) { + e.preventDefault(); + + let expense = Object.assign({}, this.state); + + if (this.props.expense) { + expense.id = this.props.expense.id; + expense.timestamp = this.props.expense.timestamp; + expense.categoryId = this.props.expense.categoryId; + } + else { + expense.categoryId = this.props.categoryId; + } + + this.props.onComplete(expense); + } + + render() { + return ( +
+ + + +
+ ); + } +} + +ExpenseForm.propTypes = { + buttonText: PropTypes.string, + onComplete: PropTypes.func, + expense: PropTypes.object, + categoryId: PropTypes.string +}; + +export default ExpenseForm; \ No newline at end of file diff --git a/lab-nathan/src/components/expense-item/expense-item.js b/lab-nathan/src/components/expense-item/expense-item.js new file mode 100644 index 0000000..c50299f --- /dev/null +++ b/lab-nathan/src/components/expense-item/expense-item.js @@ -0,0 +1,56 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import ExpenseForm from '../expense-form/expense-form.js'; +import { connect } from 'react-redux'; + +import { + expenseUpdate, + expenseDelete +} from '../../actions/expense-actions.js'; + +class ExpenseItem extends React.Component { + constructor(props) { + super(props); + + this.handleSubmit = this.handleSubmit.bind(this); + } + + handleSubmit(e) { + e.preventDefault(); + this.props.expenseDelete(this.props.expense); + } + + render() { + return ( +
+

Name: {this.props.expense.name}

+

Price: {this.props.expense.price}

+ + +
+ ); + } +} + +ExpenseItem.propTypes = { + expense: PropTypes.object, + expenseUpdate: PropTypes.func, + expenseDelete: PropTypes.func +}; + +const mapStateToProps = (state) => ({ + expenses: state.expenses +}); + +const mapDispatchToProps = (dispatch) => { + return { + expenseUpdate: (expense) => dispatch(expenseUpdate(expense)), + expenseDelete: (expense) => dispatch(expenseDelete(expense)) + } +} + +export default connect(mapStateToProps, mapDispatchToProps)(ExpenseItem); + From 129531d65b63e3570f78cc90191d9406521d4a10 Mon Sep 17 00:00:00 2001 From: Nathan Harrenstein Date: Tue, 29 Aug 2017 22:47:10 -0700 Subject: [PATCH 12/18] Create action and reducer tests. --- lab-nathan/.eslintrc.json | 9 +-- lab-nathan/.gitignore | 3 +- lab-nathan/package.json | 13 +++- .../src/__test__/category-actions.test.js | 29 ++++++++ .../src/__test__/category-reducer.test.js | 71 ++++++++++++++++++ .../src/__test__/expense-actions.test.js | 30 ++++++++ .../src/__test__/expense-reducer.test.js | 72 +++++++++++++++++++ lab-nathan/src/actions/category-actions.js | 4 -- lab-nathan/src/reducers/category-reducer.js | 2 - lab-nathan/src/reducers/expense-reducer.js | 7 +- 10 files changed, 226 insertions(+), 14 deletions(-) create mode 100644 lab-nathan/src/__test__/category-actions.test.js create mode 100644 lab-nathan/src/__test__/category-reducer.test.js create mode 100644 lab-nathan/src/__test__/expense-actions.test.js create mode 100644 lab-nathan/src/__test__/expense-reducer.test.js diff --git a/lab-nathan/.eslintrc.json b/lab-nathan/.eslintrc.json index df7b26d..467fe81 100644 --- a/lab-nathan/.eslintrc.json +++ b/lab-nathan/.eslintrc.json @@ -1,9 +1,10 @@ { - "extends": ["eslint:recommended", "plugin:react/recommended"], + "extends": ["eslint:recommended", "plugin:react/recommended", "plugin:jest/recommended"], "parser": "babel-eslint", "env": { "browser": true, - "node": true - } - + "node": true, + "jest": true + }, + "plugins": ["jest"] } \ No newline at end of file diff --git a/lab-nathan/.gitignore b/lab-nathan/.gitignore index 9c19dce..893f389 100644 --- a/lab-nathan/.gitignore +++ b/lab-nathan/.gitignore @@ -1,3 +1,4 @@ node_modules build -.vscode \ No newline at end of file +.vscode +coverage \ No newline at end of file diff --git a/lab-nathan/package.json b/lab-nathan/package.json index 800d092..a6831d4 100644 --- a/lab-nathan/package.json +++ b/lab-nathan/package.json @@ -5,7 +5,17 @@ "main": "index.js", "scripts": { "build": "webpack", - "watch": "webpack-dev-server --inline --hot" + "watch": "webpack-dev-server --inline --hot", + "test": "jest --coverage", + "test-watch": "jest --watchAll" + }, + "jest": { + "globals": { + "__DEBUG__": false, + "process.env": { + "NODE_ENV": "testing" + } + } }, "keywords": [], "author": "", @@ -42,6 +52,7 @@ "devDependencies": { "babel-eslint": "^7.2.3", "eslint": "^4.5.0", + "eslint-plugin-jest": "^20.0.3", "eslint-plugin-react": "^7.3.0" } } diff --git a/lab-nathan/src/__test__/category-actions.test.js b/lab-nathan/src/__test__/category-actions.test.js new file mode 100644 index 0000000..45d5518 --- /dev/null +++ b/lab-nathan/src/__test__/category-actions.test.js @@ -0,0 +1,29 @@ +import { categoryCreate, categoryUpdate, categoryDelete } from '../actions/category-actions.js'; + +describe('Category Actions', () => { + test('categoryCreate returns a CATEGORY_CREATE action', () => { + let action = categoryCreate({ name: 'test title' }); + expect(action.type).toEqual('CATEGORY_CREATE'); + expect(action.payload.id).toBeTruthy(); + expect(action.payload.timestamp).toBeTruthy(); + expect(action.payload.name).toBe('test title'); + }); + + test('categoryDelete returns a CATEGORY_DELETE action', () => { + let category = { id: '01234', timestamp: new Date(), title: 'test title' }; + let action = categoryDelete(category); + expect(action).toEqual({ + type: 'CATEGORY_DELETE', + payload: category + }); + }); + + test('categoryUpdate returns a CATEGORY_UPDATE action', () => { + let category = { id: '01234', timestamp: new Date(), title: 'test title' }; + let action = categoryUpdate(category); + expect(action).toEqual({ + type: 'CATEGORY_UPDATE', + payload: category + }); + }); +}); \ No newline at end of file diff --git a/lab-nathan/src/__test__/category-reducer.test.js b/lab-nathan/src/__test__/category-reducer.test.js new file mode 100644 index 0000000..a48d535 --- /dev/null +++ b/lab-nathan/src/__test__/category-reducer.test.js @@ -0,0 +1,71 @@ +import categoryReducer from '../reducers/category-reducer.js'; + +describe('Category Reducer', () => { + test('initialState should be an empty array', () => { + let result = categoryReducer(undefined, { type: null }); + expect(result).toEqual([]); + }); + + test('if no action type is presented, the state should be returned', () => { + let state = [ + { id: 'someid', title: 'some title', }, + { id: 'anotherid', title: 'another title' } + ]; + let result = categoryReducer(state, { type: null }); + expect(result).toEqual(state); + }); + + test('CATEGORY_CREATE should append a category to the categories array', () => { + let action = { + type: 'CATEGORY_CREATE', + payload: 'sample payload' + }; + + let result = categoryReducer([], action); + expect(result.length).toBe(1); + expect(result[0]).toBe(action.payload); + }); + + test('CATEGORY_UPDATE should update a category', () => { + let createAction = { + type: 'CATEGORY_CREATE', + payload: { name: 'sample payload', id: "1" } + }; + + let createResult = categoryReducer([], createAction); + + let createAction2 = { + type: 'CATEGORY_CREATE', + payload: { name: 'sample payload 2', id: "2" } + }; + + let createResult2 = categoryReducer(createResult, createAction2); + + let updateAction = { + type: 'CATEGORY_UPDATE', + payload: { name: 'updated payload', id: "1" } + }; + + let updateResult = categoryReducer(createResult2, updateAction); + + expect(updateResult[0]).toBe(updateAction.payload); + }); + + test('CATEGORY_DELETE should delete a category', () => { + let createAction = { + type: 'CATEGORY_CREATE', + payload: { name: 'sample payload', id: "1" } + }; + + let createResult = categoryReducer([], createAction); + + let deleteAction = { + type: 'CATEGORY_DELETE', + payload: { name: 'sample payload', id: "1" } + }; + + let deleteResult = categoryReducer(createResult, deleteAction); + + expect(deleteResult.length).toBe(0); + }); +}); \ No newline at end of file diff --git a/lab-nathan/src/__test__/expense-actions.test.js b/lab-nathan/src/__test__/expense-actions.test.js new file mode 100644 index 0000000..be724ac --- /dev/null +++ b/lab-nathan/src/__test__/expense-actions.test.js @@ -0,0 +1,30 @@ +import { expenseCreate, expenseUpdate, expenseDelete } from '../actions/expense-actions.js'; + +describe('Expense Actions', () => { + test('expenseCreate returns a EXPENSE_CREATE action', () => { + let {payload, type} = expenseCreate({ name: 'test name', budget: "1342" }); + expect(type).toEqual('EXPENSE_CREATE'); + expect(payload.id).toBeTruthy(); + expect(payload.timestamp).toBeTruthy(); + expect(payload.name).toBe('test name'); + expect(payload.budget).toBe("1342"); + }); + + test('expenseDelete returns a EXPENSE_DELETE action', () => { + let expense = { id: '01234', timestamp: new Date(), name: 'test name', budget: "1" }; + let action = expenseDelete(expense); + expect(action).toEqual({ + type: 'EXPENSE_DELETE', + payload: expense + }); + }); + + test('expenseUpdate returns a EXPENSE_UPDATE action', () => { + let expense = { id: '01234', timestamp: new Date(), name: 'test name', budget: "1" }; + let action = expenseUpdate(expense); + expect(action).toEqual({ + type: 'EXPENSE_UPDATE', + payload: expense + }); + }); +}); \ No newline at end of file diff --git a/lab-nathan/src/__test__/expense-reducer.test.js b/lab-nathan/src/__test__/expense-reducer.test.js new file mode 100644 index 0000000..5e3b236 --- /dev/null +++ b/lab-nathan/src/__test__/expense-reducer.test.js @@ -0,0 +1,72 @@ +import expenseReducer from '../reducers/expense-reducer.js'; + +describe('Expense Reducer', () => { + test('initialState should be an empty object', () => { + let result = expenseReducer(undefined, { type: null }); + expect(result).toEqual({}); + }); + + test('if no action type is presented, the state should be returned', () => { + let state = { + 0: [{ id: 'someid', title: 'some title', }], + 1: [{ id: 'anotherid', title: 'another title' }] + }; + let result = expenseReducer(state, { type: null }); + expect(result).toEqual(state); + }); + + test('CATEGORY_CREATE should create an empty array at the supplied category id', () => { + let action = { + type: 'CATEGORY_CREATE', + payload: { name: 'sample payload', id: "1" } + }; + + let result = expenseReducer({}, action); + expect(result[1]).toEqual([]); + }); + + test('CATEGORY_DELETE should delete the array with the supplied category id', () => { + let action = { + type: 'CATEGORY_DELETE', + payload: { name: 'sample payload', id: "1" } + }; + + let result = expenseReducer({ 1: [ { id: 'someid', categoryId: '1', title: 'another title' } ] }, action); + expect(result).toEqual({}); + }); + + test('EXPENSE_CREATE should append a expense to the categories array', () => { + let action = { + type: 'EXPENSE_CREATE', + payload: { id: 'someid', categoryId: '1', title: 'another title' } + }; + + let result = expenseReducer({ 1: [] }, action); + expect(result[1].length).toBe(1); + expect(result[1][0]).toBe(action.payload); + }); + + test('EXPENSE_UPDATE should update a expense', () => { + let action = { + type: 'EXPENSE_UPDATE', + payload: { id: 'someid', categoryId: '1', title: 'updated title' } + }; + + let result = expenseReducer({ + 1: [ { id: 'someid', categoryId: '1', title: 'another title' }, { id: 'someid2', categoryId: '1', title: 'another title2' } ], + 2: [ { id: 'someid', categoryId: '2', title: 'another title' }, { id: 'someid2', categoryId: '2', title: 'another title2' } ] + }, action); + + expect(result[1][0]).toBe(action.payload); + }); + + test('EXPENSE_DELETE should delete a expense', () => { + let action = { + type: 'EXPENSE_DELETE', + payload: { id: 'someid', categoryId: '1', title: 'updated title' } + }; + + let result = expenseReducer({ 1: [ { id: 'someid', categoryId: '1', title: 'another title' } ] }, action); + expect(result[1].length).toBe(0); + }); +}); \ No newline at end of file diff --git a/lab-nathan/src/actions/category-actions.js b/lab-nathan/src/actions/category-actions.js index fed0ad6..ec0aff0 100644 --- a/lab-nathan/src/actions/category-actions.js +++ b/lab-nathan/src/actions/category-actions.js @@ -18,7 +18,3 @@ export const categoryDelete = (category) => ({ type: 'CATEGORY_DELETE', payload: category }); - -export const categoryReset = () => ({ - type: 'CATEGORY_RESET' -}); diff --git a/lab-nathan/src/reducers/category-reducer.js b/lab-nathan/src/reducers/category-reducer.js index db1d2b1..f321fb2 100644 --- a/lab-nathan/src/reducers/category-reducer.js +++ b/lab-nathan/src/reducers/category-reducer.js @@ -6,8 +6,6 @@ const categoryReducer = function(categories = [], action) { return categories.map(category => category.id === action.payload.id ? action.payload : category); case 'CATEGORY_DELETE': return categories.filter(category => category.id !== action.payload.id); - case 'CATEGORY_RESET': - return []; default: return categories; } diff --git a/lab-nathan/src/reducers/expense-reducer.js b/lab-nathan/src/reducers/expense-reducer.js index d5bf8a0..0fa870a 100644 --- a/lab-nathan/src/reducers/expense-reducer.js +++ b/lab-nathan/src/reducers/expense-reducer.js @@ -4,8 +4,11 @@ const expenseReducer = function(expenses = {}, action) { switch (type) { case 'CATEGORY_CREATE': return {...expenses, [payload.id]: []}; - case 'CATEGORY_DELETE': - return {...expenses, [payload.id]: undefined}; + case 'CATEGORY_DELETE': { + let newExpenses = {...expenses}; + delete newExpenses[payload.id]; + return newExpenses; + } case 'EXPENSE_CREATE': { let { categoryId } = payload; let categoryExpenses = expenses[categoryId]; From cae8331bb050b377ffea86878abb2242088aacf9 Mon Sep 17 00:00:00 2001 From: Nathan Harrenstein Date: Wed, 30 Aug 2017 23:42:44 -0700 Subject: [PATCH 13/18] Add logging and validation. --- lab-nathan/src/components/app/app.js | 5 ++-- .../components/category-form/category-form.js | 4 ++-- .../components/category-item/category-item.js | 4 +++- .../components/expense-form/expense-form.js | 10 ++++---- lab-nathan/src/lib/redux-reporter.js | 16 +++++++++++++ lab-nathan/src/lib/validators.js | 11 +++++++++ lab-nathan/src/reducers/category-reducer.js | 24 +++++++++++++------ lab-nathan/src/reducers/expense-reducer.js | 16 +++++++++---- 8 files changed, 69 insertions(+), 21 deletions(-) create mode 100644 lab-nathan/src/lib/redux-reporter.js create mode 100644 lab-nathan/src/lib/validators.js diff --git a/lab-nathan/src/components/app/app.js b/lab-nathan/src/components/app/app.js index 73a9a95..9f80ae6 100644 --- a/lab-nathan/src/components/app/app.js +++ b/lab-nathan/src/components/app/app.js @@ -1,11 +1,12 @@ import React from 'react'; import { BrowserRouter, Route } from 'react-router-dom'; -import { createStore } from 'redux'; +import { createStore, applyMiddleware } from 'redux'; import { Provider } from 'react-redux'; +import reduxReporter from '../../lib/redux-reporter.js'; import reducers from '../../reducers/reducers.js'; import Dashboard from '../dashboard/dashboard.js'; -const store = createStore(reducers); +const store = createStore(reducers, applyMiddleware(reduxReporter)); class App extends React.Component { componentDidMount() { diff --git a/lab-nathan/src/components/category-form/category-form.js b/lab-nathan/src/components/category-form/category-form.js index 4394707..2ab7799 100644 --- a/lab-nathan/src/components/category-form/category-form.js +++ b/lab-nathan/src/components/category-form/category-form.js @@ -7,7 +7,7 @@ class CategoryForm extends React.Component { this.state = { name: props.category ? props.category.name : '', - budget: props.category ? props.category.budget : '' + budget: props.category ? props.category.budget : 0 }; this.handleNameChange = this.handleNameChange.bind(this); @@ -20,7 +20,7 @@ class CategoryForm extends React.Component { } handleBudgetChange(e) { - this.setState({ budget: e.target.value }); + this.setState({ budget: Number(e.target.value) }); } handleSubmit(e) { diff --git a/lab-nathan/src/components/category-item/category-item.js b/lab-nathan/src/components/category-item/category-item.js index 777f6ef..bca32f4 100644 --- a/lab-nathan/src/components/category-item/category-item.js +++ b/lab-nathan/src/components/category-item/category-item.js @@ -27,10 +27,12 @@ class CategoryItem extends React.Component { } render() { + let categoryExpenses = this.props.expenses[this.props.category.id]; + let amountSpent = categoryExpenses.length > 0 ? categoryExpenses.reduce((acc, cur) => acc += cur.price, 0) : 0; return (

Category: {this.props.category.name}

-

Budget: {this.props.category.budget}

+

Budget: {this.props.category.budget - amountSpent}

+ onChange={this.handlePriceChange} /> ); diff --git a/lab-nathan/src/lib/redux-reporter.js b/lab-nathan/src/lib/redux-reporter.js new file mode 100644 index 0000000..ed8b647 --- /dev/null +++ b/lab-nathan/src/lib/redux-reporter.js @@ -0,0 +1,16 @@ +let reporter = store => next => action => { + console.log('__ACTION__', action); + + try { + let result = next(action); + console.log('__STATE__', store.getState()); + return result; + } + catch (error) { + error.action = action; + console.error('__ERROR__', error); + return error; + } +} + +export default reporter; \ No newline at end of file diff --git a/lab-nathan/src/lib/validators.js b/lab-nathan/src/lib/validators.js new file mode 100644 index 0000000..8f86349 --- /dev/null +++ b/lab-nathan/src/lib/validators.js @@ -0,0 +1,11 @@ +export const validateCategory = (category) => { + if (!category.id || !category.name || !category.budget || !category.timestamp) { + throw new Error('VALIDATION ERROR: category must include id, name, budget, and timestamp.'); + } +} + +export const validateExpense = (expense) => { + if (!expense.id || !expense.timestamp || !expense.price || !expense.categoryId) { + throw new Error('VALIDATION ERROR: expense must include id, timestamp, price, and category id'); + } +} \ No newline at end of file diff --git a/lab-nathan/src/reducers/category-reducer.js b/lab-nathan/src/reducers/category-reducer.js index f321fb2..65aa541 100644 --- a/lab-nathan/src/reducers/category-reducer.js +++ b/lab-nathan/src/reducers/category-reducer.js @@ -1,11 +1,21 @@ +import { validateCategory } from '../lib/validators.js'; + const categoryReducer = function(categories = [], action) { - switch (action.type) { - case 'CATEGORY_CREATE': - return [...categories, action.payload]; - case 'CATEGORY_UPDATE': - return categories.map(category => category.id === action.payload.id ? action.payload : category); - case 'CATEGORY_DELETE': - return categories.filter(category => category.id !== action.payload.id); + let { type, payload } = action; + + switch (type) { + case 'CATEGORY_CREATE': { + validateCategory(payload); + return [...categories, payload]; + } + case 'CATEGORY_UPDATE': { + validateCategory(payload); + return categories.map(category => category.id === payload.id ? payload : category); + } + case 'CATEGORY_DELETE': { + validateCategory(payload); + return categories.filter(category => category.id !== payload.id); + } default: return categories; } diff --git a/lab-nathan/src/reducers/expense-reducer.js b/lab-nathan/src/reducers/expense-reducer.js index 0fa870a..55bfffb 100644 --- a/lab-nathan/src/reducers/expense-reducer.js +++ b/lab-nathan/src/reducers/expense-reducer.js @@ -1,25 +1,33 @@ +import { validateCategory, validateExpense } from '../lib/validators.js'; + const expenseReducer = function(expenses = {}, action) { - let {type, payload} = action; + let { type, payload } = action; switch (type) { - case 'CATEGORY_CREATE': + case 'CATEGORY_CREATE': { + validateCategory(action.payload); return {...expenses, [payload.id]: []}; + } case 'CATEGORY_DELETE': { - let newExpenses = {...expenses}; + validateCategory(action.payload); + let newExpenses = { ...expenses }; delete newExpenses[payload.id]; return newExpenses; } case 'EXPENSE_CREATE': { + validateExpense(action.payload); let { categoryId } = payload; let categoryExpenses = expenses[categoryId]; - return {...expenses, [categoryId]: [...categoryExpenses, payload] }; + return { ...expenses, [categoryId]: [...categoryExpenses, payload] }; } case 'EXPENSE_UPDATE': { + validateExpense(action.payload); let { categoryId } = payload; let categoryExpenses = expenses[categoryId]; return {...expenses, [categoryId]: categoryExpenses.map(expense => expense.id === payload.id ? payload : expense) }; } case 'EXPENSE_DELETE': { + validateExpense(action.payload); let { categoryId } = payload; let categoryExpenses = expenses[categoryId]; return {...expenses, [categoryId]: categoryExpenses.filter(expense => expense.id !== payload.id) }; From 3f99e591141a8951cfd0f1395a9be5177932326f Mon Sep 17 00:00:00 2001 From: Nathan Harrenstein Date: Wed, 30 Aug 2017 23:53:20 -0700 Subject: [PATCH 14/18] Fix tests. --- .../src/__test__/category-reducer.test.js | 16 +++++++------- .../src/__test__/expense-actions.test.js | 8 +++---- .../src/__test__/expense-reducer.test.js | 22 +++++++++---------- 3 files changed, 23 insertions(+), 23 deletions(-) diff --git a/lab-nathan/src/__test__/category-reducer.test.js b/lab-nathan/src/__test__/category-reducer.test.js index a48d535..2fc0b74 100644 --- a/lab-nathan/src/__test__/category-reducer.test.js +++ b/lab-nathan/src/__test__/category-reducer.test.js @@ -8,8 +8,8 @@ describe('Category Reducer', () => { test('if no action type is presented, the state should be returned', () => { let state = [ - { id: 'someid', title: 'some title', }, - { id: 'anotherid', title: 'another title' } + { id: 'someid', title: 'some title', budget: 234, timestamp: Date.now() }, + { id: 'anotherid', title: 'another title', budget: 234, timestamp: Date.now() } ]; let result = categoryReducer(state, { type: null }); expect(result).toEqual(state); @@ -18,7 +18,7 @@ describe('Category Reducer', () => { test('CATEGORY_CREATE should append a category to the categories array', () => { let action = { type: 'CATEGORY_CREATE', - payload: 'sample payload' + payload: { name: 'sample payload', id: "1", budget: 30, timestamp: Date.now() } }; let result = categoryReducer([], action); @@ -29,21 +29,21 @@ describe('Category Reducer', () => { test('CATEGORY_UPDATE should update a category', () => { let createAction = { type: 'CATEGORY_CREATE', - payload: { name: 'sample payload', id: "1" } + payload: { name: 'sample payload', id: "1", budget: 30, timestamp: Date.now() } }; let createResult = categoryReducer([], createAction); let createAction2 = { type: 'CATEGORY_CREATE', - payload: { name: 'sample payload 2', id: "2" } + payload: { name: 'sample payload 2', id: "2", budget: 30, timestamp: Date.now() } }; let createResult2 = categoryReducer(createResult, createAction2); let updateAction = { type: 'CATEGORY_UPDATE', - payload: { name: 'updated payload', id: "1" } + payload: { name: 'updated payload', id: "1", budget: 30, timestamp: Date.now() } }; let updateResult = categoryReducer(createResult2, updateAction); @@ -54,14 +54,14 @@ describe('Category Reducer', () => { test('CATEGORY_DELETE should delete a category', () => { let createAction = { type: 'CATEGORY_CREATE', - payload: { name: 'sample payload', id: "1" } + payload: { name: 'sample payload', id: "1", budget: 30, timestamp: Date.now() } }; let createResult = categoryReducer([], createAction); let deleteAction = { type: 'CATEGORY_DELETE', - payload: { name: 'sample payload', id: "1" } + payload: { name: 'sample payload', id: "1", budget: 234, timestamp: Date.now() } }; let deleteResult = categoryReducer(createResult, deleteAction); diff --git a/lab-nathan/src/__test__/expense-actions.test.js b/lab-nathan/src/__test__/expense-actions.test.js index be724ac..44c4550 100644 --- a/lab-nathan/src/__test__/expense-actions.test.js +++ b/lab-nathan/src/__test__/expense-actions.test.js @@ -2,16 +2,16 @@ import { expenseCreate, expenseUpdate, expenseDelete } from '../actions/expense- describe('Expense Actions', () => { test('expenseCreate returns a EXPENSE_CREATE action', () => { - let {payload, type} = expenseCreate({ name: 'test name', budget: "1342" }); + let {payload, type} = expenseCreate({ name: 'test name', budget: 1342 }); expect(type).toEqual('EXPENSE_CREATE'); expect(payload.id).toBeTruthy(); expect(payload.timestamp).toBeTruthy(); expect(payload.name).toBe('test name'); - expect(payload.budget).toBe("1342"); + expect(payload.budget).toBe(1342); }); test('expenseDelete returns a EXPENSE_DELETE action', () => { - let expense = { id: '01234', timestamp: new Date(), name: 'test name', budget: "1" }; + let expense = { id: '01234', timestamp: new Date(), name: 'test name', budget: 1 }; let action = expenseDelete(expense); expect(action).toEqual({ type: 'EXPENSE_DELETE', @@ -20,7 +20,7 @@ describe('Expense Actions', () => { }); test('expenseUpdate returns a EXPENSE_UPDATE action', () => { - let expense = { id: '01234', timestamp: new Date(), name: 'test name', budget: "1" }; + let expense = { id: '01234', timestamp: new Date(), name: 'test name', budget: 1 }; let action = expenseUpdate(expense); expect(action).toEqual({ type: 'EXPENSE_UPDATE', diff --git a/lab-nathan/src/__test__/expense-reducer.test.js b/lab-nathan/src/__test__/expense-reducer.test.js index 5e3b236..6ba5f98 100644 --- a/lab-nathan/src/__test__/expense-reducer.test.js +++ b/lab-nathan/src/__test__/expense-reducer.test.js @@ -8,8 +8,8 @@ describe('Expense Reducer', () => { test('if no action type is presented, the state should be returned', () => { let state = { - 0: [{ id: 'someid', title: 'some title', }], - 1: [{ id: 'anotherid', title: 'another title' }] + 0: [{ id: 'someid', title: 'some title', price: 2, categoryId: "2" }], + 1: [{ id: 'anotherid', title: 'another title', price: 2, categoryId: "2" }] }; let result = expenseReducer(state, { type: null }); expect(result).toEqual(state); @@ -18,7 +18,7 @@ describe('Expense Reducer', () => { test('CATEGORY_CREATE should create an empty array at the supplied category id', () => { let action = { type: 'CATEGORY_CREATE', - payload: { name: 'sample payload', id: "1" } + payload: { name: 'sample payload', id: "1", budget: 30, timestamp: Date.now() } }; let result = expenseReducer({}, action); @@ -28,17 +28,17 @@ describe('Expense Reducer', () => { test('CATEGORY_DELETE should delete the array with the supplied category id', () => { let action = { type: 'CATEGORY_DELETE', - payload: { name: 'sample payload', id: "1" } + payload: { name: 'sample payload', id: "1", budget: 30, timestamp: Date.now() } }; - let result = expenseReducer({ 1: [ { id: 'someid', categoryId: '1', title: 'another title' } ] }, action); + let result = expenseReducer({ 1: [ { id: 'someid', categoryId: '1', title: 'another title', price: 3 } ] }, action); expect(result).toEqual({}); }); test('EXPENSE_CREATE should append a expense to the categories array', () => { let action = { type: 'EXPENSE_CREATE', - payload: { id: 'someid', categoryId: '1', title: 'another title' } + payload: { id: 'someid', categoryId: '1', title: 'another title', price: 34, timestamp: Date.now() } }; let result = expenseReducer({ 1: [] }, action); @@ -49,12 +49,12 @@ describe('Expense Reducer', () => { test('EXPENSE_UPDATE should update a expense', () => { let action = { type: 'EXPENSE_UPDATE', - payload: { id: 'someid', categoryId: '1', title: 'updated title' } + payload: { id: 'someid', categoryId: '1', title: 'updated title', price: 342, timestamp: Date.now() } }; let result = expenseReducer({ - 1: [ { id: 'someid', categoryId: '1', title: 'another title' }, { id: 'someid2', categoryId: '1', title: 'another title2' } ], - 2: [ { id: 'someid', categoryId: '2', title: 'another title' }, { id: 'someid2', categoryId: '2', title: 'another title2' } ] + 1: [ { id: 'someid', categoryId: '1', title: 'another title', price: 342, timestamp: Date.now() }, { id: 'someid2', categoryId: '1', title: 'another title2', price: 342, timestamp: Date.now() } ], + 2: [ { id: 'someid', categoryId: '2', title: 'another title', price: 342, timestamp: Date.now() }, { id: 'someid2', categoryId: '2', title: 'another title2', price: 342, timestamp: Date.now() } ] }, action); expect(result[1][0]).toBe(action.payload); @@ -63,10 +63,10 @@ describe('Expense Reducer', () => { test('EXPENSE_DELETE should delete a expense', () => { let action = { type: 'EXPENSE_DELETE', - payload: { id: 'someid', categoryId: '1', title: 'updated title' } + payload: { id: 'someid', categoryId: '1', title: 'updated title', price: 342, timestamp: Date.now() } }; - let result = expenseReducer({ 1: [ { id: 'someid', categoryId: '1', title: 'another title' } ] }, action); + let result = expenseReducer({ 1: [ { id: 'someid', categoryId: '1', title: 'another title', price: 342, timestamp: Date.now() } ] }, action); expect(result[1].length).toBe(0); }); }); \ No newline at end of file From b8ad408d2e4aaca822fa0bdfe70743bf57c46ba1 Mon Sep 17 00:00:00 2001 From: Nathan Harrenstein Date: Fri, 1 Sep 2017 23:48:11 -0700 Subject: [PATCH 15/18] Restructure and improve style. --- lab-nathan/src/components/{app => }/app.js | 6 +-- .../{category-form => }/category-form.js | 2 +- .../{category-item => }/category-item.js | 35 ++++++++++---- lab-nathan/src/components/category-item.scss | 43 +++++++++++++++++ .../components/{dashboard => }/dashboard.js | 27 +++++------ lab-nathan/src/components/dashboard.scss | 14 ++++++ .../{expense-form => }/expense-form.js | 0 .../{expense-item => }/expense-item.js | 7 +-- lab-nathan/src/components/expense-item.scss | 5 ++ lab-nathan/src/index.html | 8 +++- lab-nathan/src/main.js | 3 +- lab-nathan/src/style/base/_base.scss | 41 ++++++++++++++++ lab-nathan/src/style/base/_reset.scss | 48 +++++++++++++++++++ lab-nathan/src/style/layout/content.scss | 3 ++ lab-nathan/src/style/layout/footer.scss | 9 ++++ lab-nathan/src/style/layout/header.scss | 8 ++++ lab-nathan/src/style/lib/_vars.scss | 21 ++++++++ lab-nathan/src/style/main.scss | 6 +++ 18 files changed, 253 insertions(+), 33 deletions(-) rename lab-nathan/src/components/{app => }/app.js (81%) rename lab-nathan/src/components/{category-form => }/category-form.js (97%) rename lab-nathan/src/components/{category-item => }/category-item.js (64%) create mode 100644 lab-nathan/src/components/category-item.scss rename lab-nathan/src/components/{dashboard => }/dashboard.js (59%) create mode 100644 lab-nathan/src/components/dashboard.scss rename lab-nathan/src/components/{expense-form => }/expense-form.js (100%) rename lab-nathan/src/components/{expense-item => }/expense-item.js (88%) create mode 100644 lab-nathan/src/components/expense-item.scss create mode 100644 lab-nathan/src/style/base/_base.scss create mode 100644 lab-nathan/src/style/base/_reset.scss create mode 100644 lab-nathan/src/style/layout/content.scss create mode 100644 lab-nathan/src/style/layout/footer.scss create mode 100644 lab-nathan/src/style/layout/header.scss create mode 100644 lab-nathan/src/style/lib/_vars.scss create mode 100644 lab-nathan/src/style/main.scss diff --git a/lab-nathan/src/components/app/app.js b/lab-nathan/src/components/app.js similarity index 81% rename from lab-nathan/src/components/app/app.js rename to lab-nathan/src/components/app.js index 9f80ae6..c3ee0ac 100644 --- a/lab-nathan/src/components/app/app.js +++ b/lab-nathan/src/components/app.js @@ -2,9 +2,9 @@ import React from 'react'; import { BrowserRouter, Route } from 'react-router-dom'; import { createStore, applyMiddleware } from 'redux'; import { Provider } from 'react-redux'; -import reduxReporter from '../../lib/redux-reporter.js'; -import reducers from '../../reducers/reducers.js'; -import Dashboard from '../dashboard/dashboard.js'; +import reduxReporter from '../lib/redux-reporter.js'; +import reducers from '../reducers/reducers.js'; +import Dashboard from './dashboard.js'; const store = createStore(reducers, applyMiddleware(reduxReporter)); diff --git a/lab-nathan/src/components/category-form/category-form.js b/lab-nathan/src/components/category-form.js similarity index 97% rename from lab-nathan/src/components/category-form/category-form.js rename to lab-nathan/src/components/category-form.js index 2ab7799..2987e12 100644 --- a/lab-nathan/src/components/category-form/category-form.js +++ b/lab-nathan/src/components/category-form.js @@ -42,7 +42,7 @@ class CategoryForm extends React.Component { 0 ? categoryExpenses.reduce((acc, cur) => acc += cur.price, 0) : 0; + let {category, expenses} = this.props; + let categoryExpenses = expenses[category.id]; + let amountSpent = 0; + + for (let expense of categoryExpenses) { + amountSpent += expense.price; + } + + let overBudget = amountSpent > category.budget; return (
-

Category: {this.props.category.name}

-

Budget: {this.props.category.budget - amountSpent}

- +
+ + {category.name} + ${category.budget - amountSpent} + + +
+ + + -

Dashboard

+
- - {this.props.categories.map((item, index) => - - )} +
+ {this.props.categories.map((item, index) => + + )} +
); } diff --git a/lab-nathan/src/components/dashboard.scss b/lab-nathan/src/components/dashboard.scss new file mode 100644 index 0000000..fffbe49 --- /dev/null +++ b/lab-nathan/src/components/dashboard.scss @@ -0,0 +1,14 @@ +@import '../style/lib/vars'; + +.category-form { + background-color: #eee; + border-bottom: 1px solid #ccc; + padding: $gutter-small $gutter-large; +} + +.category-container { + display: flex; + flex-flow: row; + flex: 1; + overflow-x: auto; +} \ No newline at end of file diff --git a/lab-nathan/src/components/expense-form/expense-form.js b/lab-nathan/src/components/expense-form.js similarity index 100% rename from lab-nathan/src/components/expense-form/expense-form.js rename to lab-nathan/src/components/expense-form.js diff --git a/lab-nathan/src/components/expense-item/expense-item.js b/lab-nathan/src/components/expense-item.js similarity index 88% rename from lab-nathan/src/components/expense-item/expense-item.js rename to lab-nathan/src/components/expense-item.js index c50299f..333b3d3 100644 --- a/lab-nathan/src/components/expense-item/expense-item.js +++ b/lab-nathan/src/components/expense-item.js @@ -1,12 +1,13 @@ +import './expense-item.scss'; import React from 'react'; import PropTypes from 'prop-types'; -import ExpenseForm from '../expense-form/expense-form.js'; +import ExpenseForm from './expense-form.js'; import { connect } from 'react-redux'; import { expenseUpdate, expenseDelete -} from '../../actions/expense-actions.js'; +} from '../actions/expense-actions.js'; class ExpenseItem extends React.Component { constructor(props) { @@ -22,7 +23,7 @@ class ExpenseItem extends React.Component { render() { return ( -
+

Name: {this.props.expense.name}

Price: {this.props.expense.price}

diff --git a/lab-nathan/src/components/expense-item.scss b/lab-nathan/src/components/expense-item.scss new file mode 100644 index 0000000..e4e17d7 --- /dev/null +++ b/lab-nathan/src/components/expense-item.scss @@ -0,0 +1,5 @@ +@import '../style/lib/vars'; + +.expense-item { + background-color: white; +} \ No newline at end of file diff --git a/lab-nathan/src/index.html b/lab-nathan/src/index.html index 709b2d0..0a0d344 100644 --- a/lab-nathan/src/index.html +++ b/lab-nathan/src/index.html @@ -1,9 +1,15 @@ - Budget Tracker + Expenses +
+

Expense Tracker

+
+
+ Public Domain 2017 +
\ No newline at end of file diff --git a/lab-nathan/src/main.js b/lab-nathan/src/main.js index cf45095..29a5a14 100644 --- a/lab-nathan/src/main.js +++ b/lab-nathan/src/main.js @@ -1,5 +1,6 @@ +import './style/main.scss'; import React from 'react'; import ReactDom from 'react-dom'; -import App from './components/app/app.js'; +import App from './components/app.js'; ReactDom.render(, document.getElementById('root')); \ No newline at end of file diff --git a/lab-nathan/src/style/base/_base.scss b/lab-nathan/src/style/base/_base.scss new file mode 100644 index 0000000..2bffb30 --- /dev/null +++ b/lab-nathan/src/style/base/_base.scss @@ -0,0 +1,41 @@ +html, body, section, main { + height: 100%; +} + +body { + background: $white; + font-family: $header-font-family; + color: $black; + display: flex; + flex-flow: column; +} + +h1 { + font-size: $extra-large-font; + font-weight: bold; +} + +button { + background: linear-gradient(to bottom, #fff, #ddd); + border-radius: $border-radius; + border: $input-border; + box-sizing: border-box; + padding: 0 $gutter-small + $gutter-xsmall; + height: 30px; +} + +form { + padding: $gutter-xsmall 0; +} + +input { + margin-right: $gutter-small; + padding: $gutter-xsmall; + border: $input-border; +} + +main { + display: flex; + flex-direction: column; +} + \ No newline at end of file diff --git a/lab-nathan/src/style/base/_reset.scss b/lab-nathan/src/style/base/_reset.scss new file mode 100644 index 0000000..d31a672 --- /dev/null +++ b/lab-nathan/src/style/base/_reset.scss @@ -0,0 +1,48 @@ +/* http://meyerweb.com/eric/tools/css/reset/ + v2.0 | 20110126 + License: none (public domain) +*/ + +html, body, div, span, applet, object, iframe, +h1, h2, h3, h4, h5, h6, p, blockquote, pre, +a, abbr, acronym, address, big, cite, code, +del, dfn, em, img, ins, kbd, q, s, samp, +small, strike, strong, sub, sup, tt, var, +b, u, i, center, +dl, dt, dd, ol, ul, li, +fieldset, form, label, legend, +table, caption, tbody, tfoot, thead, tr, th, td, +article, aside, canvas, details, embed, +figure, figcaption, footer, header, hgroup, +menu, nav, output, ruby, section, summary, +time, mark, audio, video { + margin: 0; + padding: 0; + border: 0; + font-size: 100%; + font: inherit; + vertical-align: baseline; +} +/* HTML5 display-role reset for older browsers */ +article, aside, details, figcaption, figure, +footer, header, hgroup, menu, nav, section { + display: block; +} +body { + line-height: 1; +} +ol, ul { + list-style: none; +} +blockquote, q { + quotes: none; +} +blockquote:before, blockquote:after, +q:before, q:after { + content: ''; + content: none; +} +table { + border-collapse: collapse; + border-spacing: 0; +} \ No newline at end of file diff --git a/lab-nathan/src/style/layout/content.scss b/lab-nathan/src/style/layout/content.scss new file mode 100644 index 0000000..06d6b63 --- /dev/null +++ b/lab-nathan/src/style/layout/content.scss @@ -0,0 +1,3 @@ +#root { + flex: 1; +} \ No newline at end of file diff --git a/lab-nathan/src/style/layout/footer.scss b/lab-nathan/src/style/layout/footer.scss new file mode 100644 index 0000000..e83abb2 --- /dev/null +++ b/lab-nathan/src/style/layout/footer.scss @@ -0,0 +1,9 @@ +footer { + background: $bookend-background; + min-height: $bookend-height / 2; + display: flex; + align-items: center; + padding: 0 $gutter-large; + color: white; + font-size: 0.8rem; +} \ No newline at end of file diff --git a/lab-nathan/src/style/layout/header.scss b/lab-nathan/src/style/layout/header.scss new file mode 100644 index 0000000..fe730b0 --- /dev/null +++ b/lab-nathan/src/style/layout/header.scss @@ -0,0 +1,8 @@ +header { + background: $bookend-background; + min-height: $bookend-height; + display: flex; + align-items: center; + padding: 0 $gutter-large; + color: $white; +} \ No newline at end of file diff --git a/lab-nathan/src/style/lib/_vars.scss b/lab-nathan/src/style/lib/_vars.scss new file mode 100644 index 0000000..5e95cdb --- /dev/null +++ b/lab-nathan/src/style/lib/_vars.scss @@ -0,0 +1,21 @@ +$black: #000; +$white: #fff; + +$primary: #E94F37; +$secondary: hsl(218, 98%, 95%); +$tertiary: #4387fd; + +$extra-large-font: 2rem; +$large-font: 1.25rem; + +$header-font-family: 'Helvetica Neue', sans-serif; +$font-primary: $black; +$font-secondary: $white; +$bookend-height: 75px; +$bookend-background: linear-gradient(0deg, #c00, $primary); +$gutter-large: 26px; +$gutter-small: $gutter-large / 2; +$gutter-xsmall: $gutter-small / 2; +$gutter-xxsmall: $gutter-xsmall / 2; +$border-radius: 0; +$input-border: 1px solid #ccc; \ No newline at end of file diff --git a/lab-nathan/src/style/main.scss b/lab-nathan/src/style/main.scss new file mode 100644 index 0000000..1ed5f1e --- /dev/null +++ b/lab-nathan/src/style/main.scss @@ -0,0 +1,6 @@ +@import './lib/vars'; +@import './base/reset'; +@import './base/base'; +@import './layout/header'; +@import './layout/content'; +@import './layout/footer'; \ No newline at end of file From 165dffb4d7489b593902d819d640eb0193ec6f6a Mon Sep 17 00:00:00 2001 From: Nathan Harrenstein Date: Sat, 2 Sep 2017 00:15:39 -0700 Subject: [PATCH 16/18] Improve style. --- lab-nathan/src/components/category-form.js | 7 +++++-- lab-nathan/src/components/category-form.scss | 0 lab-nathan/src/components/category-item.js | 7 ++++--- lab-nathan/src/components/category-item.scss | 16 +++++++++++++++- lab-nathan/src/components/expense-form.js | 4 +++- 5 files changed, 27 insertions(+), 7 deletions(-) create mode 100644 lab-nathan/src/components/category-form.scss diff --git a/lab-nathan/src/components/category-form.js b/lab-nathan/src/components/category-form.js index 2987e12..60aecb9 100644 --- a/lab-nathan/src/components/category-form.js +++ b/lab-nathan/src/components/category-form.js @@ -1,3 +1,4 @@ +import './category-form.scss'; import React from 'react'; import PropTypes from 'prop-types'; @@ -33,6 +34,8 @@ class CategoryForm extends React.Component { category.timestamp = this.props.category.timestamp; } + e.target.reset(); + this.props.onComplete(category); } @@ -42,12 +45,12 @@ class CategoryForm extends React.Component { diff --git a/lab-nathan/src/components/category-form.scss b/lab-nathan/src/components/category-form.scss new file mode 100644 index 0000000..e69de29 diff --git a/lab-nathan/src/components/category-item.js b/lab-nathan/src/components/category-item.js index a75fac5..807a51d 100644 --- a/lab-nathan/src/components/category-item.js +++ b/lab-nathan/src/components/category-item.js @@ -44,17 +44,18 @@ class CategoryItem extends React.Component { {category.name} ${category.budget - amountSpent} - + +
diff --git a/lab-nathan/src/components/category-item.scss b/lab-nathan/src/components/category-item.scss index 1b0a5b0..831e19e 100644 --- a/lab-nathan/src/components/category-item.scss +++ b/lab-nathan/src/components/category-item.scss @@ -5,6 +5,7 @@ min-width: 20%; & .category-form { + display: none; padding-left: 0; padding-right: 0; background-color: transparent; @@ -13,7 +14,7 @@ & .category-header { display: flex; - align-items: center; + align-items: baseline; border-bottom: 1px dotted $black; padding: $gutter-xsmall 0; } @@ -32,6 +33,19 @@ margin-right: ($gutter-small + $gutter-xsmall) / 2; } } + + & button { + background: transparent; + border: none; + height: initial; + cursor: pointer; + outline: none; + padding: 0 5px; + + &:active { + color: red; + } + } } .positive { diff --git a/lab-nathan/src/components/expense-form.js b/lab-nathan/src/components/expense-form.js index a482067..4cd5378 100644 --- a/lab-nathan/src/components/expense-form.js +++ b/lab-nathan/src/components/expense-form.js @@ -38,6 +38,8 @@ class ExpenseForm extends React.Component { expense.categoryId = this.props.categoryId; } + e.target.reset(); + this.props.onComplete(expense); } @@ -47,7 +49,7 @@ class ExpenseForm extends React.Component { Date: Sun, 3 Sep 2017 23:54:15 -0700 Subject: [PATCH 17/18] Add reordering of category items. --- lab-nathan/src/actions/category-actions.js | 8 +++ lab-nathan/src/components/category-form.js | 3 +- lab-nathan/src/components/category-item.js | 27 +++------ lab-nathan/src/components/category-item.scss | 6 +- lab-nathan/src/components/dashboard.js | 21 +++++-- lab-nathan/src/components/expense-form.js | 4 +- lab-nathan/src/components/expense-item.js | 18 +++--- lab-nathan/src/components/expense-item.scss | 10 +++- lab-nathan/src/components/item-header.js | 42 ++++++++++++++ .../src/components/reorderable-list-item.js | 46 ++++++++++++++++ lab-nathan/src/components/reorderable-list.js | 55 +++++++++++++++++++ lab-nathan/src/reducers/category-reducer.js | 17 ++++++ 12 files changed, 216 insertions(+), 41 deletions(-) create mode 100644 lab-nathan/src/components/item-header.js create mode 100644 lab-nathan/src/components/reorderable-list-item.js create mode 100644 lab-nathan/src/components/reorderable-list.js diff --git a/lab-nathan/src/actions/category-actions.js b/lab-nathan/src/actions/category-actions.js index ec0aff0..4bb5e41 100644 --- a/lab-nathan/src/actions/category-actions.js +++ b/lab-nathan/src/actions/category-actions.js @@ -14,6 +14,14 @@ export const categoryUpdate = (category) => ({ payload: category }); +export const categoryMove = (category, newIndex) => ({ + type: 'CATEGORY_MOVE', + payload: { + category: category, + newIndex: newIndex + } +}); + export const categoryDelete = (category) => ({ type: 'CATEGORY_DELETE', payload: category diff --git a/lab-nathan/src/components/category-form.js b/lab-nathan/src/components/category-form.js index 60aecb9..2fb618b 100644 --- a/lab-nathan/src/components/category-form.js +++ b/lab-nathan/src/components/category-form.js @@ -8,7 +8,8 @@ class CategoryForm extends React.Component { this.state = { name: props.category ? props.category.name : '', - budget: props.category ? props.category.budget : 0 + budget: props.category ? props.category.budget : 0, + dragging: false }; this.handleNameChange = this.handleNameChange.bind(this); diff --git a/lab-nathan/src/components/category-item.js b/lab-nathan/src/components/category-item.js index 807a51d..ba07d98 100644 --- a/lab-nathan/src/components/category-item.js +++ b/lab-nathan/src/components/category-item.js @@ -4,6 +4,7 @@ import PropTypes from 'prop-types'; import CategoryForm from './category-form.js'; import ExpenseForm from './expense-form.js'; import ExpenseItem from './expense-item.js'; +import ItemHeader from './item-header.js'; import { connect } from 'react-redux'; import { @@ -18,13 +19,6 @@ import { class CategoryItem extends React.Component { constructor(props) { super(props); - - this.handleSubmit = this.handleSubmit.bind(this); - } - - handleSubmit(e) { - e.preventDefault(); - this.props.categoryDelete(this.props.category); } render() { @@ -39,17 +33,11 @@ class CategoryItem extends React.Component { let overBudget = amountSpent > category.budget; return (
-
- - {category.name} - ${category.budget - amountSpent} - - - -
- - - + this.props.categoryDelete(category)} itemUpdate={() => this.props.categoryUpdate(category)}> + {category.name} + ${category.budget - amountSpent} + + ({ - expenses: state.expenses + expenses: state.expenses, + categories: state.categories }); const mapDispatchToProps = (dispatch) => { diff --git a/lab-nathan/src/components/category-item.scss b/lab-nathan/src/components/category-item.scss index 831e19e..66dfad6 100644 --- a/lab-nathan/src/components/category-item.scss +++ b/lab-nathan/src/components/category-item.scss @@ -12,7 +12,7 @@ border-bottom: none; } - & .category-header { + & .item-header { display: flex; align-items: baseline; border-bottom: 1px dotted $black; @@ -20,8 +20,8 @@ } } -.category-header { - & .category-header-left { +.item-header { + & .item-header-left { flex: 1; font-size: 1.25rem; display: flex; diff --git a/lab-nathan/src/components/dashboard.js b/lab-nathan/src/components/dashboard.js index ac23845..7ef1c16 100644 --- a/lab-nathan/src/components/dashboard.js +++ b/lab-nathan/src/components/dashboard.js @@ -4,9 +4,20 @@ import { connect } from 'react-redux'; import PropTypes from 'prop-types'; import CategoryItem from './category-item.js'; import CategoryForm from './category-form.js'; -import { categoryCreate } from '../actions/category-actions.js'; +import ReorderableList from './reorderable-list.js'; +import { categoryCreate, categoryMove } from '../actions/category-actions.js'; class DashboardContainer extends React.Component { + constructor(props) { + super(props); + + this.requestReorder = this.requestReorder.bind(this); + } + + requestReorder(fromIndex, toIndex) { + this.props.categoryMove(this.props.categories[fromIndex], toIndex); + } + render() { return (
@@ -14,13 +25,13 @@ class DashboardContainer extends React.Component { buttonText='Add' onComplete={this.props.categoryCreate} /> -
+ {this.props.categories.map((item, index) => )} -
+
); } @@ -28,7 +39,8 @@ class DashboardContainer extends React.Component { DashboardContainer.propTypes = { categories: PropTypes.array, - categoryCreate: PropTypes.func + categoryCreate: PropTypes.func, + categoryMove: PropTypes.func }; const mapStateToProps = (state) => ({ @@ -38,6 +50,7 @@ const mapStateToProps = (state) => ({ const mapDispatchToProps = (dispatch) => { return { categoryCreate: (category) => dispatch(categoryCreate(category)), + categoryMove: (category, newIndex) => dispatch(categoryMove(category, newIndex)), } } diff --git a/lab-nathan/src/components/expense-form.js b/lab-nathan/src/components/expense-form.js index 4cd5378..a4297aa 100644 --- a/lab-nathan/src/components/expense-form.js +++ b/lab-nathan/src/components/expense-form.js @@ -49,12 +49,12 @@ class ExpenseForm extends React.Component { diff --git a/lab-nathan/src/components/expense-item.js b/lab-nathan/src/components/expense-item.js index 333b3d3..0c977ab 100644 --- a/lab-nathan/src/components/expense-item.js +++ b/lab-nathan/src/components/expense-item.js @@ -2,6 +2,7 @@ import './expense-item.scss'; import React from 'react'; import PropTypes from 'prop-types'; import ExpenseForm from './expense-form.js'; +import ItemHeader from './item-header.js'; import { connect } from 'react-redux'; import { @@ -12,23 +13,18 @@ import { class ExpenseItem extends React.Component { constructor(props) { super(props); - - this.handleSubmit = this.handleSubmit.bind(this); - } - - handleSubmit(e) { - e.preventDefault(); - this.props.expenseDelete(this.props.expense); } render() { return (
-

Name: {this.props.expense.name}

-

Price: {this.props.expense.price}

- + this.props.expenseDelete(this.props.expense)} itemUpdate={() => this.props.expenseUpdate(this.props.expense)}> + {this.props.expense.name} + ${this.props.expense.price} + +
diff --git a/lab-nathan/src/components/expense-item.scss b/lab-nathan/src/components/expense-item.scss index e4e17d7..760eca9 100644 --- a/lab-nathan/src/components/expense-item.scss +++ b/lab-nathan/src/components/expense-item.scss @@ -2,4 +2,12 @@ .expense-item { background-color: white; -} \ No newline at end of file + + & .item-header { + border-bottom: none; + } + + & .expense-form { + display: none; + } +} diff --git a/lab-nathan/src/components/item-header.js b/lab-nathan/src/components/item-header.js new file mode 100644 index 0000000..3aa2bfc --- /dev/null +++ b/lab-nathan/src/components/item-header.js @@ -0,0 +1,42 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +class ItemHeader extends React.Component { + constructor(props) { + super(props); + + this.handleUpdate = this.handleUpdate.bind(this); + this.handleDelete = this.handleDelete.bind(this); + } + + handleDelete(e) { + e.preventDefault(); + this.props.itemDelete(); + } + + handleUpdate(e) { + e.preventDefault(); + this.props.itemUpdate(); + } + + render() { + return ( +
+ + {this.props.children} + + + +
+ ); + } +} + +ItemHeader.propTypes = { + children: PropTypes.array, + itemUpdate: PropTypes.func, + itemDelete: PropTypes.func, +}; + +export default ItemHeader; + diff --git a/lab-nathan/src/components/reorderable-list-item.js b/lab-nathan/src/components/reorderable-list-item.js new file mode 100644 index 0000000..2191aef --- /dev/null +++ b/lab-nathan/src/components/reorderable-list-item.js @@ -0,0 +1,46 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +class ReorderableListItem extends React.Component { + constructor(props) { + super(props); + + this.handleDrag = this.handleDrag.bind(this); + this.handleDragOver = this.handleDragOver.bind(this); + this.handleDrop = this.handleDrop.bind(this); + } + + handleDrag() { + this.props.requestReorder(this.props.index); + } + + handleDragOver() { + this.props.reorderTo(this.props.index); + } + + handleDrop() { + this.props.requestReorder(); + } + + render() { + return ( +
+ {this.props.children} +
+ ); + } +} + +ReorderableListItem.propTypes = { + children: PropTypes.object, + index: PropTypes.number, + requestReorder: PropTypes.func, + reorderTo: PropTypes.func +}; + +export default ReorderableListItem; diff --git a/lab-nathan/src/components/reorderable-list.js b/lab-nathan/src/components/reorderable-list.js new file mode 100644 index 0000000..0e698ef --- /dev/null +++ b/lab-nathan/src/components/reorderable-list.js @@ -0,0 +1,55 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import ReorderableListItem from './reorderable-list-item.js'; + +class ReorderableList extends React.Component { + constructor(props) { + super(props); + + this.state = { + selectedKey: undefined, + } + + this.requestReorder = this.requestReorder.bind(this); + this.reorderTo = this.reorderTo.bind(this); + } + + requestReorder(key) { + this.setState({ selectedKey: key }); + } + + reorderTo(key) { + if (this.state.selectedKey !== key) { + this.props.requestReorder(this.state.selectedKey, key); + } + } + + render() { + let listItems = this.props.children.map((child, index) => { + return ( + + + {this.props.children[index]} + + + )}); + + return ( +
+ {listItems} +
+ ); + } +} + +ReorderableList.propTypes = { + children: PropTypes.array, + requestReorder: PropTypes.func +}; + +export default ReorderableList; + diff --git a/lab-nathan/src/reducers/category-reducer.js b/lab-nathan/src/reducers/category-reducer.js index 65aa541..ef9b006 100644 --- a/lab-nathan/src/reducers/category-reducer.js +++ b/lab-nathan/src/reducers/category-reducer.js @@ -12,6 +12,23 @@ const categoryReducer = function(categories = [], action) { validateCategory(payload); return categories.map(category => category.id === payload.id ? payload : category); } + case 'CATEGORY_MOVE': { + validateCategory(payload.category); + + let newCategories = []; + + for (let index = 0; index < categories.length; index++) { + let category = categories[index]; + if (index === payload.newIndex) { + newCategories.push(payload.category); + } + if (category.id !== payload.category.id) { + newCategories.push(category); + } + } + + return newCategories; + } case 'CATEGORY_DELETE': { validateCategory(payload); return categories.filter(category => category.id !== payload.id); From 456fdd516e29cdbdbbdbae37fc665532542f2822 Mon Sep 17 00:00:00 2001 From: Nathan Harrenstein Date: Mon, 4 Sep 2017 23:13:34 -0700 Subject: [PATCH 18/18] Hide and show edit form. --- lab-nathan/src/actions/category-actions.js | 8 --- lab-nathan/src/components/category-form.js | 1 - lab-nathan/src/components/category-form.scss | 0 lab-nathan/src/components/category-item.js | 23 ++++++-- lab-nathan/src/components/category-item.scss | 1 - lab-nathan/src/components/dashboard.js | 17 +----- lab-nathan/src/components/expense-item.js | 24 ++++++-- lab-nathan/src/components/expense-item.scss | 15 ++++- .../src/components/reorderable-list-item.js | 46 ---------------- lab-nathan/src/components/reorderable-list.js | 55 ------------------- lab-nathan/src/index.html | 1 - lab-nathan/src/reducers/category-reducer.js | 17 ------ 12 files changed, 50 insertions(+), 158 deletions(-) delete mode 100644 lab-nathan/src/components/category-form.scss delete mode 100644 lab-nathan/src/components/reorderable-list-item.js delete mode 100644 lab-nathan/src/components/reorderable-list.js diff --git a/lab-nathan/src/actions/category-actions.js b/lab-nathan/src/actions/category-actions.js index 4bb5e41..ec0aff0 100644 --- a/lab-nathan/src/actions/category-actions.js +++ b/lab-nathan/src/actions/category-actions.js @@ -14,14 +14,6 @@ export const categoryUpdate = (category) => ({ payload: category }); -export const categoryMove = (category, newIndex) => ({ - type: 'CATEGORY_MOVE', - payload: { - category: category, - newIndex: newIndex - } -}); - export const categoryDelete = (category) => ({ type: 'CATEGORY_DELETE', payload: category diff --git a/lab-nathan/src/components/category-form.js b/lab-nathan/src/components/category-form.js index 2fb618b..3ff9010 100644 --- a/lab-nathan/src/components/category-form.js +++ b/lab-nathan/src/components/category-form.js @@ -1,4 +1,3 @@ -import './category-form.scss'; import React from 'react'; import PropTypes from 'prop-types'; diff --git a/lab-nathan/src/components/category-form.scss b/lab-nathan/src/components/category-form.scss deleted file mode 100644 index e69de29..0000000 diff --git a/lab-nathan/src/components/category-item.js b/lab-nathan/src/components/category-item.js index ba07d98..611fa3a 100644 --- a/lab-nathan/src/components/category-item.js +++ b/lab-nathan/src/components/category-item.js @@ -19,6 +19,10 @@ import { class CategoryItem extends React.Component { constructor(props) { super(props); + + this.state = { + showUpdate: false + } } render() { @@ -33,15 +37,24 @@ class CategoryItem extends React.Component { let overBudget = amountSpent > category.budget; return (
- this.props.categoryDelete(category)} itemUpdate={() => this.props.categoryUpdate(category)}> + this.props.categoryDelete(category)} itemUpdate={() => this.setState({ showUpdate: !this.state.showUpdate })}> {category.name} ${category.budget - amountSpent} - + {this.state.showUpdate + ? + { + this.setState({ showUpdate: false }); + return this.props.categoryUpdate(category); + }} /> + : + null + } + @@ -25,13 +14,13 @@ class DashboardContainer extends React.Component { buttonText='Add' onComplete={this.props.categoryCreate} /> - +
{this.props.categories.map((item, index) => )} - +
); } @@ -40,7 +29,6 @@ class DashboardContainer extends React.Component { DashboardContainer.propTypes = { categories: PropTypes.array, categoryCreate: PropTypes.func, - categoryMove: PropTypes.func }; const mapStateToProps = (state) => ({ @@ -50,7 +38,6 @@ const mapStateToProps = (state) => ({ const mapDispatchToProps = (dispatch) => { return { categoryCreate: (category) => dispatch(categoryCreate(category)), - categoryMove: (category, newIndex) => dispatch(categoryMove(category, newIndex)), } } diff --git a/lab-nathan/src/components/expense-item.js b/lab-nathan/src/components/expense-item.js index 0c977ab..5baa226 100644 --- a/lab-nathan/src/components/expense-item.js +++ b/lab-nathan/src/components/expense-item.js @@ -13,20 +13,32 @@ import { class ExpenseItem extends React.Component { constructor(props) { super(props); + + this.state = { + showUpdate: false + } } render() { return (
- this.props.expenseDelete(this.props.expense)} itemUpdate={() => this.props.expenseUpdate(this.props.expense)}> - {this.props.expense.name} + this.props.expenseDelete(this.props.expense)} itemUpdate={() => this.setState({ showUpdate: !this.state.showUpdate })}> + {this.props.expense.name} ${this.props.expense.price} - + {this.state.showUpdate + ? + { + this.setState({ showUpdate: false }); + return this.props.expenseUpdate(expense); + }} /> + : + null + }
); } diff --git a/lab-nathan/src/components/expense-item.scss b/lab-nathan/src/components/expense-item.scss index 760eca9..d63a018 100644 --- a/lab-nathan/src/components/expense-item.scss +++ b/lab-nathan/src/components/expense-item.scss @@ -6,8 +6,17 @@ & .item-header { border-bottom: none; } +} + +.item-header { + & .item-header-left { + flex: 1; + font-size: 1.25rem; + display: flex; + align-items: baseline; - & .expense-form { - display: none; + & .expense-title { + margin-right: ($gutter-small + $gutter-xsmall) / 2; + } } -} +} \ No newline at end of file diff --git a/lab-nathan/src/components/reorderable-list-item.js b/lab-nathan/src/components/reorderable-list-item.js deleted file mode 100644 index 2191aef..0000000 --- a/lab-nathan/src/components/reorderable-list-item.js +++ /dev/null @@ -1,46 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; - -class ReorderableListItem extends React.Component { - constructor(props) { - super(props); - - this.handleDrag = this.handleDrag.bind(this); - this.handleDragOver = this.handleDragOver.bind(this); - this.handleDrop = this.handleDrop.bind(this); - } - - handleDrag() { - this.props.requestReorder(this.props.index); - } - - handleDragOver() { - this.props.reorderTo(this.props.index); - } - - handleDrop() { - this.props.requestReorder(); - } - - render() { - return ( -
- {this.props.children} -
- ); - } -} - -ReorderableListItem.propTypes = { - children: PropTypes.object, - index: PropTypes.number, - requestReorder: PropTypes.func, - reorderTo: PropTypes.func -}; - -export default ReorderableListItem; diff --git a/lab-nathan/src/components/reorderable-list.js b/lab-nathan/src/components/reorderable-list.js deleted file mode 100644 index 0e698ef..0000000 --- a/lab-nathan/src/components/reorderable-list.js +++ /dev/null @@ -1,55 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import ReorderableListItem from './reorderable-list-item.js'; - -class ReorderableList extends React.Component { - constructor(props) { - super(props); - - this.state = { - selectedKey: undefined, - } - - this.requestReorder = this.requestReorder.bind(this); - this.reorderTo = this.reorderTo.bind(this); - } - - requestReorder(key) { - this.setState({ selectedKey: key }); - } - - reorderTo(key) { - if (this.state.selectedKey !== key) { - this.props.requestReorder(this.state.selectedKey, key); - } - } - - render() { - let listItems = this.props.children.map((child, index) => { - return ( - - - {this.props.children[index]} - - - )}); - - return ( -
- {listItems} -
- ); - } -} - -ReorderableList.propTypes = { - children: PropTypes.array, - requestReorder: PropTypes.func -}; - -export default ReorderableList; - diff --git a/lab-nathan/src/index.html b/lab-nathan/src/index.html index 0a0d344..45623fe 100644 --- a/lab-nathan/src/index.html +++ b/lab-nathan/src/index.html @@ -9,7 +9,6 @@

Expense Tracker

\ No newline at end of file diff --git a/lab-nathan/src/reducers/category-reducer.js b/lab-nathan/src/reducers/category-reducer.js index ef9b006..65aa541 100644 --- a/lab-nathan/src/reducers/category-reducer.js +++ b/lab-nathan/src/reducers/category-reducer.js @@ -12,23 +12,6 @@ const categoryReducer = function(categories = [], action) { validateCategory(payload); return categories.map(category => category.id === payload.id ? payload : category); } - case 'CATEGORY_MOVE': { - validateCategory(payload.category); - - let newCategories = []; - - for (let index = 0; index < categories.length; index++) { - let category = categories[index]; - if (index === payload.newIndex) { - newCategories.push(payload.category); - } - if (category.id !== payload.category.id) { - newCategories.push(category); - } - } - - return newCategories; - } case 'CATEGORY_DELETE': { validateCategory(payload); return categories.filter(category => category.id !== payload.id);