├── .babelrc ├── .eslintrc ├── .gitignore ├── Readme.md ├── client ├── actions │ └── index.js ├── components │ ├── Grid │ │ ├── Body.js │ │ ├── Cell.js │ │ ├── Footer.js │ │ ├── Grid.js │ │ ├── Header.js │ │ ├── Row.js │ │ ├── index.js │ │ └── style.css │ └── Header │ │ └── index.js ├── containers │ ├── App │ │ ├── index.js │ │ └── style.css │ ├── TransactionForm │ │ └── index.js │ └── TransactionSummary │ │ └── index.js ├── data │ └── transactions.json ├── index.html ├── index.js ├── reducers │ ├── defaults.js │ ├── index.js │ └── transactions.js └── store │ └── index.js ├── package.json └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "react"], 3 | "plugins": ["transform-runtime", "transform-class-properties"] 4 | } 5 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | extends: 2 | eslint-config-airbnb 3 | 4 | parser: 5 | babel-eslint 6 | 7 | settings: 8 | ecmascript: 6 9 | 10 | ecmaFeatures: 11 | jsx: true 12 | modules: true 13 | destructuring: true 14 | classes: true 15 | forOf: true 16 | blockBindings: true 17 | arrowFunctions: true 18 | 19 | env: 20 | browser: true 21 | 22 | rules: 23 | indent: 2 24 | func-style: 0 25 | func-names: 0 26 | comma-dangle: 0 27 | no-console: 0 28 | no-param-reassign: 0 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_STORE 2 | node_modules 3 | static 4 | .module-cache 5 | *.log* 6 | 7 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | ![No longer maintained](https://img.shields.io/badge/Maintenance-OFF-red.svg) 2 | ### [DEPRECATED] This repository is no longer maintained 3 | > While this project is fully functional, the dependencies are no longer up to date. You are still welcome to explore, learn, and use the code provided here. 4 | > 5 | > Modus is dedicated to supporting the community with innovative ideas, best-practice patterns, and inspiring open source solutions. Check out the latest [Modus Labs](https://labs.moduscreate.com?utm_source=github&utm_medium=readme&utm_campaign=deprecated) projects. 6 | 7 | [![Modus Labs](https://res.cloudinary.com/modus-labs/image/upload/h_80/v1531492623/labs/logo-black.png)](https://labs.moduscreate.com?utm_source=github&utm_medium=readme&utm_campaign=deprecated) 8 | 9 | --- 10 | 11 | # Budgeting - React + Redux Sample App 12 | 13 | Sample app demonstrating the power and simplicity of React, Redux, and Webpack. 14 | 15 | ## Contains 16 | 17 | - [x] [Webpack](https://webpack.github.io) 18 | - [x] [React](https://facebook.github.io/react/) 19 | - [x] [Redux](https://github.com/rackt/redux) 20 | - [x] [Babel](https://babeljs.io/) 21 | 22 | ## Setup 23 | 24 | ``` 25 | $ npm install 26 | ``` 27 | 28 | ## Running 29 | 30 | ``` 31 | $ npm start 32 | ``` 33 | 34 | ## Build 35 | 36 | ``` 37 | $ npm run build 38 | ``` 39 | 40 | # License 41 | 42 | MIT 43 | -------------------------------------------------------------------------------- /client/actions/index.js: -------------------------------------------------------------------------------- 1 | export const ADD_TRANSACTION = 'ADD_TRANSACTION'; 2 | export const DELETE_TRANSACTION = 'DELETE_TRANSACTION'; 3 | export const GET_TRANSACTION_GRID_FIELDS = 'GET_TRANSACTION_GRID_FIELDS'; 4 | export const REQUEST_SUM = 'REQUEST_SUM'; 5 | 6 | function createTransaction(transaction) { 7 | return { 8 | type: ADD_TRANSACTION, 9 | transaction 10 | }; 11 | } 12 | 13 | export function deleteTransaction(id) { 14 | return { 15 | type: ADD_TRANSACTION, 16 | id 17 | }; 18 | } 19 | 20 | export function requestSum(data) { 21 | return { 22 | type: REQUEST_SUM, 23 | data 24 | }; 25 | } 26 | 27 | export function addTransaction(transaction) { 28 | return (dispatch, getState) => { 29 | const addedResult = dispatch(createTransaction(transaction)); 30 | dispatch(requestSum(getState().transactions.transactions)); 31 | return addedResult; 32 | }; 33 | } 34 | -------------------------------------------------------------------------------- /client/components/Grid/Body.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default ({ children })=> { 4 | return ( 5 | {children} 6 | ); 7 | }; 8 | -------------------------------------------------------------------------------- /client/components/Grid/Cell.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | 3 | const Cell = ({ text, className, children }, { header })=> { 4 | if (header === true) { 5 | return ( 6 | {text} 7 | ); 8 | } 9 | 10 | return ( 11 | {text || children} 12 | ); 13 | }; 14 | 15 | Cell.contextTypes = { 16 | header: PropTypes.bool 17 | }; 18 | 19 | export default Cell; 20 | -------------------------------------------------------------------------------- /client/components/Grid/Footer.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default ({children})=> { 4 | return ( 5 | {children} 6 | ); 7 | }; 8 | -------------------------------------------------------------------------------- /client/components/Grid/Grid.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import Header from './Header'; 3 | import Body from './Body'; 4 | import Row from './Row'; 5 | import Cell from './Cell'; 6 | import './style.css'; 7 | 8 | const { string, shape, arrayOf, object, node } = PropTypes; 9 | 10 | function buildRow(fields, row, rowIndex) { 11 | return ( 12 | 13 | { 14 | fields.map((field, cellIndex) => ) 19 | } 20 | 21 | ); 22 | } 23 | 24 | function buildBody(fields, data) { 25 | return ( 26 | 27 | { 28 | data.map((row, index) => buildRow(fields, row, index)) 29 | } 30 | 31 | ); 32 | } 33 | 34 | export default class Grid extends Component { 35 | static propTypes = { 36 | fields: arrayOf(shape({ 37 | name: string, 38 | mapping: string, 39 | className: string 40 | })).isRequired, 41 | data: arrayOf(object), 42 | children: node 43 | }; 44 | 45 | render() { 46 | const { fields, data, children } = this.props; 47 | return ( 48 | 49 |
50 | 51 | { 52 | fields.map((field, index) => { 53 | return ( 54 | 59 | ); 60 | }) 61 | } 62 | 63 |
64 | { buildBody(fields, data) } 65 | {children} 66 |
67 | ); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /client/components/Grid/Header.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | 3 | export default class Header extends Component { 4 | static propTypes = { 5 | children: PropTypes.node.isRequired 6 | }; 7 | 8 | static childContextTypes = { 9 | header: PropTypes.bool 10 | }; 11 | 12 | getChildContext() { 13 | return { 14 | header: true 15 | }; 16 | } 17 | 18 | render() { 19 | const { children } = this.props; 20 | return ( 21 | {children} 22 | ); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /client/components/Grid/Row.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default ({children})=> { 4 | return ( 5 | {children} 6 | ); 7 | }; 8 | -------------------------------------------------------------------------------- /client/components/Grid/index.js: -------------------------------------------------------------------------------- 1 | import Grid from './Grid'; 2 | import Header from './Header'; 3 | import Footer from './Footer'; 4 | import Body from './Body'; 5 | import Row from './Row'; 6 | import Cell from './Cell'; 7 | 8 | export { Grid }; 9 | export { Header }; 10 | export { Body }; 11 | export { Row }; 12 | export { Cell }; 13 | 14 | export default Object.assign(Grid, { 15 | Header, Footer, Body, Row, Cell 16 | }); 17 | -------------------------------------------------------------------------------- /client/components/Grid/style.css: -------------------------------------------------------------------------------- 1 | table { 2 | border-collapse: collapse; 3 | padding: 3px; 4 | user-select: none; 5 | -webkit-user-select: none; 6 | cursor: default; 7 | } 8 | 9 | td { 10 | min-width: 150px; 11 | } 12 | 13 | th { 14 | text-align: left; 15 | border-bottom: 1px solid #ececec; 16 | } 17 | 18 | td, th { 19 | padding: 3px; 20 | } 21 | 22 | td.flex { 23 | width: 100%; 24 | } 25 | 26 | td.align-right, th.align-right { 27 | text-align: right; 28 | } 29 | 30 | td:last-child, th.last-child { 31 | background-color: rgba(210,210,210,0.2); 32 | } 33 | 34 | tbody > tr:hover { 35 | background-color: rgba(150,190,190,0.2); 36 | } 37 | 38 | tbody tr:last-child td { 39 | border-bottom: 1px solid #ececec; 40 | } 41 | 42 | tfoot input { 43 | width: 98%; 44 | } 45 | 46 | tfoot { 47 | font-weight: bold; 48 | } 49 | -------------------------------------------------------------------------------- /client/components/Header/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default ()=> { 4 | return ( 5 |
6 |

Budget

7 |
8 | ); 9 | }; 10 | -------------------------------------------------------------------------------- /client/containers/App/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import { bindActionCreators } from 'redux'; 3 | import { connect } from 'react-redux'; 4 | 5 | import Header from 'components/Header'; 6 | import Grid from 'components/Grid'; 7 | import TransactionForm from 'containers/TransactionForm'; 8 | import TransactionSummary from 'containers/TransactionSummary'; 9 | import * as AppActions from 'actions'; 10 | import './style.css'; 11 | 12 | class App extends Component { 13 | static propTypes = { 14 | transactions: PropTypes.array, 15 | summary: PropTypes.object, 16 | gridFields: PropTypes.array, 17 | actions: PropTypes.object 18 | }; 19 | 20 | componentWillMount() { 21 | const { transactions, actions } = this.props; 22 | actions.requestSum(transactions); 23 | } 24 | 25 | render() { 26 | const { 27 | transactions, 28 | gridFields, 29 | summary, 30 | actions 31 | } = this.props; 32 | 33 | return ( 34 |
35 |
36 | 37 | 38 | 39 | 40 |
41 | ); 42 | } 43 | } 44 | 45 | function mapStateToProps(state) { 46 | const { transactions } = state; 47 | return { 48 | transactions: transactions.transactions, 49 | summary: transactions.summary, 50 | gridFields: transactions.transactionsGrid 51 | }; 52 | } 53 | 54 | function mapDispatchToProps(dispatch) { 55 | return { 56 | actions: bindActionCreators(AppActions, dispatch) 57 | }; 58 | } 59 | 60 | export default connect( 61 | mapStateToProps, 62 | mapDispatchToProps 63 | )(App); 64 | -------------------------------------------------------------------------------- /client/containers/App/style.css: -------------------------------------------------------------------------------- 1 | 2 | html, 3 | body { 4 | margin: 0; 5 | padding: 0; 6 | } 7 | 8 | button { 9 | margin: 0; 10 | padding: 0; 11 | border: 0; 12 | background: none; 13 | font-size: 100%; 14 | vertical-align: baseline; 15 | font-family: inherit; 16 | font-weight: inherit; 17 | color: inherit; 18 | appearance: none; 19 | font-smoothing: antialiased; 20 | } 21 | 22 | body { 23 | font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif; 24 | line-height: 1.4em; 25 | background: #f5f5f5; 26 | color: #4d4d4d; 27 | min-width: 230px; 28 | max-width: 550px; 29 | margin: 0 auto; 30 | -webkit-font-smoothing: antialiased; 31 | -moz-font-smoothing: antialiased; 32 | -ms-font-smoothing: antialiased; 33 | font-smoothing: antialiased; 34 | font-weight: 300; 35 | } 36 | 37 | .viewport { 38 | background: #fff; 39 | margin: 200px 0 40px 0; 40 | position: relative; 41 | box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 42 | 0 25px 50px 0 rgba(0, 0, 0, 0.1); 43 | } 44 | 45 | .viewport input::-webkit-input-placeholder { 46 | font-style: italic; 47 | font-weight: 300; 48 | color: #e6e6e6; 49 | } 50 | 51 | .viewport input::-moz-placeholder { 52 | font-style: italic; 53 | font-weight: 300; 54 | color: #e6e6e6; 55 | } 56 | 57 | .viewport input::input-placeholder { 58 | font-style: italic; 59 | font-weight: 300; 60 | color: #e6e6e6; 61 | } 62 | 63 | .viewport h1 { 64 | position: absolute; 65 | top: -155px; 66 | width: 100%; 67 | font-size: 100px; 68 | font-weight: 100; 69 | text-align: center; 70 | color: rgba(175, 47, 47, 0.15); 71 | user-select: none; 72 | -webkit-user-select: none; 73 | cursor: default; 74 | -webkit-text-rendering: optimizeLegibility; 75 | -moz-text-rendering: optimizeLegibility; 76 | -ms-text-rendering: optimizeLegibility; 77 | text-rendering: optimizeLegibility; 78 | } 79 | -------------------------------------------------------------------------------- /client/containers/TransactionForm/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import Grid from 'components/Grid'; 3 | 4 | export default class TransactionForm extends Component { 5 | static propTypes = { 6 | action: PropTypes.func 7 | }; 8 | 9 | onFieldKeyUp(e) { 10 | if (e.keyCode === 13) { 11 | this.submitForm(); 12 | } 13 | } 14 | 15 | submitForm() { 16 | const { valueField, descField } = this.refs; 17 | const { action } = this.props; 18 | const value = parseFloat(valueField.value, 10); 19 | const description = descField.value; 20 | 21 | if (value === 0 || isNaN(value) || description.length === 0) { 22 | return; 23 | } 24 | 25 | action({ value, description }); 26 | 27 | valueField.value = descField.value = ''; 28 | descField.focus(); 29 | } 30 | 31 | render() { 32 | return ( 33 | 34 | 35 | 36 | 42 | 43 | 44 | 52 | 53 | 54 | 55 | ); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /client/containers/TransactionSummary/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import Grid from 'components/Grid'; 3 | 4 | const { string, shape, arrayOf, object } = PropTypes; 5 | 6 | export default class TransactionSummary extends Component { 7 | static propTypes = { 8 | fields: arrayOf(shape({ 9 | mapping: string, 10 | className: string 11 | })).isRequired, 12 | data: object 13 | }; 14 | 15 | render() { 16 | const { fields, data } = this.props; 17 | return ( 18 | 19 | 20 | { 21 | fields.map((field, index) => { 22 | return ( 23 | 28 | ); 29 | }) 30 | } 31 | 32 | 33 | ); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /client/data/transactions.json: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ModusCreateOrg/budgeting-sample-app/760ffcc2996474753fc3041e7a8783abd79ff22e/client/data/transactions.json -------------------------------------------------------------------------------- /client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Budgeting - React + Redux Sample 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /client/index.js: -------------------------------------------------------------------------------- 1 | import { Provider } from 'react-redux'; 2 | import ReactDOM from 'react-dom'; 3 | import React from 'react'; 4 | 5 | import App from 'containers/App'; 6 | import store from 'store'; 7 | 8 | ReactDOM.render( 9 | 10 | 11 | , 12 | document.getElementById('root') 13 | ); 14 | -------------------------------------------------------------------------------- /client/reducers/defaults.js: -------------------------------------------------------------------------------- 1 | export const defaultTransactions = [ 2 | { 3 | id: 1, 4 | description: 'Trader Joe\'s food', 5 | value: -123.34 6 | }, 7 | { 8 | id: 2, 9 | description: 'Gas', 10 | value: -64.73 11 | }, 12 | { 13 | id: 3, 14 | description: 'Ebay sale - guitar', 15 | value: 102.00 16 | } 17 | ]; 18 | export const defaultTransactionGridFields = [ 19 | { 20 | name: 'Description', 21 | className: 'flex', 22 | mapping: 'description' 23 | }, 24 | { 25 | name: 'Value', 26 | className: 'align-right', 27 | mapping: 'value' 28 | } 29 | ]; 30 | 31 | export const defaultSummary = { 32 | description: 'Balance' 33 | }; 34 | -------------------------------------------------------------------------------- /client/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { routeReducer as routing } from 'react-router-redux'; 2 | import { combineReducers } from 'redux'; 3 | 4 | import transactions from './transactions'; 5 | 6 | /** 7 | * Routing to be implemented 8 | */ 9 | export default combineReducers({ 10 | routing, 11 | transactions 12 | }); 13 | -------------------------------------------------------------------------------- /client/reducers/transactions.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | 3 | import { 4 | ADD_TRANSACTION, 5 | DELETE_TRANSACTION, 6 | GET_TRANSACTION_GRID_FIELDS, 7 | REQUEST_SUM, 8 | } from 'actions'; 9 | 10 | import { 11 | defaultTransactions, 12 | defaultTransactionGridFields, 13 | defaultSummary 14 | } from './defaults'; 15 | 16 | /** 17 | * Add a new transaction. 18 | * This is a helper function for the transactions reducer 19 | * @param {Object} state 20 | * @param {Object} action 21 | */ 22 | function addTransaction(state, action) { 23 | const { description, value } = action.transaction; 24 | const newState = [...state, { 25 | id: state.reduce((maxId, todo) => Math.max(todo.id, maxId), -1) + 1, 26 | description: description, 27 | value: value 28 | }]; 29 | return newState; 30 | } 31 | 32 | /** 33 | * Main transactions reducer 34 | * @param {Object} state Current state 35 | * @param {Object} action Dispatched action 36 | * @return {Object} Default state 37 | */ 38 | function transactions(state=defaultTransactions, action) { 39 | let newState; 40 | switch (action.type) { 41 | case ADD_TRANSACTION: 42 | return addTransaction(state, action); 43 | case DELETE_TRANSACTION: 44 | newState = state.filter(todo => todo.id !== action.id ); 45 | return newState; 46 | default: 47 | return state; 48 | } 49 | } 50 | 51 | /** 52 | * Reserved for future use. 53 | * Intended for dynamic grid column setup 54 | * @param {Object} state Current state 55 | * @param {Object} action Dispatched action 56 | * @return {Object} Default state 57 | */ 58 | function transactionsGrid(state=defaultTransactionGridFields, action) { 59 | switch (action.type) { 60 | case GET_TRANSACTION_GRID_FIELDS: 61 | return state; 62 | default: 63 | return state; 64 | } 65 | } 66 | 67 | /** 68 | * Summary calculation 69 | * @param {Object} state Current state 70 | * @param {Object} action Dispatched action 71 | * @return {Object} Default state 72 | */ 73 | function summary(state=defaultSummary, action) { 74 | switch (action.type) { 75 | case REQUEST_SUM: 76 | let sum = action.data.reduce((prev, current) => { 77 | return {value: prev.value + current.value }; 78 | }); 79 | 80 | sum = {value: Math.round(sum.value * 100) / 100}; 81 | 82 | // Return ES2015 friendly 83 | // or stage-0 {...state, ...sum} 84 | return Object.assign({}, state, sum); 85 | default: 86 | return state; 87 | } 88 | } 89 | 90 | export default combineReducers({ 91 | transactionsGrid, 92 | transactions, 93 | summary 94 | }); 95 | -------------------------------------------------------------------------------- /client/store/index.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware } from 'redux'; 2 | import thunk from 'redux-thunk'; 3 | 4 | import rootReducer from 'reducers'; 5 | 6 | const createStoreWithMiddleware = applyMiddleware(thunk)(createStore); 7 | const store = createStoreWithMiddleware(rootReducer); 8 | 9 | export default store; 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redux-clean-training", 3 | "version": "1.0.0", 4 | "private": true, 5 | "description": "Clean Redux setup for training", 6 | "main": "index.js", 7 | "scripts": { 8 | "test": "echo \"Error: no test specified\" && exit 1", 9 | "start": "webpack-dev-server --history-api-fallback --hot --inline --progress --colors --port 3000", 10 | "build": "NODE_ENV=production webpack --progress --colors" 11 | }, 12 | "license": "MIT", 13 | "devDependencies": { 14 | "babel-core": "^6.3.26", 15 | "babel-loader": "^6.2.0", 16 | "babel-plugin-transform-class-properties": "^6.16.0", 17 | "babel-plugin-transform-runtime": "^6.3.13", 18 | "babel-preset-es2015": "^6.3.13", 19 | "babel-preset-react": "^6.3.13", 20 | "css-loader": "^0.25.0", 21 | "file-loader": "^0.8.4", 22 | "style-loader": "^0.13.1", 23 | "webpack": "^1.12.2", 24 | "webpack-dev-server": "^1.12.0", 25 | "webpack-hot-middleware": "^2.2.0" 26 | }, 27 | "dependencies": { 28 | "babel-runtime": "^6.3.19", 29 | "es6-promise": "^3.0.2", 30 | "isomorphic-fetch": "^2.2.1", 31 | "react": "^0.14.0", 32 | "react-dom": "^0.14.0", 33 | "react-hot-loader": "^1.3.0", 34 | "react-redux": "^4.0.6", 35 | "react-router-redux": "^4.0.6", 36 | "redux": "^3.0.2", 37 | "redux-thunk": "^1.0.3" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const path = require('path'); 3 | 4 | const nodeEnv = process.env.NODE_ENV || 'development'; 5 | const isProd = nodeEnv === 'production'; 6 | 7 | module.exports = { 8 | devtool: isProd ? 'hidden-source-map' : 'cheap-eval-source-map', 9 | context: path.join(__dirname, './client'), 10 | entry: { 11 | jsx: './index.js', 12 | html: './index.html', 13 | vendor: ['react'] 14 | }, 15 | output: { 16 | path: path.join(__dirname, './static'), 17 | filename: 'bundle.js', 18 | }, 19 | module: { 20 | loaders: [ 21 | { 22 | test: /\.html$/, 23 | loader: 'file?name=[name].[ext]' 24 | }, 25 | { 26 | test: /\.css$/, 27 | loaders: [ 28 | 'style-loader', 29 | 'css-loader' 30 | ] 31 | }, 32 | { 33 | test: /\.(js|jsx)$/, 34 | exclude: /node_modules/, 35 | loaders: [ 36 | 'react-hot', 37 | 'babel-loader' 38 | ] 39 | }, 40 | ], 41 | }, 42 | resolve: { 43 | extensions: ['', '.js', '.jsx'], 44 | root: [ 45 | path.resolve('./client') 46 | ] 47 | }, 48 | plugins: [ 49 | new webpack.optimize.CommonsChunkPlugin('vendor', 'vendor.bundle.js'), 50 | new webpack.optimize.UglifyJsPlugin({ 51 | compress: { 52 | warnings: false 53 | }, 54 | sourceMap: false 55 | }), 56 | new webpack.DefinePlugin({ 57 | 'process.env': { NODE_ENV: JSON.stringify(nodeEnv) } 58 | }) 59 | ], 60 | devServer: { 61 | contentBase: './client', 62 | hot: true 63 | } 64 | }; 65 | --------------------------------------------------------------------------------