├── .babelrc ├── .eslintignore ├── .eslintrc ├── .gitignore ├── README.md ├── circle.yml ├── index.html ├── mocha.opts ├── package.json ├── src ├── assets │ └── background.jpg ├── components │ ├── Counter.js │ ├── Loading.js │ ├── Nav.js │ ├── Status.js │ └── __test__ │ │ └── Counter.test.js ├── containers │ ├── AboutPage.js │ ├── App.js │ ├── CounterPage.js │ └── DevTools.js ├── index.js ├── middleware │ └── promiseMiddleware.js ├── reducers │ ├── __tests__ │ │ ├── counter.test.js │ │ └── loading.test.js │ ├── counter.js │ ├── index.js │ └── loading.js ├── sagas │ ├── __tests__ │ │ └── delay.test.js │ └── delay.js ├── store │ ├── configureStore.js │ ├── history.js │ ├── logger.js │ └── router.js ├── styles │ ├── base.css │ └── styles.css └── utils │ ├── immutableToJS.js │ └── isPromise.js └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "react", "stage-0"] 3 | } -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | bin/** 2 | dist/** 3 | build/** 4 | tmp/** 5 | coverage/** 6 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", // https://github.com/babel/babel-eslint 3 | "plugins": [ 4 | "react" // https://github.com/yannickcr/eslint-plugin-react 5 | ], 6 | "env": { // http://eslint.org/docs/user-guide/configuring.html#specifying-environments 7 | "browser": true, // browser global variables 8 | "node": true // Node.js global variables and Node.js-specific rules 9 | }, 10 | "ecmaFeatures": { 11 | "arrowFunctions": true, 12 | "blockBindings": true, 13 | "classes": true, 14 | "defaultParams": true, 15 | "destructuring": true, 16 | "forOf": true, 17 | "generators": false, 18 | "modules": true, 19 | "objectLiteralComputedProperties": true, 20 | "objectLiteralDuplicateProperties": false, 21 | "objectLiteralShorthandMethods": true, 22 | "objectLiteralShorthandProperties": true, 23 | "spread": true, 24 | "superInFunctions": true, 25 | "templateStrings": true, 26 | "jsx": true 27 | }, 28 | "rules": { 29 | /** 30 | * Strict mode 31 | */ 32 | "strict": [2, "never"], // http://eslint.org/docs/rules/strict 33 | 34 | /** 35 | * ES6 36 | */ 37 | "no-var": 2, // http://eslint.org/docs/rules/no-var 38 | "prefer-const": 2, // http://eslint.org/docs/rules/prefer-const 39 | 40 | /** 41 | * Variables 42 | */ 43 | "no-shadow": 2, // http://eslint.org/docs/rules/no-shadow 44 | "no-shadow-restricted-names": 2, // http://eslint.org/docs/rules/no-shadow-restricted-names 45 | "no-unused-vars": [2, { // http://eslint.org/docs/rules/no-unused-vars 46 | "vars": "local", 47 | "args": "after-used" 48 | }], 49 | "no-use-before-define": 0, // http://eslint.org/docs/rules/no-use-before-define 50 | 51 | /** 52 | * Possible errors 53 | */ 54 | "comma-dangle": [2, "always-multiline"], // http://eslint.org/docs/rules/comma-dangle 55 | "no-cond-assign": [2, "always"], // http://eslint.org/docs/rules/no-cond-assign 56 | "no-console": 1, // http://eslint.org/docs/rules/no-console 57 | "no-debugger": 1, // http://eslint.org/docs/rules/no-debugger 58 | "no-alert": 1, // http://eslint.org/docs/rules/no-alert 59 | "no-constant-condition": 1, // http://eslint.org/docs/rules/no-constant-condition 60 | "no-dupe-keys": 2, // http://eslint.org/docs/rules/no-dupe-keys 61 | "no-duplicate-case": 2, // http://eslint.org/docs/rules/no-duplicate-case 62 | "no-empty": 2, // http://eslint.org/docs/rules/no-empty 63 | "no-ex-assign": 2, // http://eslint.org/docs/rules/no-ex-assign 64 | "no-extra-boolean-cast": 0, // http://eslint.org/docs/rules/no-extra-boolean-cast 65 | "no-extra-semi": 2, // http://eslint.org/docs/rules/no-extra-semi 66 | "no-func-assign": 2, // http://eslint.org/docs/rules/no-func-assign 67 | "no-inner-declarations": 2, // http://eslint.org/docs/rules/no-inner-declarations 68 | "no-invalid-regexp": 2, // http://eslint.org/docs/rules/no-invalid-regexp 69 | "no-irregular-whitespace": 2, // http://eslint.org/docs/rules/no-irregular-whitespace 70 | "no-obj-calls": 2, // http://eslint.org/docs/rules/no-obj-calls 71 | "no-sparse-arrays": 2, // http://eslint.org/docs/rules/no-sparse-arrays 72 | "no-unreachable": 2, // http://eslint.org/docs/rules/no-unreachable 73 | "use-isnan": 2, // http://eslint.org/docs/rules/use-isnan 74 | "block-scoped-var": 0, // http://eslint.org/docs/rules/block-scoped-var 75 | 76 | /** 77 | * Best practices 78 | */ 79 | "consistent-return": 2, // http://eslint.org/docs/rules/consistent-return 80 | "curly": [2, "multi-line"], // http://eslint.org/docs/rules/curly 81 | "default-case": 2, // http://eslint.org/docs/rules/default-case 82 | "dot-notation": [2, { // http://eslint.org/docs/rules/dot-notation 83 | "allowKeywords": true 84 | }], 85 | "eqeqeq": 2, // http://eslint.org/docs/rules/eqeqeq 86 | "guard-for-in": 0, // http://eslint.org/docs/rules/guard-for-in 87 | "no-caller": 2, // http://eslint.org/docs/rules/no-caller 88 | "no-else-return": 2, // http://eslint.org/docs/rules/no-else-return 89 | "no-eq-null": 2, // http://eslint.org/docs/rules/no-eq-null 90 | "no-eval": 2, // http://eslint.org/docs/rules/no-eval 91 | "no-extend-native": 2, // http://eslint.org/docs/rules/no-extend-native 92 | "no-extra-bind": 2, // http://eslint.org/docs/rules/no-extra-bind 93 | "no-fallthrough": 2, // http://eslint.org/docs/rules/no-fallthrough 94 | "no-floating-decimal": 2, // http://eslint.org/docs/rules/no-floating-decimal 95 | "no-implied-eval": 2, // http://eslint.org/docs/rules/no-implied-eval 96 | "no-lone-blocks": 2, // http://eslint.org/docs/rules/no-lone-blocks 97 | "no-loop-func": 2, // http://eslint.org/docs/rules/no-loop-func 98 | "no-multi-str": 2, // http://eslint.org/docs/rules/no-multi-str 99 | "no-native-reassign": 2, // http://eslint.org/docs/rules/no-native-reassign 100 | "no-new": 2, // http://eslint.org/docs/rules/no-new 101 | "no-new-func": 2, // http://eslint.org/docs/rules/no-new-func 102 | "no-new-wrappers": 2, // http://eslint.org/docs/rules/no-new-wrappers 103 | "no-octal": 2, // http://eslint.org/docs/rules/no-octal 104 | "no-octal-escape": 2, // http://eslint.org/docs/rules/no-octal-escape 105 | "no-param-reassign": 2, // http://eslint.org/docs/rules/no-param-reassign 106 | "no-proto": 2, // http://eslint.org/docs/rules/no-proto 107 | "no-redeclare": 2, // http://eslint.org/docs/rules/no-redeclare 108 | "no-return-assign": 2, // http://eslint.org/docs/rules/no-return-assign 109 | "no-script-url": 2, // http://eslint.org/docs/rules/no-script-url 110 | "no-self-compare": 2, // http://eslint.org/docs/rules/no-self-compare 111 | "no-sequences": 2, // http://eslint.org/docs/rules/no-sequences 112 | "no-throw-literal": 2, // http://eslint.org/docs/rules/no-throw-literal 113 | "no-with": 2, // http://eslint.org/docs/rules/no-with 114 | "radix": 2, // http://eslint.org/docs/rules/radix 115 | "vars-on-top": 2, // http://eslint.org/docs/rules/vars-on-top 116 | "wrap-iife": [2, "any"], // http://eslint.org/docs/rules/wrap-iife 117 | "yoda": 2, // http://eslint.org/docs/rules/yoda 118 | 119 | /** 120 | * Style 121 | */ 122 | "indent": [2, 2], // http://eslint.org/docs/rules/indent 123 | "brace-style": [2, // http://eslint.org/docs/rules/brace-style 124 | "1tbs", { 125 | "allowSingleLine": true 126 | }], 127 | "quotes": [ 128 | 2, "single", "avoid-escape" // http://eslint.org/docs/rules/quotes 129 | ], 130 | "camelcase": [2, { // http://eslint.org/docs/rules/camelcase 131 | "properties": "never" 132 | }], 133 | "comma-spacing": [2, { // http://eslint.org/docs/rules/comma-spacing 134 | "before": false, 135 | "after": true 136 | }], 137 | "comma-style": [2, "last"], // http://eslint.org/docs/rules/comma-style 138 | "eol-last": 2, // http://eslint.org/docs/rules/eol-last 139 | "func-names": 1, // http://eslint.org/docs/rules/func-names 140 | "key-spacing": [2, { // http://eslint.org/docs/rules/key-spacing 141 | "beforeColon": false, 142 | "afterColon": true 143 | }], 144 | "new-cap": [2, { // http://eslint.org/docs/rules/new-cap 145 | "newIsCap": true, 146 | "capIsNew": false 147 | }], 148 | "no-multiple-empty-lines": [2, { // http://eslint.org/docs/rules/no-multiple-empty-lines 149 | "max": 2 150 | }], 151 | "no-nested-ternary": 2, // http://eslint.org/docs/rules/no-nested-ternary 152 | "no-new-object": 2, // http://eslint.org/docs/rules/no-new-object 153 | "no-spaced-func": 2, // http://eslint.org/docs/rules/no-spaced-func 154 | "no-trailing-spaces": 2, // http://eslint.org/docs/rules/no-trailing-spaces 155 | "no-extra-parens": [2, "functions"], // http://eslint.org/docs/rules/no-extra-parens 156 | "no-underscore-dangle": 0, // http://eslint.org/docs/rules/no-underscore-dangle 157 | "one-var": [2, "never"], // http://eslint.org/docs/rules/one-var 158 | "padded-blocks": [2, "never"], // http://eslint.org/docs/rules/padded-blocks 159 | "semi": [2, "always"], // http://eslint.org/docs/rules/semi 160 | "semi-spacing": [2, { // http://eslint.org/docs/rules/semi-spacing 161 | "before": false, 162 | "after": true 163 | }], 164 | "space-after-keywords": 2, // http://eslint.org/docs/rules/space-after-keywords 165 | "space-before-blocks": 2, // http://eslint.org/docs/rules/space-before-blocks 166 | "space-before-function-paren": [2, "never"], // http://eslint.org/docs/rules/space-before-function-paren 167 | "space-infix-ops": 2, // http://eslint.org/docs/rules/space-infix-ops 168 | "space-return-throw-case": 2, // http://eslint.org/docs/rules/space-return-throw-case 169 | "spaced-comment": 2, // http://eslint.org/docs/rules/spaced-comment 170 | 171 | /** 172 | * JSX style 173 | */ 174 | "react/display-name": 0, // https://github.com/yannickcr/eslint-plugin-react/blob/master/docs/rules/display-name.md 175 | "react/jsx-boolean-value": 2, // https://github.com/yannickcr/eslint-plugin-react/blob/master/docs/rules/jsx-boolean-value.md 176 | "react/jsx-quotes": 0, // deprecated 177 | "jsx-quotes": [2, "prefer-double"], // https://github.com/yannickcr/eslint-plugin-react/blob/master/docs/rules/jsx-quotes.md 178 | "react/jsx-no-undef": 2, // https://github.com/yannickcr/eslint-plugin-react/blob/master/docs/rules/jsx-no-undef.md 179 | "react/jsx-sort-props": 0, // https://github.com/yannickcr/eslint-plugin-react/blob/master/docs/rules/jsx-sort-props.md 180 | "react/jsx-sort-prop-types": 0, // https://github.com/yannickcr/eslint-plugin-react/blob/master/docs/rules/jsx-sort-prop-types.md 181 | "react/jsx-uses-react": 2, // https://github.com/yannickcr/eslint-plugin-react/blob/master/docs/rules/jsx-uses-react.md 182 | "react/jsx-uses-vars": 2, // https://github.com/yannickcr/eslint-plugin-react/blob/master/docs/rules/jsx-uses-vars.md 183 | "react/no-did-mount-set-state": [2, "allow-in-func"], // https://github.com/yannickcr/eslint-plugin-react/blob/master/docs/rules/no-did-mount-set-state.md 184 | "react/no-did-update-set-state": 2, // https://github.com/yannickcr/eslint-plugin-react/blob/master/docs/rules/no-did-update-set-state.md 185 | "react/no-multi-comp": 2, // https://github.com/yannickcr/eslint-plugin-react/blob/master/docs/rules/no-multi-comp.md 186 | "react/no-unknown-property": 2, // https://github.com/yannickcr/eslint-plugin-react/blob/master/docs/rules/no-unknown-property.md 187 | "react/prop-types": 2, // https://github.com/yannickcr/eslint-plugin-react/blob/master/docs/rules/prop-types.md 188 | "react/react-in-jsx-scope": 2, // https://github.com/yannickcr/eslint-plugin-react/blob/master/docs/rules/react-in-jsx-scope.md 189 | "react/self-closing-comp": 2, // https://github.com/yannickcr/eslint-plugin-react/blob/master/docs/rules/self-closing-comp.md 190 | "react/wrap-multilines": 2, // https://github.com/yannickcr/eslint-plugin-react/blob/master/docs/rules/wrap-multilines.md 191 | "react/sort-comp": [2, { // https://github.com/yannickcr/eslint-plugin-react/blob/master/docs/rules/sort-comp.md 192 | "order": [ 193 | "displayName", 194 | "propTypes", 195 | "contextTypes", 196 | "childContextTypes", 197 | "mixins", 198 | "statics", 199 | "defaultProps", 200 | "/^_(?!(on|get|render))/", 201 | "constructor", 202 | "getDefaultProps", 203 | "getInitialState", 204 | "state", 205 | "getChildContext", 206 | "componentWillMount", 207 | "componentDidMount", 208 | "componentWillReceiveProps", 209 | "shouldComponentUpdate", 210 | "componentWillUpdate", 211 | "componentDidUpdate", 212 | "componentWillUnmount", 213 | "/^_?on.+$/", 214 | "/^_?get.+$/", 215 | "/^_?render.+$/", 216 | "render" 217 | ] 218 | }] 219 | } 220 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # node-waf configuration 20 | .lock-wscript 21 | 22 | # Compiled binary addons (http://nodejs.org/api/addons.html) 23 | build/Release 24 | 25 | # Dependency directory 26 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 27 | node_modules 28 | 29 | # Build directory 30 | dist 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Circle CI](https://circleci.com/gh/andrewdamelio/react-redux-boilerplate/tree/master.svg?style=svg&circle-token=35697916f8fc181b59f088c95a8dad886fc610a3)](https://circleci.com/gh/andrewdamelio/react-redux-boilerplate/tree/master) 2 | 3 | # React / Redux Boilerplate 4 | 5 | [Demo](http://andrewdamel.io/dev/react-redux) 6 | 7 | #### Core libraries: 8 | - [React](https://facebook.github.io/react/) 🚀 9 | - [Redux](http://redux.js.org/) ⚗ 10 | - [React-Router](https://github.com/rackt/react-router) 🛰 11 | - [React-Router-Redux](https://github.com/rackt/react-router-redux) 🚀 🛰 ⚗ 12 | - [Redux DevTools](https://github.com/gaearon/redux-devtools) 📊 13 | - [ImmutableJS](https://facebook.github.io/immutable-js/) 💎 14 | - [Webpack](https://webpack.github.io/) 🕸 15 | - [Babel6](http://babeljs.io/) 🔑 16 | - [ESlint](http://eslint.org/) 🚨 17 | - Styles 🖌 18 | - [Radium](http://stack.formidable.com/radium/) 19 | - [PostCSS](https://github.com/postcss/postcss) 20 | - [cssnext](http://cssnext.io/) 21 | 22 | #### middleware 23 | - [promiseMiddleware](https://github.com/andrewdamelio/react-redux-boilerplate/blob/master/src/middleware/promiseMiddleware.js) (example of custom middleware) 24 | - [redux-logger](https://github.com/fcomb/redux-logger) 25 | - [redux-thunk](https://github.com/gaearon/redux-thunk) 26 | - [redux-saga](https://github.com/yelouafi/redux-saga) 27 | 28 | 29 | ## Installation 30 | 31 | You'll need to have [Node.js] (https://nodejs.org/) to get started. 32 | 33 | ```bash 34 | $ npm install # Install dependencies 35 | ``` 36 | 37 | * If your having issues try updating to the latest version of node. 38 | 39 | 40 | ## Getting Started 41 | 42 | #### Dev 43 | ```bash 44 | $ npm run dev 45 | ``` 46 | 47 | #### ESLint 48 | ```bash 49 | $ npm run lint 50 | ``` 51 | 52 | #### Tests 53 | ```bash 54 | $ npm run test 55 | $ npm run test:watch 56 | ``` 57 | 58 | #### Build 59 | ```bash 60 | $ npm run clean 61 | $ npm run build 62 | $ http-server -p 8080 . 63 | 64 | ``` 65 | Open http://localhost:8080 in your browser. 66 | 67 | ## Containers (smart) vs Components (dumb) 68 | 69 | - **Containers** are smart because they provide functions and data to the components. We also **connect** to redux at this level, grabbing reducer actions and state to also pass down to our components. 70 | 71 | - **Components** are dumb because they don't know anything about anything. They get data and functions passed in via 72 | **props**, and we try and avoid having any component state (including **lifecycle** events and **refs**). Your notice all the components in this boilerplate are using React's **stateless component** syntax. 73 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | machine: 2 | node: 3 | version: 4.1.1 4 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | React / Redux 4 | 5 | 6 |
7 |
8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /mocha.opts: -------------------------------------------------------------------------------- 1 | --require babel-core/register 2 | --require babel-polyfill 3 | src/**/*.test.js 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-redux-boilerplate", 3 | "version": "1.0.0", 4 | "description": "A react / redux starter project.", 5 | "engines": { 6 | "node": "4.x" 7 | }, 8 | "scripts": { 9 | "clean": "rimraf dist", 10 | "build": "NODE_ENV=production webpack -p", 11 | "dev": "webpack-dev-server --hot --progress", 12 | "lint": "eslint .", 13 | "test": "mocha --opts ./mocha.opts", 14 | "test:watch": "npm run test -- --watch" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "git+https://github.com/andrewdamelio/react-redux-boilerplate" 19 | }, 20 | "license": "MIT", 21 | "homepage": "https://github.com/andrewdamelio/react-redux-boilerplate#readme", 22 | "dependencies": { 23 | "autoprefixer": "^6.3.4", 24 | "babel": "^6.3.26", 25 | "babel-core": "^6.4.5", 26 | "babel-eslint": "^5.0.0-beta8", 27 | "babel-loader": "^6.2.1", 28 | "babel-polyfill": "^6.3.14", 29 | "babel-preset-es2015": "^6.3.13", 30 | "babel-preset-react": "^6.3.13", 31 | "babel-preset-stage-0": "^6.3.13", 32 | "basscss": "^7.1.0", 33 | "chai": "^3.5.0", 34 | "css-loader": "^0.23.1", 35 | "cssnext-loader": "^1.0.1", 36 | "eslint": "^1.10.3", 37 | "eslint-loader": "^1.2.1", 38 | "eslint-plugin-react": "^3.16.1", 39 | "file-loader": "^0.8.5", 40 | "history": "^2.0.0-rc2", 41 | "immutable": "^3.7.6", 42 | "jsdom": "^8.0.1", 43 | "mocha": "^2.4.5", 44 | "postcss-loader": "^0.8.0", 45 | "radium": "^0.16.5", 46 | "react": "^0.14.7", 47 | "react-addons-test-utils": "^0.14.7", 48 | "react-dom": "^0.14.7", 49 | "react-hot-loader": "^1.3.0", 50 | "react-redux": "^4.4.0", 51 | "react-router": "^2.0.0", 52 | "react-router-redux": "^3.0.0", 53 | "redux": "^3.3.1", 54 | "redux-logger": "^2.5.0", 55 | "redux-saga": "^0.8.2", 56 | "redux-thunk": "^1.0.3", 57 | "rimraf": "^2.5.1", 58 | "source-map-loader": "^0.1.5", 59 | "style-loader": "^0.13.0", 60 | "url-loader": "^0.5.7", 61 | "webpack": "^1.12.12", 62 | "webpack-dev-server": "^1.14.1" 63 | }, 64 | "devDependencies": { 65 | "redux-devtools": "^3.1.1", 66 | "redux-devtools-dock-monitor": "^1.1.0", 67 | "redux-devtools-log-monitor": "^1.0.4" 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/assets/background.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewdamelio/react-redux-boilerplate/96ea9f8af1120f44220c68ea00bda1725ca70c43/src/assets/background.jpg -------------------------------------------------------------------------------- /src/components/Counter.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | import Radium from 'radium'; 3 | import Status from './Status'; 4 | 5 | const Counter = ({ counter, increment, decrement }) => { 6 | return ( 7 |
8 | 9 | 10 |
11 | 14 | 15 |
16 | { ' ' + counter + ' ' } 17 |
18 | 19 | 22 |
23 |
24 | ); 25 | }; 26 | 27 | Counter.propTypes = { 28 | counter: PropTypes.number.isRequired, 29 | increment: PropTypes.func.isRequired, 30 | decrement: PropTypes.func.isRequired, 31 | }; 32 | 33 | export default Radium(Counter); 34 | 35 | const styles = { 36 | base: { 37 | userSelect: 'none', 38 | }, 39 | button: { 40 | fontSize: '0.7em', 41 | }, 42 | }; 43 | -------------------------------------------------------------------------------- /src/components/Loading.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | 3 | 4 | const Loading = ({ isVisible }) => { 5 | const transition = { transition: 'opacity 300ms, visbility 300ms' }; 6 | 7 | const styles = isVisible ? 8 | { 9 | ...transition, 10 | visbility: 'visible', 11 | zIndex: 999, 12 | opacity: 1, 13 | } : 14 | { 15 | ...transition, 16 | visbility: 'hidden', 17 | zIndex: -1, 18 | opacity: 0, 19 | }; 20 | 21 | return ( 22 |
25 |
28 |

Loading...

29 |
30 |
31 | ); 32 | }; 33 | 34 | Loading.propTypes = { 35 | isVisible: PropTypes.bool.isRequired, 36 | }; 37 | 38 | export default Loading; 39 | -------------------------------------------------------------------------------- /src/components/Nav.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | import Radium from 'radium'; 3 | 4 | const Nav = ({ navigate, location, className = '' }) => { 5 | const homeActive = location === '/'; 6 | 7 | return ( 8 | 20 | ); 21 | }; 22 | 23 | Nav.propTypes = { 24 | location: PropTypes.string.isRequired, 25 | className: PropTypes.string.isRequired, 26 | navigate: PropTypes.func.isRequired, 27 | }; 28 | 29 | export default Radium(Nav); 30 | 31 | const styles = { 32 | base: { 33 | userSelect: 'none', 34 | }, 35 | active: { 36 | textDecoration: 'none', 37 | borderBottom: '1px solid rgb(15, 16, 18)', 38 | }, 39 | inactive: { 40 | textDecoration: 'none', 41 | cursor: 'pointer', 42 | }, 43 | }; 44 | -------------------------------------------------------------------------------- /src/components/Status.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | import Radium from 'radium'; 3 | 4 | const Status = ({ counter }) => { 5 | const happyOrSad = counter > 0 ? ':)' : ':('; 6 | 7 | let displayColor; 8 | if (counter < 0) { 9 | displayColor = 'red'; 10 | } else if (counter > 0) { 11 | displayColor = 'green'; 12 | } else { 13 | displayColor = 'black'; 14 | } 15 | 16 | return ( 17 |

18 |
23 | { counter === 0 ? ':|' : happyOrSad } 24 |
25 |

26 | ); 27 | }; 28 | 29 | Status.propTypes = { 30 | counter: PropTypes.number.isRequired, 31 | }; 32 | 33 | export default Radium(Status); 34 | 35 | const styles = { 36 | base: { 37 | fontSize: '3em', 38 | transform: 'rotate(90deg)', 39 | marginBottom: '50vh', 40 | textAlign: 'center', 41 | }, 42 | red: { 43 | color: '#FE3232', 44 | }, 45 | green: { 46 | color: '#2AFE1B', 47 | }, 48 | black: { 49 | color: '#000000', 50 | }, 51 | }; 52 | -------------------------------------------------------------------------------- /src/components/__test__/Counter.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node, mocha */ 2 | import jsdom from 'jsdom'; 3 | import assert from'assert'; 4 | import React from 'react'; 5 | import ReactDOM from 'react-dom'; 6 | import Counter from '../Counter'; 7 | import TestUtils from 'react-addons-test-utils'; 8 | 9 | 10 | /** 11 | * Returns the computed styles of a 'component' at the 12 | * provided 'lookup' id. 13 | * 14 | * @param {React Component} component 15 | * @return computed styles 16 | */ 17 | function getStylesByTestId(component, lookup) { 18 | const node = ReactDOM.findDOMNode(component); 19 | const targetNode = node.querySelector(lookup); 20 | const styles = global.window.getComputedStyle(targetNode); 21 | 22 | return styles; 23 | } 24 | 25 | describe('Counter', () => { 26 | beforeEach(() => { 27 | global.document = jsdom.jsdom(); 28 | global.window = document.defaultView; 29 | }); 30 | 31 | describe('counter >= 0', () => { 32 | let counter; 33 | 34 | beforeEach(() => { 35 | const mockFn = () => true; 36 | counter = TestUtils.renderIntoDocument( 37 | 40 | ); 41 | }); 42 | 43 | it('should display with green color style', () => { 44 | const style = getStylesByTestId(counter, '#status-test-id'); 45 | const statusIndicatorStyles = style.color === 'rgb(42, 254, 27)'; 46 | assert.strictEqual(statusIndicatorStyles, true); 47 | }); 48 | }); 49 | 50 | describe('counter < 0', () => { 51 | let counter; 52 | 53 | beforeEach(() => { 54 | const mockFn = () => true; 55 | counter = TestUtils.renderIntoDocument( 56 | 59 | ); 60 | }); 61 | 62 | it('should display with red color style', () => { 63 | const style = getStylesByTestId(counter, '#status-test-id'); 64 | const statusIndicatorStyles = style.color === 'rgb(254, 50, 50)'; 65 | assert.strictEqual(statusIndicatorStyles, true); 66 | }); 67 | }); 68 | }); 69 | -------------------------------------------------------------------------------- /src/containers/AboutPage.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { fromJS } from 'immutable'; 4 | import Loading from '../components/Loading'; 5 | import { showLoadingModal } from '../reducers/loading'; 6 | 7 | const mockData = fromJS([{ 8 | 'name': 'React', 9 | }, { 10 | 'name': 'Redux', 11 | }, { 12 | 'name': 'React-Router', 13 | }, { 14 | 'name': 'ImmutableJS', 15 | }, { 16 | 'name': 'Radium', 17 | }, { 18 | 'name': 'Eslint', 19 | }]); 20 | 21 | function mapStateToProps(state) { 22 | return { 23 | loading: state.loading, 24 | }; 25 | } 26 | 27 | function mapDispatchToProps(dispatch) { 28 | return { 29 | showLoadingModal: () => dispatch(showLoadingModal()), 30 | }; 31 | } 32 | 33 | 34 | class AboutPage extends Component { 35 | 36 | componentDidMount() { 37 | this.props.showLoadingModal(); 38 | } 39 | 40 | render() { 41 | const rows = mockData.map((data, index) => { 42 | return ( 43 |
  • 44 | { data.get('name') } 45 |
  • 46 | ); 47 | }); 48 | 49 | const { loading } = this.props; 50 | return ( 51 |
    52 | 53 |

    React / Redux boilerplate

    54 |
      { rows }
    55 |
    56 | ); 57 | } 58 | } 59 | 60 | AboutPage.propTypes = { 61 | loading: PropTypes.bool.isRequired, 62 | showLoadingModal: PropTypes.func.isRequired, 63 | }; 64 | 65 | 66 | export default connect( 67 | mapStateToProps, 68 | mapDispatchToProps, 69 | )(AboutPage); 70 | -------------------------------------------------------------------------------- /src/containers/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { routeActions } from 'react-router-redux'; 4 | import Radium from 'radium'; 5 | import Nav from '../components/Nav'; 6 | 7 | function mapStateToProps(state) { 8 | return { 9 | location: state.routing.location.pathname, 10 | }; 11 | } 12 | 13 | function mapDispatchToProps(dispatch) { 14 | return { 15 | navigate: (route) => dispatch(routeActions.push(route)), 16 | }; 17 | } 18 | 19 | class App extends Component { 20 | 21 | render() { 22 | const { props } = this; 23 | return ( 24 |
    25 | 26 |
    37 | ); 38 | } 39 | } 40 | 41 | App.propTypes = { 42 | location: PropTypes.string.isRequired, 43 | navigate: PropTypes.func.isRequired, 44 | }; 45 | 46 | export default connect( 47 | mapStateToProps, 48 | mapDispatchToProps, 49 | )(Radium(App)); 50 | 51 | 52 | const styles = { 53 | background: { 54 | width: '80vw', 55 | height: '100vh', 56 | }, 57 | container: { 58 | width: '20vw', 59 | }, 60 | }; 61 | -------------------------------------------------------------------------------- /src/containers/CounterPage.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import Counter from '../components/Counter'; 4 | import { increment, decrement } from '../reducers/counter'; 5 | import Loading from '../components/Loading'; 6 | import { showLoadingModal } from '../reducers/loading'; 7 | 8 | function mapStateToProps(state) { 9 | return { 10 | counter: state.counter.get('value'), 11 | loading: state.loading, 12 | }; 13 | } 14 | 15 | function mapDispatchToProps(dispatch) { 16 | return { 17 | incrementCounter: () => dispatch(increment()), 18 | decrementCounter: () => dispatch(decrement()), 19 | showLoadingModal: () => dispatch(showLoadingModal()), 20 | }; 21 | } 22 | 23 | class CounterPage extends Component { 24 | 25 | componentDidMount() { 26 | this.props.showLoadingModal(); 27 | } 28 | 29 | render() { 30 | const { counter, loading, incrementCounter, decrementCounter } = this.props; 31 | return ( 32 |
    33 | 34 | 37 |
    38 | ); 39 | } 40 | } 41 | 42 | CounterPage.propTypes = { 43 | loading: PropTypes.bool.isRequired, 44 | counter: PropTypes.number.isRequired, 45 | incrementCounter: PropTypes.func.isRequired, 46 | decrementCounter: PropTypes.func.isRequired, 47 | showLoadingModal: PropTypes.func.isRequired, 48 | }; 49 | 50 | 51 | export default connect( 52 | mapStateToProps, 53 | mapDispatchToProps 54 | )(CounterPage); 55 | -------------------------------------------------------------------------------- /src/containers/DevTools.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { createDevTools } from 'redux-devtools'; 3 | import LogMonitor from 'redux-devtools-log-monitor'; 4 | import DockMonitor from 'redux-devtools-dock-monitor'; 5 | 6 | // DOCS: https://github.com/gaearon/redux-devtools 7 | 8 | // createDevTools takes a monitor and produces a DevTools component 9 | const DevTools = createDevTools( 10 | 13 | 14 | 15 | ); 16 | 17 | export default DevTools; 18 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import 'babel-polyfill'; 2 | 3 | import './styles/styles.css'; 4 | 5 | import React from 'react'; 6 | import ReactDOM from 'react-dom'; 7 | import { Provider } from 'react-redux'; 8 | import { Router } from 'react-router'; 9 | import history from './store/history'; 10 | import routes from './store/router'; 11 | import DevTools from './containers/DevTools'; 12 | import configureStore from './store/configureStore'; 13 | 14 | const initialState = {}; 15 | const store = configureStore(initialState); 16 | 17 | const injectDevTools = ()=> { 18 | if (__DEV__) { 19 | return ( 20 | 21 | ); 22 | } 23 | return null; 24 | }; 25 | 26 | ReactDOM.render( 27 | 28 |
    29 | 30 | { routes } 31 | 32 | { injectDevTools() } 33 |
    34 |
    , 35 | document.getElementById('root') 36 | ); 37 | -------------------------------------------------------------------------------- /src/middleware/promiseMiddleware.js: -------------------------------------------------------------------------------- 1 | import isPromise from '../utils/isPromise'; 2 | 3 | export default function promiseMiddleware({ dispatch }) { 4 | return next => action => { 5 | if (!isPromise(action.payload)) { 6 | return next(action); 7 | } 8 | 9 | const { types, payload, meta } = action; 10 | const { promise, data } = payload; 11 | const [ PENDING, FULFILLED, REJECTED ] = types; 12 | 13 | /** 14 | * Dispatch the pending action 15 | */ 16 | dispatch({ 17 | type: PENDING, 18 | ...data && { payload: data }, 19 | ...meta && { meta }, 20 | }); 21 | 22 | /** 23 | * If successful, dispatch the fulfilled action, otherwise dispatch 24 | * rejected action. 25 | */ 26 | return promise.then( 27 | result => { 28 | dispatch({ 29 | type: FULFILLED, 30 | payload: result, 31 | meta, 32 | }); 33 | }, 34 | error => { 35 | dispatch({ 36 | type: REJECTED, 37 | payload: error, 38 | meta, 39 | }); 40 | } 41 | ); 42 | }; 43 | } 44 | -------------------------------------------------------------------------------- /src/reducers/__tests__/counter.test.js: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import counterReducer from '../../reducers/counter'; 3 | import { Map } from 'immutable'; 4 | 5 | const INCREMENT = 'INCREMENT'; 6 | const DECREMENT = 'DECREMENT'; 7 | 8 | let state = counterReducer(undefined, {}); 9 | 10 | describe('counter reducer', () => { 11 | describe('inital state', () => { 12 | it('should be a Map', () => { 13 | assert.strictEqual(Map.isMap(state), true); 14 | }); 15 | }); 16 | 17 | describe('on INCREMENT', () => { 18 | it('should increment state.value', () => { 19 | const previousValue = state.get('value'); 20 | state = fireAction(INCREMENT, state); 21 | assert.strictEqual(previousValue + 1, state.get('value')); 22 | }); 23 | }); 24 | 25 | describe('on DECREMENT', () => { 26 | it('should decrement state.value', () => { 27 | const previousValue = state.get('value'); 28 | state = fireAction(DECREMENT, state); 29 | assert.strictEqual(previousValue - 1, state.get('value')); 30 | }); 31 | }); 32 | }); 33 | 34 | function fireAction(actionType, currentState) { 35 | return counterReducer(currentState, { 36 | type: actionType, 37 | }); 38 | } 39 | -------------------------------------------------------------------------------- /src/reducers/__tests__/loading.test.js: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import loadingReducer from '../../reducers/loading'; 3 | 4 | const SHOW_LOADING_MODAL = 'SHOW_LOADING_MODAL'; 5 | const HIDE_LOADING_MODAL = 'HIDE_LOADING_MODAL'; 6 | 7 | let state = loadingReducer(undefined, {}); 8 | 9 | describe('loading reducer', () => { 10 | describe('inital state', () => { 11 | it('should default to false', () => { 12 | assert.strictEqual(state, false); 13 | }); 14 | }); 15 | 16 | describe('on SHOW_LOADING_MODAL', () => { 17 | it('should set state to true', () => { 18 | state = fireAction(SHOW_LOADING_MODAL, state); 19 | assert.strictEqual(state, true); 20 | }); 21 | }); 22 | 23 | describe('on HIDE_LOADING_MODAL', () => { 24 | it('should set state to false', () => { 25 | state = fireAction(HIDE_LOADING_MODAL, state); 26 | assert.strictEqual(state, false); 27 | }); 28 | }); 29 | }); 30 | 31 | function fireAction(actionType, currentState) { 32 | return loadingReducer(currentState, { 33 | type: actionType, 34 | }); 35 | } 36 | -------------------------------------------------------------------------------- /src/reducers/counter.js: -------------------------------------------------------------------------------- 1 | import { fromJS } from 'immutable'; 2 | 3 | /* Constants */ 4 | 5 | const INCREMENT = 'INCREMENT'; 6 | const DECREMENT = 'DECREMENT'; 7 | 8 | 9 | /* Reducer */ 10 | 11 | const initialState = fromJS({ 12 | value: 0, 13 | }); 14 | 15 | function counterReducer(state = initialState, action = {}) { 16 | switch (action.type) { 17 | 18 | case INCREMENT: 19 | return state.update('value', (value) => value + 1); 20 | 21 | case DECREMENT: 22 | return state.update('value', (value) => value - 1); 23 | 24 | default: 25 | return state; 26 | } 27 | } 28 | 29 | export default counterReducer; 30 | 31 | 32 | /* Actions */ 33 | 34 | export function increment() { 35 | return { 36 | type: INCREMENT, 37 | }; 38 | } 39 | 40 | export function decrement() { 41 | return { 42 | type: DECREMENT, 43 | }; 44 | } 45 | 46 | // Thunk Example 47 | export function incrementIfEven() { 48 | return (dispatch, getState) => { 49 | if (getState().counter.get('value') % 2 === 0) { 50 | return dispatch(increment()); 51 | } 52 | }; 53 | } 54 | -------------------------------------------------------------------------------- /src/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import { routeReducer } from 'react-router-redux'; 3 | import counter from './counter'; 4 | import loading from './loading'; 5 | 6 | const rootReducer = combineReducers({ 7 | counter, 8 | loading, 9 | routing: routeReducer, 10 | }); 11 | 12 | export default rootReducer; 13 | -------------------------------------------------------------------------------- /src/reducers/loading.js: -------------------------------------------------------------------------------- 1 | /* Constants */ 2 | 3 | const SHOW_LOADING_MODAL = 'SHOW_LOADING_MODAL'; 4 | const HIDE_LOADING_MODAL = 'HIDE_LOADING_MODAL'; 5 | 6 | /* Reducer */ 7 | 8 | const initialState = false; 9 | 10 | function loadingReducer(state = initialState, action = {}) { 11 | switch (action.type) { 12 | 13 | case SHOW_LOADING_MODAL: 14 | return true; 15 | 16 | case HIDE_LOADING_MODAL: 17 | return false; 18 | 19 | default: 20 | return state; 21 | } 22 | } 23 | 24 | export default loadingReducer; 25 | 26 | /* Actions */ 27 | 28 | export function showLoadingModal() { 29 | return { type: SHOW_LOADING_MODAL }; 30 | } 31 | 32 | export function hideLoadingModal() { 33 | return { type: HIDE_LOADING_MODAL }; 34 | } 35 | -------------------------------------------------------------------------------- /src/sagas/__tests__/delay.test.js: -------------------------------------------------------------------------------- 1 | import delay from '../delay'; 2 | import assert from 'assert'; 3 | 4 | const getStateMock = () => { 5 | return { 6 | loading: true, 7 | }; 8 | }; 9 | 10 | const iterator = delay(getStateMock); 11 | 12 | describe('delay generator', () => { 13 | it('1. yield should take SHOW_LOADING_MODAL action', () => { 14 | assert.deepEqual(iterator.next().value, { 15 | TAKE: 'SHOW_LOADING_MODAL', 16 | }); 17 | }); 18 | 19 | it('2. yield should call wait', () => { 20 | const generatorValue = iterator.next('SHOW_LOADING_MODAL').value; 21 | assert.strictEqual(generatorValue.CALL.fn.name, 'wait'); 22 | assert.strictEqual(generatorValue.CALL.args[0], 500); 23 | }); 24 | 25 | it('3. yield should put hideLoadingModal and trigger the action', () => { 26 | assert.deepEqual(iterator.next().value, { 27 | PUT: { 28 | type: 'HIDE_LOADING_MODAL', 29 | }, 30 | }); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /src/sagas/delay.js: -------------------------------------------------------------------------------- 1 | import { take, put, call } from 'redux-saga/effects'; 2 | import { hideLoadingModal } from '../reducers/loading'; 3 | 4 | const HALF_SECOND = 500; 5 | 6 | const wait = ms => ( 7 | new Promise(resolve => { 8 | setTimeout(() => resolve(), ms); 9 | }) 10 | ); 11 | 12 | function* delay(getState) { 13 | // Wake up when SHOW_LOADING_MODAL is dispatched 14 | while (yield take('SHOW_LOADING_MODAL')) { 15 | if (getState().loading) { 16 | // call delay 17 | yield call(wait, HALF_SECOND); 18 | 19 | // dispatch HIDE_LOADING_MODAL 20 | yield put(hideLoadingModal()); 21 | } 22 | } 23 | } 24 | 25 | export default delay; 26 | -------------------------------------------------------------------------------- /src/store/configureStore.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware, compose } from 'redux'; 2 | import { syncHistory } from 'react-router-redux'; 3 | import sagaMiddleware from 'redux-saga'; 4 | import thunk from 'redux-thunk'; 5 | import promiseMiddleware from '../middleware/promiseMiddleware'; 6 | import logger from './logger'; 7 | import DevTools from '../containers/DevTools'; 8 | import history from './history'; 9 | import rootReducer from '../reducers'; 10 | import delay from '../sagas/delay'; 11 | 12 | const reduxRouter = syncHistory(history); 13 | 14 | export default function configureStore(initialState) { 15 | let createStoreWithMiddleware; 16 | 17 | if (__DEV__) { 18 | createStoreWithMiddleware = compose( 19 | applyMiddleware( 20 | reduxRouter, 21 | sagaMiddleware(delay), 22 | promiseMiddleware, 23 | thunk, 24 | logger, 25 | ), 26 | DevTools.instrument(), 27 | )(createStore); 28 | } else { 29 | createStoreWithMiddleware = compose( 30 | applyMiddleware( 31 | reduxRouter, 32 | sagaMiddleware(delay), 33 | promiseMiddleware, 34 | thunk, 35 | ), 36 | )(createStore); 37 | } 38 | 39 | const store = createStoreWithMiddleware(rootReducer, initialState); 40 | 41 | if (module.hot) { 42 | // Enable Webpack hot module replacement for reducers 43 | module.hot.accept('../reducers', () => { 44 | const nextRootReducer = require('../reducers'); 45 | store.replaceReducer(nextRootReducer); 46 | }); 47 | } 48 | 49 | // Required for replaying actions from devtools to work 50 | reduxRouter.listenForReplays(store); 51 | 52 | return store; 53 | } 54 | -------------------------------------------------------------------------------- /src/store/history.js: -------------------------------------------------------------------------------- 1 | import { hashHistory } from 'react-router'; 2 | 3 | const _createHashHistory = () => { 4 | if (global.window) { 5 | return hashHistory; 6 | } 7 | 8 | return undefined; 9 | }; 10 | 11 | const createHistory = _createHashHistory(); 12 | 13 | export default createHistory; 14 | -------------------------------------------------------------------------------- /src/store/logger.js: -------------------------------------------------------------------------------- 1 | import createLogger from 'redux-logger'; 2 | import { immutableToJS } from '../utils/immutableToJS'; 3 | 4 | export default createLogger({ 5 | collapsed: true, 6 | stateTransformer: (state) => { 7 | return immutableToJS(state); 8 | }, 9 | }); 10 | -------------------------------------------------------------------------------- /src/store/router.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Route, IndexRoute } from 'react-router'; 3 | import App from '../containers/App'; 4 | import CounterPage from '../containers/CounterPage'; 5 | import AboutPage from '../containers/AboutPage'; 6 | 7 | const routes = ( 8 | 9 | 10 | 11 | 12 | 13 | ); 14 | 15 | export default routes; 16 | -------------------------------------------------------------------------------- /src/styles/base.css: -------------------------------------------------------------------------------- 1 | *, 2 | *::before, 3 | *::after { 4 | box-sizing: border-box; 5 | } 6 | -------------------------------------------------------------------------------- /src/styles/styles.css: -------------------------------------------------------------------------------- 1 | @import 'basscss'; 2 | @import 'base'; 3 | -------------------------------------------------------------------------------- /src/utils/immutableToJS.js: -------------------------------------------------------------------------------- 1 | import { Iterable } from 'immutable'; 2 | 3 | /** 4 | * [immutableToJS 5 | * converts properties of the provided [state] object from immutable 6 | * data structures to regular JavaScript data structures - used with 7 | * redux-logger 8 | * 9 | * @param {object} state [state reference] 10 | * @return {object} [transformed state] 11 | */ 12 | export function immutableToJS(state) { 13 | return Object.keys(state).reduce((newState, key) => { 14 | const val = state[key]; 15 | newState[key] = Iterable.isIterable(val) ? val.toJS() : val; 16 | return newState; 17 | }, {}); 18 | } 19 | -------------------------------------------------------------------------------- /src/utils/isPromise.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Returns whether the provided value is a promise 3 | * 4 | * @param {object} value Potential promise 5 | * @return {Boolean} 6 | */ 7 | export default function isPromise(value) { 8 | if (value !== null && typeof value === 'object') { 9 | return value.promise && typeof value.promise.then === 'function'; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const autoprefixer = require('autoprefixer'); 3 | 4 | function getEntrySources(sources) { 5 | if (process.env.NODE_ENV !== 'production') { 6 | sources.push('webpack-dev-server/client?http://localhost:8080'); 7 | sources.push('webpack/hot/only-dev-server'); 8 | } 9 | 10 | return sources; 11 | } 12 | 13 | module.exports = { 14 | devtool: process.env.NODE_ENV !== 'production' ? 'eval-source-map' : '', 15 | entry: { 16 | bundle: getEntrySources([ 17 | './src/index.js', 18 | ]), 19 | }, 20 | output: { 21 | publicPath: 'http://localhost:8080/', 22 | filename: 'dist/[name].js', 23 | }, 24 | plugins: [ 25 | new webpack.DefinePlugin({ 26 | __DEV__: process.env.NODE_ENV !== 'production', 27 | 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV), 28 | }), 29 | ], 30 | module: { 31 | preLoaders: [{ 32 | test: /\.js$/, 33 | loader: 'source-map-loader', 34 | }], 35 | loaders: [{ 36 | test: /\.js$/, 37 | loader: 'react-hot!babel!eslint-loader', 38 | exclude: /node_modules/, 39 | }, { 40 | test: /\.css$/, 41 | loader: 'style-loader!css-loader!postcss-loader!cssnext-loader', 42 | }, { 43 | test: /\.(png|jpg|jpeg|gif|svg)$/, 44 | loader: 'url-loader?prefix=img/&limit=53248', 45 | }, { 46 | test: /\.(woff|woff2|ttf|eot)$/, 47 | loader: 'url-loader?prefix=font/&limit=53248', 48 | }], 49 | }, 50 | postcss: [ autoprefixer({ browsers: ['last 2 versions'] }) ], 51 | }; 52 | --------------------------------------------------------------------------------