From 8ab276406af665657c8870b6c861fe2b5f174729 Mon Sep 17 00:00:00 2001 From: Bret Ldenburg Date: Mon, 28 Aug 2017 18:29:03 -0700 Subject: [PATCH 1/4] finished as much of the lab as I could, got the delete button to work but I still need to work on the update button --- .babelrc | 4 + .dev.env | 1 + .eslintrc | 21 ++++ .gitignore | 136 +++++++++++++++++++++ package.json | 48 ++++++++ src/action/category-actions.js | 22 ++++ src/component/app/index.js | 31 +++++ src/component/category-form/index.js | 67 ++++++++++ src/component/category-item/index.js | 39 ++++++ src/component/dashboard-container/index.js | 57 +++++++++ src/index.html | 9 ++ src/lib/store.js | 4 + src/lib/util.js | 4 + src/main.js | 5 + src/reducer/category.js | 19 +++ webpack.config.js | 85 +++++++++++++ 16 files changed, 552 insertions(+) create mode 100644 .babelrc create mode 100644 .dev.env create mode 100644 .eslintrc create mode 100644 .gitignore create mode 100644 package.json create mode 100644 src/action/category-actions.js create mode 100644 src/component/app/index.js create mode 100644 src/component/category-form/index.js create mode 100644 src/component/category-item/index.js create mode 100644 src/component/dashboard-container/index.js create mode 100644 src/index.html create mode 100644 src/lib/store.js create mode 100644 src/lib/util.js create mode 100644 src/main.js create mode 100644 src/reducer/category.js create mode 100644 webpack.config.js diff --git a/.babelrc b/.babelrc new file mode 100644 index 0000000..47c9ace --- /dev/null +++ b/.babelrc @@ -0,0 +1,4 @@ +{ + "presets": ["es2015", "react"], + "plugins": ["transform-object-rest-spread"] +} diff --git a/.dev.env b/.dev.env new file mode 100644 index 0000000..13ea3e1 --- /dev/null +++ b/.dev.env @@ -0,0 +1 @@ +NODE_ENV='dev' diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..8dc6807 --- /dev/null +++ b/.eslintrc @@ -0,0 +1,21 @@ +{ + "rules": { + "no-console": "off", + "indent": [ "error", 2 ], + "quotes": [ "error", "single" ], + "semi": ["error", "always"], + "linebreak-style": [ "error", "unix" ] + }, + "env": { + "es6": true, + "node": true, + "mocha": true, + "jasmine": true + }, + "ecmaFeatures": { + "modules": true, + "experimentalObjectRestSpread": true, + "impliedStrict": true + }, + "extends": "eslint:recommended" +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..345130c --- /dev/null +++ b/.gitignore @@ -0,0 +1,136 @@ +# Created by https://www.gitignore.io/api/osx,vim,node,macos,windows + +### macOS ### +*.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +### Node ### +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (http://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Typescript v1 declaration files +typings/ + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env + + +### OSX ### + +# Icon must end with two \r + +# Thumbnails + +# Files that might appear in the root of a volume + +# Directories potentially created on remote AFP share + +### Vim ### +# swap +[._]*.s[a-v][a-z] +[._]*.sw[a-p] +[._]s[a-v][a-z] +[._]sw[a-p] +# session +Session.vim +# temporary +.netrwhist +*~ +# auto-generated tag files +tags + +### Windows ### +# Windows thumbnail cache files +Thumbs.db +ehthumbs.db +ehthumbs_vista.db + +# Folder config file +Desktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msm +*.msp + +# Windows shortcuts +*.lnk + +# End of https://www.gitignore.io/api/osx,vim,node,macos,windows diff --git a/package.json b/package.json new file mode 100644 index 0000000..b7446fc --- /dev/null +++ b/package.json @@ -0,0 +1,48 @@ +{ + "name": "26-react-redux", + "version": "1.0.0", + "description": "![cf](https://i.imgur.com/7v5ASc8.png) 26: React & Redux ======", + "main": "index.js", + "scripts": { + "build": "webpack", + "watch": "webpack-dev-server --inline --hot" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/bretladenburg/26-react-redux.git" + }, + "keywords": [], + "author": "", + "license": "ISC", + "bugs": { + "url": "https://github.com/bretladenburg/26-react-redux/issues" + }, + "homepage": "https://github.com/bretladenburg/26-react-redux#readme", + "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-router-dom": "^4.2.2", + "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" + } +} diff --git a/src/action/category-actions.js b/src/action/category-actions.js new file mode 100644 index 0000000..0b9eb6b --- /dev/null +++ b/src/action/category-actions.js @@ -0,0 +1,22 @@ +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'}) diff --git a/src/component/app/index.js b/src/component/app/index.js new file mode 100644 index 0000000..2b957a7 --- /dev/null +++ b/src/component/app/index.js @@ -0,0 +1,31 @@ +import React from 'react'; +import {Provider} from 'react-redux'; +import {BrowserRouter, Route} from 'react-router-dom'; +import createAppStore from '../../lib/store.js'; +import DashboardContainer from '../dashboard-container'; + +const store = createAppStore(); + +class App extends React.Component { + componentDidMount() { + store.subscribe(() => { + console.log('__STATE__', store.getState()); + }); + + store.dispatch({type: null}); + } + + render() { + return ( +
+ + + + + +
+ ) + } +} + +export default App; diff --git a/src/component/category-form/index.js b/src/component/category-form/index.js new file mode 100644 index 0000000..48e6aad --- /dev/null +++ b/src/component/category-form/index.js @@ -0,0 +1,67 @@ +import React from 'react'; +import uuid from 'uuid'; + +class CategoryForm extends React.Component { + constructor(props) { + super(props); + + this.state = { + id: props.category ? props.category.id: uuid.v1(), + timestamp: props.category ? props.category.timestamp: new Date(), + name: props.category ? props.category.name: '', + budget: props.category ? props.category.budget: '' + } + + this.handleChange = this.handleChange.bind(this); + this.handleSubmit = this.handleSubmit.bind(this); + } + + handleChange(e) { + let {name, value, type} = e.target; + + if (type === 'number') { + try { + this.setState({ + [name]: parseInt(value) + }) + } catch(err) { + console.error(err); + } + } else { + this.setState({ + [name]: value + }) + } + } + + handleSubmit(e) { + e.preventDefault(); + this.props.onComplete(Object.assign({}, this.state)); + } + + render() { + return ( +
+ + + + + +
+ ) + } +} + +export default CategoryForm; diff --git a/src/component/category-item/index.js b/src/component/category-item/index.js new file mode 100644 index 0000000..e543570 --- /dev/null +++ b/src/component/category-item/index.js @@ -0,0 +1,39 @@ +import React from 'react'; +import CategoryForm from '../category-form'; + +class CategoryItem extends React.Component { + constructor(props){ + super(props) + console.log('hello there!!!', this.props.categoryDelete ); +} + render() { + return ( +
+ +
+ ) + } +} + +export default CategoryItem; diff --git a/src/component/dashboard-container/index.js b/src/component/dashboard-container/index.js new file mode 100644 index 0000000..1b1d19b --- /dev/null +++ b/src/component/dashboard-container/index.js @@ -0,0 +1,57 @@ +import React from 'react'; +import {connect} from 'react-redux'; + +import { + categoryCreate, + categoryUpdate, + categoryDelete +} from '../../action/category-actions.js'; + +import CategoryForm from '../category-form'; +import CategoryItem from '../category-item'; + +class DashboardContainer extends React.Component { + constructor(props){ + super(props); + } + render() { + return ( +
+

Dashboard

+ + + + + + {this.props.categories.map((item) => +
+

{item.title}

+
+ )} +
+ ) + } +} + +const mapStateToProps = (state) => { + return { + categories: state + } +} + +const mapDispatchToProps = (dispatch, getState) => { + return { + categoryCreate: (category) => dispatch(categoryCreate(category)), + categoryUpdate: (category) => dispatch(categoryUpdate(category)), + categoryDelete: (category) => dispatch(categoryDelete(category)) + } +} + +export default connect(mapStateToProps, mapDispatchToProps)(DashboardContainer); diff --git a/src/index.html b/src/index.html new file mode 100644 index 0000000..e28b54a --- /dev/null +++ b/src/index.html @@ -0,0 +1,9 @@ + + + + Budget App + + +
+ + diff --git a/src/lib/store.js b/src/lib/store.js new file mode 100644 index 0000000..d99620c --- /dev/null +++ b/src/lib/store.js @@ -0,0 +1,4 @@ +import {createStore} from 'redux'; +import reducer from '../reducer/category.js'; + +export default () => createStore(reducer); diff --git a/src/lib/util.js b/src/lib/util.js new file mode 100644 index 0000000..526f9a5 --- /dev/null +++ b/src/lib/util.js @@ -0,0 +1,4 @@ +export const renderIf = (test, component) => test ? component : undefined; + +export const classToggler = (options) => + Object.keys(options).filter(key => !!options[keys]).join(' '); diff --git a/src/main.js b/src/main.js new file mode 100644 index 0000000..faafd74 --- /dev/null +++ b/src/main.js @@ -0,0 +1,5 @@ +import React from 'react'; +import ReactDom from 'react-dom'; +import App from './component/app'; + +ReactDom.render(, document.getElementById('root')); diff --git a/src/reducer/category.js b/src/reducer/category.js new file mode 100644 index 0000000..46e14fd --- /dev/null +++ b/src/reducer/category.js @@ -0,0 +1,19 @@ +let initialState = []; + +export default (state=initialState, action) => { + let {type, payload} = action; + + switch(type) { + case 'CATEGORY_CREATE': + return [...state, 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 initialState + default: + return state + } +} diff --git a/webpack.config.js b/webpack.config.js new file mode 100644 index 0000000..84feae2 --- /dev/null +++ b/webpack.config.js @@ -0,0 +1,85 @@ +'use strict'; + +require('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 CleanPlugin = require('clean-webpack-plugin'); +const UglifyPlugin = require('uglifyjs-webpack-plugin'); +const ExtractPlugin = require('extract-text-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.concat([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)$/, + exclude: /\.glyph.svg/, + use: [ + { + loader: 'file-loader', + options: {name: 'audio/[name].[ext]'} + } + ] + } + ] + } +}; From 7e9b4dfef4896f44a7928cac23e1b8385abf1278 Mon Sep 17 00:00:00 2001 From: Bret Ldenburg Date: Tue, 29 Aug 2017 17:43:23 -0700 Subject: [PATCH 2/4] made progress on the lab today --- package.json | 13 +++- src/__test__/category-actions.test.js | 0 src/__test__/category-reducer.test.js | 0 src/action/expense-actions.js | 22 +++++++ src/component/category-form/index.js | 8 ++- src/component/category-item/index.js | 71 +++++++++++++-------- src/component/dashboard-container/index.js | 32 ++++------ src/component/expense-form/index.js | 74 ++++++++++++++++++++++ src/component/expense-item/index.js | 38 +++++++++++ src/lib/store.js | 2 +- src/reducer/expenses.js | 25 ++++++++ src/reducer/index.js | 8 +++ 12 files changed, 245 insertions(+), 48 deletions(-) create mode 100644 src/__test__/category-actions.test.js create mode 100644 src/__test__/category-reducer.test.js create mode 100644 src/action/expense-actions.js create mode 100644 src/component/expense-form/index.js create mode 100644 src/component/expense-item/index.js create mode 100644 src/reducer/expenses.js create mode 100644 src/reducer/index.js diff --git a/package.json b/package.json index b7446fc..8f963e2 100644 --- a/package.json +++ b/package.json @@ -3,9 +3,19 @@ "version": "1.0.0", "description": "![cf](https://i.imgur.com/7v5ASc8.png) 26: React & Redux ======", "main": "index.js", + "jest": { + "globals": { + "__DEBUG__": false, + "process.env": { + "NODE_ENV": "testing" + } + } + }, "scripts": { "build": "webpack", - "watch": "webpack-dev-server --inline --hot" + "watch": "webpack-dev-server --inline --hot", + "test": "jest --coverage", + "test-watch": "jest --watchAll" }, "repository": { "type": "git", @@ -30,6 +40,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", "react": "^15.6.1", "react-dom": "^15.6.1", diff --git a/src/__test__/category-actions.test.js b/src/__test__/category-actions.test.js new file mode 100644 index 0000000..e69de29 diff --git a/src/__test__/category-reducer.test.js b/src/__test__/category-reducer.test.js new file mode 100644 index 0000000..e69de29 diff --git a/src/action/expense-actions.js b/src/action/expense-actions.js new file mode 100644 index 0000000..050b578 --- /dev/null +++ b/src/action/expense-actions.js @@ -0,0 +1,22 @@ +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 +}) + +export const expenseReset = () => ({type: 'EXPENSE_RESET'}) diff --git a/src/component/category-form/index.js b/src/component/category-form/index.js index 48e6aad..a7bcaa1 100644 --- a/src/component/category-form/index.js +++ b/src/component/category-form/index.js @@ -16,6 +16,12 @@ class CategoryForm extends React.Component { this.handleSubmit = this.handleSubmit.bind(this); } + componentWillReceiveProps(props) { + if (props.category) { + this.setState(props.category); + } + } + handleChange(e) { let {name, value, type} = e.target; @@ -36,7 +42,7 @@ class CategoryForm extends React.Component { handleSubmit(e) { e.preventDefault(); - this.props.onComplete(Object.assign({}, this.state)); + this.props.onComplete({...this.state}); } render() { diff --git a/src/component/category-item/index.js b/src/component/category-item/index.js index e543570..478a0ea 100644 --- a/src/component/category-item/index.js +++ b/src/component/category-item/index.js @@ -1,39 +1,56 @@ import React from 'react'; +import {connect} from 'react-redux'; import CategoryForm from '../category-form'; +import ExpenseForm from '../expense-form'; +import {categoryUpdate, categoryDelete} from '../../action/category-actions.js'; +import {expenseCreate} from '../../action/expense-actions.js'; + class CategoryItem extends React.Component { - constructor(props){ - super(props) - console.log('hello there!!!', this.props.categoryDelete ); -} render() { + let {category, categoryUpdate, categoryDelete} = this.props; + console.log(this.props); + return (
-
    - {this.props.categories.map((item, i) => -
  • - - -
    -

    name: {item.name}

    -

    budget: {item.budget}

    -
    - - { - category.id = item.id; - this.props.categoryUpdate(category); - }} - /> -
  • - )} -
+
+
+

{category.name}

+

{category.budget}

+ +
+
+ + + + + +
+
) } } -export default CategoryItem; +const mapStateToProps = (state) => { + return { + categories: state.categories, + expenses: state.expenses + } +} + +let mapDispatchToProps = dispatch => ({ + categoryUpdate: (category) => dispatch(categoryUpdate(category)), + categoryDelete: (category) => dispatch(categoryDelete(category)), + expenseCreate: (expense) => dispatch(expenseCreate(expense)) +}); + +export default connect(mapStateToProps, mapDispatchToProps)(CategoryItem); diff --git a/src/component/dashboard-container/index.js b/src/component/dashboard-container/index.js index 1b1d19b..1d3dbea 100644 --- a/src/component/dashboard-container/index.js +++ b/src/component/dashboard-container/index.js @@ -1,14 +1,13 @@ import React from 'react'; import {connect} from 'react-redux'; -import { - categoryCreate, - categoryUpdate, - categoryDelete -} from '../../action/category-actions.js'; +import {categoryCreate as categoryActionCreate} from '../../action/category-actions.js'; +import {expenseCreate as expenseActionCreate} from '../../action/expense-actions.js'; import CategoryForm from '../category-form'; import CategoryItem from '../category-item'; +import ExpenseForm from '../expense-form'; +import ExpenseItem from '../expense-item'; class DashboardContainer extends React.Component { constructor(props){ @@ -24,17 +23,14 @@ class DashboardContainer extends React.Component { onComplete={this.props.categoryCreate} /> - - {this.props.categories.map((item) => -
-

{item.title}

-
+ )} + + ) } @@ -42,15 +38,15 @@ class DashboardContainer extends React.Component { const mapStateToProps = (state) => { return { - categories: state + categories: state.categories, + expenses: state.expenses } } const mapDispatchToProps = (dispatch, getState) => { return { - categoryCreate: (category) => dispatch(categoryCreate(category)), - categoryUpdate: (category) => dispatch(categoryUpdate(category)), - categoryDelete: (category) => dispatch(categoryDelete(category)) + categoryCreate: (category) => dispatch(categoryActionCreate(category)), + expenseCreate: (expense) => dispatch(expenseActionCreate(expense)) } } diff --git a/src/component/expense-form/index.js b/src/component/expense-form/index.js new file mode 100644 index 0000000..36db8a3 --- /dev/null +++ b/src/component/expense-form/index.js @@ -0,0 +1,74 @@ +import React from 'react'; +import uuid from 'uuid'; + +class ExpenseForm extends React.Component { + constructor(props){ + super(props); + + this.state = { + id: props.expense ? props.expense.id: uuid.v1(), + timestamp: props.expense ? props.expense.timestamp: new Date(), + name: props.expense ? props.expense.name: '', + categoryID: props.category ? props.category.id: null, + price: props.category ? props.category.budget: '' + } + + this.handleChange = this.handleChange.bind(this); + this.handleSubmit = this.handleSubmit.bind(this); + } + + componentWillReceiveProps(props) { + if (props.expense) { + this.setState(props.expense); + } + } + + handleChange(e) { + let {name, value, type} = e.target; + + if (type === 'number') { + try { + this.setState({ + [name]: parseInt(value) + }) + } catch(err) { + console.error(err); + } + } else { + this.setState({ + [name]: value + }) + } + } + + handleSubmit(e) { + e.preventDefault(); + this.props.onComplete({...this.state}); + } + + render() { + return ( +
+ + + + + +
+ ) + } +} + +export default ExpenseForm; diff --git a/src/component/expense-item/index.js b/src/component/expense-item/index.js new file mode 100644 index 0000000..af1fc0d --- /dev/null +++ b/src/component/expense-item/index.js @@ -0,0 +1,38 @@ +import React from 'react'; +import {connect} from 'react-redux'; +import ExpenseForm from '../expense-form'; +import {expenseUpdate, expenseDelete} from '../../action/expense-actions.js'; + +class ExpenseItem extends React.Component { + render() { + let {expense, expenseUpdate, expenseDelete} = this.props; + + return ( +
+
+
+

{expense.name}

+

{expense.price}

+ +
+
+ +
+
+
+ ) + } +} + +let mapStateToProps = () =>({}); + +let mapDispatchToProps = dispatch => ({ + expenseUpdate: (expense) => dispatch(expenseUpdate(expense)), + expenseDelete: (expense) => dispatch(expenseDelete(expense)) +}); + +export default connect(mapStateToProps, mapDispatchToProps)(ExpenseItem); diff --git a/src/lib/store.js b/src/lib/store.js index d99620c..169be6d 100644 --- a/src/lib/store.js +++ b/src/lib/store.js @@ -1,4 +1,4 @@ import {createStore} from 'redux'; -import reducer from '../reducer/category.js'; +import reducer from '../reducer'; export default () => createStore(reducer); diff --git a/src/reducer/expenses.js b/src/reducer/expenses.js new file mode 100644 index 0000000..681a5e3 --- /dev/null +++ b/src/reducer/expenses.js @@ -0,0 +1,25 @@ +let initialState = {}; + +export default (state=initialState, action) => { + let {type, payload} = action; + + switch(type) { + case 'CATEGORY_CREATE': + return {...state, [payload.id] : []}; + case 'CATEGORY-DELETE': + return {...state, [payload.id] : undefined}; + case 'EXPENSE_CREATE': + let {categoryID} = payload; + let categoryExpenses = state[categoryID]; + return {...state, [categoryID] : [...categoryExpenses, payload]}; + case 'EXPENSE_UPDATE': + // let {categoryID} = payload; + return state[categoryID].map(expense => { + expense.id === payload.id ? payload : expense + }) + case 'EXPENSE_DELETE': + + default: + return state; + } +} diff --git a/src/reducer/index.js b/src/reducer/index.js new file mode 100644 index 0000000..250d838 --- /dev/null +++ b/src/reducer/index.js @@ -0,0 +1,8 @@ +import {combineReducers} from 'redux'; +import categoriesReducer from './category.js'; +import expensesReducer from './expenses.js'; + +export default combineReducers({ + categories: categoriesReducer, + expenses: expensesReducer +}); From 1c550b5e14b763a234b2c30c1178c3dc280ea2d4 Mon Sep 17 00:00:00 2001 From: Bret Ldenburg Date: Wed, 30 Aug 2017 17:32:26 -0700 Subject: [PATCH 3/4] got all of the functionality working for the app, still need to work on some tests --- src/__test__/category-actions.test.js | 29 +++++++++++++++ src/__test__/category-reducer.test.js | 40 +++++++++++++++++++++ src/action/expense-actions.js | 16 ++++----- src/component/category-item/index.js | 38 +++++++++++--------- src/component/expense-form/index.js | 11 ++++-- src/component/expense-item/index.js | 13 +++---- src/lib/redux-reporter.js | 15 ++++++++ src/lib/store.js | 5 +-- src/reducer/expenses.js | 52 ++++++++++++++++++++++----- 9 files changed, 174 insertions(+), 45 deletions(-) create mode 100644 src/lib/redux-reporter.js diff --git a/src/__test__/category-actions.test.js b/src/__test__/category-actions.test.js index e69de29..e491650 100644 --- a/src/__test__/category-actions.test.js +++ b/src/__test__/category-actions.test.js @@ -0,0 +1,29 @@ +import {categoryCreate, categoryUpdate, categoryDelete} from '../action/category-actions.js'; + +describe('Category Actions', () => { + test('categoryCreate returns a CATEGORY_CREATE action', () => { + let action = categoryCreate({name: 'test name'}); + expect(action.type).toEqual('CATEGORY_CREATE'); + expect(action.payload.id).toBeTruthy(); + expect(action.payload.timestamp).toBeTruthy(); + expect(action.payload.name).toBe('test name'); + }); + + test('categoryDelete returns a CATEGORY_DELETE action', () => { + let category = {id: '9876', timestamp: new Date(), name: 'test name'}; + let action = categoryDelete(category); + expect(action).toEqual({ + type: 'CATEGORY_DELETE', + payload: category + }) + }); + + test('categoryUpdate returns a CATEGORY_UPDATE action', () => { + let category = {id: '9876', timestamp: new Date(), name: 'test name'}; + let action = categoryUpdate(category); + expect(action).toEqual({ + type: 'CATEGORY_UPDATE', + payload: category + }); + }); +}); diff --git a/src/__test__/category-reducer.test.js b/src/__test__/category-reducer.test.js index e69de29..2227f16 100644 --- a/src/__test__/category-reducer.test.js +++ b/src/__test__/category-reducer.test.js @@ -0,0 +1,40 @@ +import categoryReducer from '../reducer/category.js'; + +describe('Category Reducer', () => { + test('initial state 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', name: 'some name'}, + {id: 'anotherid', name: 'another name'} + ] + + 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 with new information', () => { + let action = { + type: 'CATEGORY_UPDATE', + payload: 'sample payload' + } + + let result = categoryReducer([...state], action); + expect(result.length).toBe(1); + expect(result[0]).toBe(action.payload); + }); +}); diff --git a/src/action/expense-actions.js b/src/action/expense-actions.js index 050b578..9ead500 100644 --- a/src/action/expense-actions.js +++ b/src/action/expense-actions.js @@ -1,22 +1,18 @@ import uuid from 'uuid/v1'; -export const expenseCreate = (expense) => { - expense.id = uuid(); - expense.timestamp = new Date(); - return { - type: 'EXPENSE_CREATE', - payload: expense - } -} +export const expenseCreate = (expense) => ({ + type: 'EXPENSE_CREATE', + payload: {...expense, id: uuid(), timestamp: new Date()} +}) export const expenseUpdate = (expense) => ({ type: 'EXPENSE_UPDATE', - payload: expense + payload: {...expense} }) export const expenseDelete = (expense) => ({ type: 'EXPENSE_DELETE', - payload: expense + payload: {...expense} }) export const expenseReset = () => ({type: 'EXPENSE_RESET'}) diff --git a/src/component/category-item/index.js b/src/component/category-item/index.js index 478a0ea..d1b0f8d 100644 --- a/src/component/category-item/index.js +++ b/src/component/category-item/index.js @@ -2,18 +2,18 @@ import React from 'react'; import {connect} from 'react-redux'; import CategoryForm from '../category-form'; import ExpenseForm from '../expense-form'; +import ExpenseItem from '../expense-item' import {categoryUpdate, categoryDelete} from '../../action/category-actions.js'; import {expenseCreate} from '../../action/expense-actions.js'; class CategoryItem extends React.Component { render() { - let {category, categoryUpdate, categoryDelete} = this.props; - console.log(this.props); + let {category, categoryUpdate, categoryDelete, expenses} = this.props; return (
-
+

{category.name}

{category.budget}

@@ -25,27 +25,31 @@ class CategoryItem extends React.Component { category={category} onComplete={categoryUpdate} /> - - - -
+ +
+ + +
    + {expenses.map(expense => + + )} +
+
) } } -const mapStateToProps = (state) => { - return { - categories: state.categories, - expenses: state.expenses - } -} +let mapStateToProps = (state, props) => ({ + expenses: state.expenses[props.category.id] +}); + let mapDispatchToProps = dispatch => ({ categoryUpdate: (category) => dispatch(categoryUpdate(category)), diff --git a/src/component/expense-form/index.js b/src/component/expense-form/index.js index 36db8a3..0160603 100644 --- a/src/component/expense-form/index.js +++ b/src/component/expense-form/index.js @@ -19,7 +19,11 @@ class ExpenseForm extends React.Component { componentWillReceiveProps(props) { if (props.expense) { - this.setState(props.expense); + this.setState({...props.expense}); + } + + if (props.categoryID) { + this.setState({categoryID: props.categoryID}) } } @@ -43,7 +47,10 @@ class ExpenseForm extends React.Component { handleSubmit(e) { e.preventDefault(); - this.props.onComplete({...this.state}); + this.props.onComplete(this.state); + if (!this.props.expense) { + this.setState({name: ''}); + } } render() { diff --git a/src/component/expense-item/index.js b/src/component/expense-item/index.js index af1fc0d..6e34a04 100644 --- a/src/component/expense-item/index.js +++ b/src/component/expense-item/index.js @@ -5,32 +5,33 @@ import {expenseUpdate, expenseDelete} from '../../action/expense-actions.js'; class ExpenseItem extends React.Component { render() { - let {expense, expenseUpdate, expenseDelete} = this.props; + let {category, expense, expenseUpdate, expenseDelete} = this.props; return ( -
+
  • {expense.name}

    {expense.price}

    - +
    -
  • + ) } } let mapStateToProps = () =>({}); -let mapDispatchToProps = dispatch => ({ +let mapDispatchToProps = (dispatch) => ({ expenseUpdate: (expense) => dispatch(expenseUpdate(expense)), expenseDelete: (expense) => dispatch(expenseDelete(expense)) }); diff --git a/src/lib/redux-reporter.js b/src/lib/redux-reporter.js new file mode 100644 index 0000000..5e02f4f --- /dev/null +++ b/src/lib/redux-reporter.js @@ -0,0 +1,15 @@ +let reporter = store => next => action => { + console.log('__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; diff --git a/src/lib/store.js b/src/lib/store.js index 169be6d..177095f 100644 --- a/src/lib/store.js +++ b/src/lib/store.js @@ -1,4 +1,5 @@ -import {createStore} from 'redux'; +import {createStore, applyMiddleware} from 'redux'; import reducer from '../reducer'; +import reporter from './redux-reporter.js'; -export default () => createStore(reducer); +export default () => createStore(reducer, applyMiddleware(reporter)); diff --git a/src/reducer/expenses.js b/src/reducer/expenses.js index 681a5e3..3c33d51 100644 --- a/src/reducer/expenses.js +++ b/src/reducer/expenses.js @@ -1,24 +1,60 @@ +let validateCategory = (category) => { + if (!category.id || !category.name || !category.timestamp || !category.budget) { + throw new Error('VALIDATION ERROR: category must include id, name, timestamp and budget') + } +} + +let validateExpense = (expense) => { + if (!expense.id || !expense.name || !expense.categoryID) { + throw new Error('VALIDATION ERROR: expense must include an id, name, and a categoryID') + } +} + let initialState = {}; export default (state=initialState, action) => { let {type, payload} = action; + let categoryID, categoryExpenses; switch(type) { case 'CATEGORY_CREATE': + console.log('__STATE__!!', state); + validateCategory(payload); return {...state, [payload.id] : []}; - case 'CATEGORY-DELETE': + case 'CATEGORY_DELETE': + validateCategory(payload); return {...state, [payload.id] : undefined}; case 'EXPENSE_CREATE': - let {categoryID} = payload; - let categoryExpenses = state[categoryID]; - return {...state, [categoryID] : [...categoryExpenses, payload]}; + validateExpense(payload); + categoryID = payload.categoryID; + categoryExpenses = state[categoryID]; + return {...state, [categoryID]: [...categoryExpenses, payload]}; case 'EXPENSE_UPDATE': - // let {categoryID} = payload; - return state[categoryID].map(expense => { - expense.id === payload.id ? payload : expense - }) + validateExpense(payload); + categoryID = payload.categoryID; + categoryExpenses = state[categoryID]; + + return { + ...state, + [categoryID]: categoryExpenses.map(expense => { + console.log('LOGGING EXPENSE', expense); + return expense.id === payload.id ? payload : expense; + }) + } case 'EXPENSE_DELETE': + console.log('LOGGING THE PAYLOAD', payload); + console.log('LOGGING THE STATE', state); + console.log('LOGGING CATEGORY ID', categoryID); + validateExpense(payload); + categoryID = payload.categoryID; + categoryExpenses = state[categoryID]; + return { + ...state, + [categoryID]: categoryExpenses.filter(expense => { + return expense.id !== payload.id + }) + } default: return state; } From 18306391e3101edb12b750b41c2728c1cfe0ddea Mon Sep 17 00:00:00 2001 From: Bret Ldenburg Date: Thu, 31 Aug 2017 17:08:04 -0700 Subject: [PATCH 4/4] added some styling for my app --- .../category-item/_category-item.scss | 37 +++++++++ src/component/category-item/index.js | 7 +- src/component/dashboard-container/index.js | 4 +- src/component/expense-form/_expense-form.scss | 7 ++ src/component/expense-form/index.js | 1 + src/component/expense-item/_expense-item.scss | 30 ++++++++ src/component/expense-item/index.js | 7 +- src/index.html | 4 + src/main.js | 1 + src/style/base/_base.scss | 76 +++++++++++++++++++ src/style/base/_reset.scss | 48 ++++++++++++ src/style/layout/_content.scss | 5 ++ src/style/layout/_footer.scss | 5 ++ src/style/layout/_header.scss | 10 +++ src/style/lib/_vars.scss | 10 +++ src/style/main.scss | 6 ++ 16 files changed, 250 insertions(+), 8 deletions(-) create mode 100644 src/component/category-item/_category-item.scss create mode 100644 src/component/expense-form/_expense-form.scss create mode 100644 src/component/expense-item/_expense-item.scss create mode 100644 src/style/base/_base.scss create mode 100644 src/style/base/_reset.scss create mode 100644 src/style/layout/_content.scss create mode 100644 src/style/layout/_footer.scss create mode 100644 src/style/layout/_header.scss create mode 100644 src/style/lib/_vars.scss create mode 100644 src/style/main.scss diff --git a/src/component/category-item/_category-item.scss b/src/component/category-item/_category-item.scss new file mode 100644 index 0000000..5e26fe4 --- /dev/null +++ b/src/component/category-item/_category-item.scss @@ -0,0 +1,37 @@ +@import '../../style/lib/vars'; + +.category-item { + .category-container { + position: relative; + margin: $gutter-sm 0; + + .remove { + background: none; + color: red; + position: absolute; + top: 0; + padding: 0; + right: 0; + font-size: 2vw; + + &:hover { + text-shadow: 1px 1px 1px $black; + } + } + + button[type="submit"] { + background: green; + + &:hover { + background: #777; + } + } + + .category-content { + position: relative; + margin: $gutter-sm 0; + + } + } + +} diff --git a/src/component/category-item/index.js b/src/component/category-item/index.js index d1b0f8d..0b55a93 100644 --- a/src/component/category-item/index.js +++ b/src/component/category-item/index.js @@ -1,3 +1,4 @@ +import './_category-item.scss'; import React from 'react'; import {connect} from 'react-redux'; import CategoryForm from '../category-form'; @@ -16,12 +17,12 @@ class CategoryItem extends React.Component {

    {category.name}

    -

    {category.budget}

    - +

    {category.budget}

    +
    diff --git a/src/component/dashboard-container/index.js b/src/component/dashboard-container/index.js index 1d3dbea..51c5350 100644 --- a/src/component/dashboard-container/index.js +++ b/src/component/dashboard-container/index.js @@ -16,10 +16,10 @@ class DashboardContainer extends React.Component { render() { return (
    -

    Dashboard

    +

    Create Budget:

    diff --git a/src/component/expense-form/_expense-form.scss b/src/component/expense-form/_expense-form.scss new file mode 100644 index 0000000..8830e3e --- /dev/null +++ b/src/component/expense-form/_expense-form.scss @@ -0,0 +1,7 @@ +@import '../../style/lib/vars'; + +.card-form { + input[type="text"] { + border: solid .5vw $black; + } +} diff --git a/src/component/expense-form/index.js b/src/component/expense-form/index.js index 0160603..9140996 100644 --- a/src/component/expense-form/index.js +++ b/src/component/expense-form/index.js @@ -1,3 +1,4 @@ +import './_expense-form.scss'; import React from 'react'; import uuid from 'uuid'; diff --git a/src/component/expense-item/_expense-item.scss b/src/component/expense-item/_expense-item.scss new file mode 100644 index 0000000..d9c0688 --- /dev/null +++ b/src/component/expense-item/_expense-item.scss @@ -0,0 +1,30 @@ +@import '../../style/lib/vars'; +@import '../category-item/_category-item.scss'; + +.expense-item { + position: relative; + padding: $gutter-sm; + background: $btn-primary; + margin: $gutter-sm; + font-size: 2vw; + border: solid .25vw $white; + border-radius: $border-radius; + margin-left: 0; + margin-right: 0; + line-height: 3vw; + + p { + margin-bottom: $gutter-sm; + text-align: center; + color: $white; + } + + button { + color: $white; + + &.remove { + right: 1vw; + color: $white; + } + } +} diff --git a/src/component/expense-item/index.js b/src/component/expense-item/index.js index 6e34a04..c9702d1 100644 --- a/src/component/expense-item/index.js +++ b/src/component/expense-item/index.js @@ -1,3 +1,4 @@ +import './_expense-item.scss'; import React from 'react'; import {connect} from 'react-redux'; import ExpenseForm from '../expense-form'; @@ -11,9 +12,9 @@ class ExpenseItem extends React.Component {
  • -

    {expense.name}

    -

    {expense.price}

    - +

    {expense.name}

    +

    {expense.price}

    +
    Budget App +
    +

    Give Money To The Miami Dolphins

    +
    + diff --git a/src/main.js b/src/main.js index faafd74..4e78816 100644 --- a/src/main.js +++ b/src/main.js @@ -1,3 +1,4 @@ +import './style/main.scss'; import React from 'react'; import ReactDom from 'react-dom'; import App from './component/app'; diff --git a/src/style/base/_base.scss b/src/style/base/_base.scss new file mode 100644 index 0000000..f15110d --- /dev/null +++ b/src/style/base/_base.scss @@ -0,0 +1,76 @@ +body { + font-family: $font-primary; + background: $primary; +} + +a { + text-decoration: none; + color: $white; +} + +button { + border: none; + color: $white; + background: $secondary; + padding: $gutter-sm $gutter-lg; + border-radius: $border-radius; + cursor: pointer; + font-size: 2vw; + transition: $transition-primary; + + &:hover { + background: $btn-primary; + } + + &.remove { + background: none; + color: red; + position: absolute; + top: 0; + padding: 0; + right: 0; + font-size: 2.5vw; + } +} + +input[type="text"] { + border-radius: $border-radius; + box-sizing: border-box; + padding: $gutter-sm; + width: 100%; + font-size: 2vw; + margin-bottom: $gutter-sm; + border: none; + transition: $transition-primary; + + &:focus { + // background: + } +} + +input[type="number"] { + border-radius: $border-radius; + box-sizing: border-box; + padding: $gutter-sm; + width: 100%; + font-size: 2vw; + margin-bottom: $gutter-sm; + border: none; + transition: $transition-primary; + + &:focus { + // background: + } +} + +h1 { + text-align: center; +} + +h2 { + font-size: 3vw; + text-decoration: underline; + margin-bottom: $gutter-sm; + text-align: center; + color: $white; +} diff --git a/src/style/base/_reset.scss b/src/style/base/_reset.scss new file mode 100644 index 0000000..ed11813 --- /dev/null +++ b/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; +} diff --git a/src/style/layout/_content.scss b/src/style/layout/_content.scss new file mode 100644 index 0000000..f4a7b72 --- /dev/null +++ b/src/style/layout/_content.scss @@ -0,0 +1,5 @@ +main { + margin-top: 7.5vw; + padding: $gutter-sm; + min-height: 700px; +} diff --git a/src/style/layout/_footer.scss b/src/style/layout/_footer.scss new file mode 100644 index 0000000..b1d7d0c --- /dev/null +++ b/src/style/layout/_footer.scss @@ -0,0 +1,5 @@ +footer { + background: $white * 2; + height: 10vw; + width: 100%; +} diff --git a/src/style/layout/_header.scss b/src/style/layout/_header.scss new file mode 100644 index 0000000..b581640 --- /dev/null +++ b/src/style/layout/_header.scss @@ -0,0 +1,10 @@ +header { + position: fixed; + top: 0; + padding: $gutter-sm; + font-size: 3vw; + width: 100%; + background: $secondary; + color: $white; + z-index: 9; +} diff --git a/src/style/lib/_vars.scss b/src/style/lib/_vars.scss new file mode 100644 index 0000000..298e90e --- /dev/null +++ b/src/style/lib/_vars.scss @@ -0,0 +1,10 @@ +$primary: #008E97; +$secondary: #F58220; +$btn-primary: #005778; +$font-primary: white; +$transition-primary: #005778; +$gutter-lg: 5%; +$gutter-sm: $gutter-lg / 2; +$border-radius: 3px; +$black: #111; +$white: #fff; diff --git a/src/style/main.scss b/src/style/main.scss new file mode 100644 index 0000000..5019591 --- /dev/null +++ b/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';