├── .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 | [](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 |
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 |
29 |
30 |

32 |
33 |
34 | { props.children }
35 |
36 |
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 |
--------------------------------------------------------------------------------