├── .env.example ├── .eslintignore ├── .eslintrc ├── .github ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE.md └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── .styleci.yml ├── LICENSE ├── README.md ├── package.json ├── src ├── components │ ├── hello │ │ ├── _hello.scss │ │ └── hello.js │ ├── login │ │ ├── _login.scss │ │ └── login.js │ ├── logout │ │ └── Logout.js │ ├── navbar │ │ ├── _navbar.scss │ │ └── navbar.js │ └── not-found │ │ ├── _not-found.scss │ │ └── not-found.js ├── helpers │ ├── api.js │ ├── auth.js │ ├── log.js │ ├── storage.js │ └── store.js ├── main.html ├── main.js ├── main.scss ├── middleware │ └── api.js ├── routes.js └── state │ ├── auth │ ├── actions.js │ └── reducer.js │ └── reducers.js ├── test └── index.js └── webpack ├── config.js ├── development.js ├── helpers.js └── production.js /.env.example: -------------------------------------------------------------------------------- 1 | API_URL=http://api.example.com/v1 2 | OAUTH_CLIENT_ID=app 3 | OAUTH_CLIENT_SECRET=secret 4 | STORAGE_PREFIX=app 5 | IS_LOGGING=true 6 | LOGGING_LEVEL=5 7 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dist 2 | test 3 | **/*{.,-}min.js 4 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "parserOptions": { 4 | "sourceType": "module", 5 | "ecmaVersion": 6, 6 | "ecmaFeatures": { 7 | "jsx": true, 8 | "experimentalObjectRestSpread": true 9 | } 10 | }, 11 | "env": { 12 | "es6": true, 13 | "node": true 14 | }, 15 | "plugins": [ 16 | "react" 17 | ], 18 | "globals": { 19 | "window": true, 20 | "document": true, 21 | "fetch": true, 22 | "Headers": true, 23 | "Request": true, 24 | "FormData": true, 25 | "FileReader": true, 26 | "localStorage": true 27 | }, 28 | "rules": { 29 | "block-scoped-var": [0], 30 | "brace-style": [2, "1tbs", { 31 | "allowSingleLine": true 32 | }], 33 | "camelcase": [0], 34 | "comma-dangle": [0], 35 | "comma-spacing": [2], 36 | "comma-style": [2, "last"], 37 | "complexity": [0, 11], 38 | "consistent-return": [2], 39 | "consistent-this": [0, "that"], 40 | "curly": [2, "multi-line"], 41 | "default-case": [2], 42 | "dot-notation": [2, { 43 | "allowKeywords": true 44 | }], 45 | "eol-last": [2], 46 | "eqeqeq": [2], 47 | "func-names": [0], 48 | "func-style": [0, "declaration"], 49 | "generator-star-spacing": [2, "after"], 50 | "guard-for-in": [0], 51 | "handle-callback-err": [0], 52 | "indent": [2, 2, {"SwitchCase": 1}], 53 | "key-spacing": [2, { 54 | "beforeColon": false, 55 | "afterColon": true 56 | }], 57 | "quotes": [2, "single", "avoid-escape"], 58 | "max-depth": [0, 4], 59 | "max-len": [0, 80, 4], 60 | "max-nested-callbacks": [0, 2], 61 | "max-params": [0, 3], 62 | "max-statements": [0, 10], 63 | "new-parens": [2], 64 | "new-cap": [0], 65 | "newline-after-var": [0], 66 | "no-alert": [2], 67 | "no-array-constructor": [2], 68 | "no-bitwise": [0], 69 | "no-caller": [2], 70 | "no-catch-shadow": [2], 71 | "no-cond-assign": [2], 72 | "no-console": [0], 73 | "no-constant-condition": [1], 74 | "no-continue": [2], 75 | "no-control-regex": [2], 76 | "no-debugger": [2], 77 | "no-delete-var": [2], 78 | "no-div-regex": [0], 79 | "no-dupe-args": [2], 80 | "no-dupe-keys": [2], 81 | "no-duplicate-case": [2], 82 | "no-else-return": [0], 83 | "no-empty": [2], 84 | "no-empty-character-class": [2], 85 | "no-eq-null": [0], 86 | "no-eval": [2], 87 | "no-ex-assign": [2], 88 | "no-extend-native": [1], 89 | "no-extra-bind": [2], 90 | "no-extra-boolean-cast": [2], 91 | "no-extra-parens": [0], 92 | "no-extra-semi": [1], 93 | "no-fallthrough": [2], 94 | "no-floating-decimal": [2], 95 | "no-func-assign": [2], 96 | "no-implied-eval": [2], 97 | "no-inline-comments": [0], 98 | "no-inner-declarations": [2, "functions"], 99 | "no-invalid-regexp": [2], 100 | "no-irregular-whitespace": [2], 101 | "no-iterator": [2], 102 | "no-label-var": [2], 103 | "no-labels": [2], 104 | "no-lone-blocks": [2], 105 | "no-lonely-if": [2], 106 | "no-loop-func": [2], 107 | "no-mixed-requires": [0, false], 108 | "no-mixed-spaces-and-tabs": [2, false], 109 | "no-multi-spaces": [2], 110 | "no-multi-str": [2], 111 | "no-multiple-empty-lines": [2, { 112 | "max": 1 113 | }], 114 | "no-native-reassign": [1], 115 | "no-negated-in-lhs": [2], 116 | "no-nested-ternary": [0], 117 | "no-new": [2], 118 | "no-new-func": [2], 119 | "no-new-object": [2], 120 | "no-new-require": [0], 121 | "no-new-wrappers": [2], 122 | "no-obj-calls": [2], 123 | "no-octal": [2], 124 | "no-octal-escape": [2], 125 | "no-param-reassign": [0], 126 | "no-path-concat": [0], 127 | "no-plusplus": [0], 128 | "no-process-env": [0], 129 | "no-process-exit": [0], 130 | "no-proto": [2], 131 | "no-redeclare": [2], 132 | "no-regex-spaces": [2], 133 | "no-reserved-keys": [0], 134 | "no-restricted-modules": [0], 135 | "no-return-assign": [2], 136 | "no-script-url": [2], 137 | "no-self-compare": [0], 138 | "no-sequences": [2], 139 | "no-shadow": [2], 140 | "no-shadow-restricted-names": [2], 141 | "no-spaced-func": [2], 142 | "no-sparse-arrays": [2], 143 | "no-sync": [0], 144 | "no-ternary": [0], 145 | "no-throw-literal": [2], 146 | "no-trailing-spaces": [2, {"skipBlankLines": true}], 147 | "no-undef": [2], 148 | "no-undef-init": [2], 149 | "no-undefined": [0], 150 | "no-underscore-dangle": [0], 151 | "no-unreachable": [2], 152 | "no-unused-expressions": [2], 153 | "no-unused-vars": [2, { 154 | "vars": "all", 155 | "args": "after-used" 156 | }], 157 | "no-use-before-define": [0], 158 | "no-void": [0], 159 | "no-warning-comments": [0, { 160 | "terms": ["todo", "fixme", "xxx"], 161 | "location": "start" 162 | }], 163 | "no-with": [2], 164 | "object-curly-spacing": [2, "always"], 165 | "one-var": [0], 166 | "operator-assignment": [0, "always"], 167 | "operator-linebreak": [2, "before"], 168 | "padded-blocks": [0], 169 | "quote-props": [0], 170 | "radix": [0], 171 | "semi": [2], 172 | "semi-spacing": [2, { 173 | "before": false, 174 | "after": true 175 | }], 176 | "sort-vars": [0], 177 | "keyword-spacing": [2, {"before": true, "after": true}], 178 | "space-before-function-paren": [2, { 179 | "anonymous": "never", 180 | "named": "never" 181 | }], 182 | "space-before-blocks": [0, "always"], 183 | "space-in-brackets": [ 184 | 0, "never", { 185 | "singleValue": true, 186 | "arraysInArrays": false, 187 | "arraysInObjects": false, 188 | "objectsInArrays": true, 189 | "objectsInObjects": true, 190 | "propertyName": false 191 | } 192 | ], 193 | "space-in-parens": [2, "never"], 194 | "space-infix-ops": [2], 195 | "space-unary-ops": [2, { 196 | "words": true, 197 | "nonwords": false 198 | }], 199 | "spaced-line-comment": [0, "always"], 200 | "strict": 0, 201 | "use-isnan": [2], 202 | "valid-jsdoc": [0], 203 | "valid-typeof": [2], 204 | "vars-on-top": [0], 205 | "wrap-iife": [2], 206 | "wrap-regex": [2], 207 | "yoda": [2, "never", { 208 | "exceptRange": true 209 | }], 210 | "jsx-quotes": [2, "prefer-double"], 211 | "react/display-name": 0, 212 | "react/jsx-boolean-value": 0, 213 | "react/jsx-no-undef": 1, 214 | "react/jsx-sort-props": 0, 215 | "react/jsx-uses-react": 2, 216 | "react/jsx-uses-vars": 1, 217 | "react/no-did-mount-set-state": [1, "allow-in-func"], 218 | "react/no-did-update-set-state": [1, "allow-in-func"], 219 | "react/no-multi-comp": 0, 220 | "react/no-unknown-property": 0, 221 | "react/prop-types": 0, 222 | "react/react-in-jsx-scope": 0, 223 | "react/self-closing-comp": 1, 224 | "react/wrap-multilines": 0 225 | } 226 | } 227 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Please note the following guidelines before submitting pull requests: 4 | 5 | - Make sure ESLint passes with `npm run lint` 6 | - Always create pull requests to the *develop* branch 7 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### What did you do? 2 | 3 | ### What did you expect to happen? 4 | 5 | ### What actually happened? 6 | 7 | ### What version of this module are you using? 8 | 9 | Please include code that reproduces the issue. The best reproductions are self-contained scripts with minimal dependencies. 10 | 11 | ```js 12 | code goes here 13 | ``` 14 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | (Please read the [guidelines](.github/CONTRIBUTING.md) before creating PRs.) 2 | 3 | Fixes #. 4 | 5 | Changes proposed in this pull request: 6 | * 7 | * 8 | * 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | dist 3 | cover 4 | node_modules 5 | *.log 6 | -------------------------------------------------------------------------------- /.styleci.yml: -------------------------------------------------------------------------------- 1 | preset: psr2 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Nord Software 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React starter 2 | 3 | ## DEPRECATED 4 | 5 | **This project is deprecated. Please use [Create React App](https://github.com/facebook/create-react-app) to bootstrap your React projects.** 6 | 7 | [![Code Climate](https://codeclimate.com/github/nordsoftware/react-starter/badges/gpa.svg)](https://codeclimate.com/github/nordsoftware/react-starter) 8 | [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/nordsoftware/react-starter/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/nordsoftware/react-starter/?branch=master) 9 | [![StyleCI](https://styleci.io/repos/47612236/shield?style=flat)](https://styleci.io/repos/47612236) 10 | [![npm version](https://img.shields.io/npm/v/react-starter.svg)](https://www.npmjs.com/package/react-starter) 11 | [![npm downloads](https://img.shields.io/npm/dt/react-starter.svg)](https://www.npmjs.com/package/react-starter) 12 | [![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) 13 | 14 | Starter template for React applications. 15 | 16 | ## Building blocks 17 | 18 | - [React](https://facebook.github.io/react/) User interface components 19 | - [React + Foundation](https://github.com/nordsoftware/react-foundation/) Foundation as React components 20 | - [Redux](http://redux.js.org/) Predictable state container 21 | - [Immutable](https://facebook.github.io/immutable-js/) Immutable collections 22 | - [Lodash](https://lodash.com/) Utility library 23 | - [Babel](https://babeljs.io/) ES.Next transpiler 24 | - [Eslint](http://eslint.org/) Linting utility 25 | - [Webpack](https://webpack.github.io/) Module bundler 26 | - [Foundation](http://foundation.zurb.com/sites.html) CSS framework 27 | - [SASS](http://sass-lang.com/) CSS pre-processor 28 | - [Mocha](https://mochajs.org/) Testing framework 29 | - [Chai](http://chaijs.com/) Assertation library 30 | 31 | ## Install 32 | 33 | Run the following command to install the dependencies: 34 | 35 | ```bash 36 | npm install 37 | ``` 38 | 39 | ## Usage 40 | 41 | ### Development server 42 | 43 | Run the following command to start the development server: 44 | 45 | ```bash 46 | npm start 47 | ``` 48 | 49 | ### Distribution build 50 | 51 | Run the following command to build the distribution build: 52 | 53 | ```bash 54 | npm run dist 55 | ``` 56 | 57 | ## Test 58 | 59 | Run the following command to run the test suite: 60 | 61 | ```bash 62 | npm test 63 | ``` 64 | 65 | Alternatively you can run the test suite in watch mode: 66 | 67 | ``` 68 | npm run test:watch 69 | ``` 70 | 71 | ## License 72 | 73 | See [LICENSE](LICENSE). 74 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nord-react-starter", 3 | "version": "1.0.0", 4 | "description": "React starter template from Nord Software.", 5 | "main": "src/main.js", 6 | "scripts": { 7 | "start": "webpack-dev-server --inline --progress --colors --history-api-fallback --config webpack/development.js", 8 | "dist": "webpack -p --config webpack/production.js", 9 | "lint": "eslint src", 10 | "test": "mocha --compilers js:babel-core/register --require test/index.js --recursive", 11 | "test:cover": "istanbul cover _mocha -- --compilers js:babel-core/register --require test/index.js --recursive", 12 | "test:watch": "npm run test -- --watch" 13 | }, 14 | "authors": [ 15 | "Christoffer Niska " 16 | ], 17 | "license": "MIT", 18 | "devDependencies": { 19 | "chai": "^3.5.0", 20 | "chai-enzyme": "^0.4.1", 21 | "chai-immutable": "^1.5.3", 22 | "chai-jsx": "^1.0.1", 23 | "cheerio": "^0.20.0", 24 | "enzyme": "^2.1.0", 25 | "istanbul": "^1.0.0-alpha.2", 26 | "jsdom": "^8.1.0", 27 | "mocha": "^2.4.5", 28 | "react": "^0.14.7", 29 | "sinon": "^1.17.3", 30 | "sinon-chai": "^2.8.0", 31 | "webpack-dev-server": "^1.12.1" 32 | }, 33 | "dependencies": { 34 | "babel-core": "^6.2.1", 35 | "babel-eslint": "^6.0.4", 36 | "babel-loader": "^6.2.0", 37 | "babel-preset-es2015": "^6.1.18", 38 | "babel-preset-react": "^6.1.18", 39 | "babel-preset-stage-2": "^6.1.18", 40 | "css-loader": "^0.23.0", 41 | "dotenv": "^2.0.0", 42 | "eslint": "^2.9.0", 43 | "eslint-loader": "^1.3.0", 44 | "eslint-plugin-react": "^4.1.0", 45 | "exports-loader": "^0.6.2", 46 | "file-loader": "^0.8.5", 47 | "foundation-sites": "^6.1.1", 48 | "html-webpack-plugin": "^1.7.0", 49 | "immutable": "^3.7.6", 50 | "imports-loader": "^0.6.5", 51 | "json-loader": "^0.5.4", 52 | "lodash": "^4.6.1", 53 | "node-sass": "^3.4.2", 54 | "react": "^0.14.3", 55 | "react-dom": "^0.14.3", 56 | "react-foundation": "^0.6.3", 57 | "react-hot-loader": "^1.3.0", 58 | "react-redux": "^4.0.6", 59 | "react-router": "^2.0.0-rc4", 60 | "react-router-redux": "^4.0.0", 61 | "redux": "^3.0.5", 62 | "redux-thunk": "^1.0.3", 63 | "require-dir": "^0.3.0", 64 | "sass-loader": "^3.1.2", 65 | "source-map-loader": "^0.1.5", 66 | "style-loader": "^0.13.0", 67 | "url-loader": "^0.5.7", 68 | "webpack": "^1.12.8" 69 | }, 70 | "babel": { 71 | "presets": [ 72 | "es2015", 73 | "react", 74 | "stage-2" 75 | ] 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/components/hello/_hello.scss: -------------------------------------------------------------------------------- 1 | .hello { 2 | &__text { 3 | margin: rem-calc(40 0); 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/components/hello/hello.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { Row, Column } from 'react-foundation'; 4 | import { Navbar } from '../navbar/navbar'; 5 | 6 | export const Hello = ({ isAuthenticated }) => ( 7 |
8 | 9 | 10 | 11 |

Hello from React!

12 |
13 |
14 |
15 | ); 16 | 17 | function mapStateToProps(state) { 18 | return { 19 | isAuthenticated: state.auth.get('isAuthenticated') 20 | }; 21 | } 22 | 23 | export const HelloContainer = connect(mapStateToProps)(Hello); 24 | -------------------------------------------------------------------------------- /src/components/login/_login.scss: -------------------------------------------------------------------------------- 1 | .login { 2 | &__title { 3 | font-size: rem-calc(30); 4 | font-weight: bold; 5 | padding: rem-calc(40 0); 6 | text-align: center; 7 | } 8 | 9 | &__box { 10 | box-shadow: 0 0 2px rgba(0, 0, 0, 0.2); 11 | background: white; 12 | margin: auto; 13 | padding: rem-calc(20); 14 | width: 300px; 15 | } 16 | 17 | &__submit { 18 | margin-bottom: 0; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/components/login/login.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { push } from 'react-router-redux'; 4 | import { login } from '../../state/auth/actions'; 5 | import { debug } from '../../helpers/log'; 6 | import { Colors, Button } from 'react-foundation'; 7 | 8 | export class Login extends Component { 9 | componentWillReceiveProps(nextProps) { 10 | if (nextProps.isAuthenticated) { 11 | this.props.onSuccess(); 12 | } 13 | 14 | if (nextProps.errorMessage) { 15 | this.props.onError(nextProps.errorMessage); 16 | } 17 | } 18 | 19 | render() { 20 | const handleSubmit = event => { 21 | event.preventDefault(); 22 | 23 | this.props.onSubmit(this.refs.email.value, this.refs.password.value); 24 | }; 25 | 26 | return ( 27 |
28 |

React starter

29 |
30 |
31 | 35 | 39 | 40 |
41 |
42 |
43 | ); 44 | } 45 | } 46 | 47 | function mapStateToProps(state) { 48 | return { 49 | isAuthenticated: state.auth.get('isAuthenticated'), 50 | errorMessage: state.auth.get('errorMessage'), 51 | isLoading: state.auth.getIn(['session', 'isLoading']) 52 | }; 53 | } 54 | 55 | function mapDispatchToProps(dispatch) { 56 | return { 57 | onSubmit: (username, password) => { 58 | dispatch(login(username, password)); 59 | }, 60 | onSuccess: () => { 61 | dispatch(push('/')); 62 | }, 63 | onError: (message) => { 64 | debug(message); 65 | } 66 | }; 67 | } 68 | 69 | export const LoginContainer = connect(mapStateToProps, mapDispatchToProps)(Login); 70 | -------------------------------------------------------------------------------- /src/components/logout/Logout.js: -------------------------------------------------------------------------------- 1 | /*eslint no-unused-vars: 0*/ 2 | 3 | import React, { Component, PropTypes } from 'react'; 4 | import { connect } from 'react-redux'; 5 | import { push } from 'react-router-redux'; 6 | import { logout } from '../../state/auth/actions'; 7 | 8 | export class Logout extends Component { 9 | componentWillMount() { 10 | this.props.onMount(); 11 | } 12 | 13 | render() { 14 | return ( 15 |
You have been logged out.
16 | ); 17 | } 18 | } 19 | 20 | Logout.propTypes = { 21 | onMount: PropTypes.func 22 | }; 23 | 24 | function mapDispatchToProps(dispatch) { 25 | return { 26 | onMount: () => { 27 | dispatch(logout()); 28 | dispatch(push('/login')); 29 | } 30 | }; 31 | } 32 | 33 | export const LogoutContainer = connect(null, mapDispatchToProps)(Logout); 34 | -------------------------------------------------------------------------------- /src/components/navbar/_navbar.scss: -------------------------------------------------------------------------------- 1 | .navbar { 2 | &__title > a { 3 | color: #222; 4 | display: inline-block; 5 | font-weight: bold; 6 | margin-top: 7px; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/components/navbar/navbar.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | import { Link } from 'react-router'; 3 | import { 4 | Row, 5 | Column, 6 | TopBar, 7 | TopBarTitle, 8 | TopBarRight, 9 | Menu, 10 | MenuItem 11 | } from 'react-foundation'; 12 | 13 | export const Navbar = ({ isAuthenticated }) => ( 14 | 15 | 16 | 17 | React starter 18 | 19 | 20 | 21 | {!isAuthenticated ? Log in : Log out} 22 | 23 | 24 | 25 | 26 | 27 | 28 | ); 29 | 30 | Navbar.propTypes = { 31 | isAuthenticated: PropTypes.bool 32 | }; 33 | -------------------------------------------------------------------------------- /src/components/not-found/_not-found.scss: -------------------------------------------------------------------------------- 1 | .not-found { 2 | margin: rem-calc(40) 0; 3 | } 4 | -------------------------------------------------------------------------------- /src/components/not-found/not-found.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Row, Column } from 'react-foundation'; 3 | 4 | export const NotFound = () => ( 5 |
6 | 7 | 8 | Page not found 9 | 10 | 11 |
12 | ); 13 | -------------------------------------------------------------------------------- /src/helpers/api.js: -------------------------------------------------------------------------------- 1 | /*eslint consistent-return: 0*/ 2 | /*eslint no-undef: 0*/ 3 | 4 | import { forEach, isUndefined } from 'lodash'; 5 | import { push } from 'react-router-redux'; 6 | import { getIsAuthenticated, getAccessToken, getRefreshToken } from './auth'; 7 | import { refreshSuccess } from '../state/auth/actions'; 8 | 9 | if (isUndefined(API_URL)) { 10 | throw new Error('API_URL must be set.'); 11 | } 12 | 13 | /** 14 | * 15 | * @param {string} url 16 | * @param {Object} init 17 | * @param {function} dispatch 18 | * @returns {Promise} 19 | */ 20 | export function fetchFromApi(url, init, dispatch) { 21 | return new Promise((resolve, reject) => { 22 | fetch(buildRequest(url, init)) 23 | .then(response => { 24 | switch (response.status) { 25 | case 401: 26 | if (response.url === buildApiUrl('auth/login')) { 27 | // Login failed 28 | resolve(response); 29 | } 30 | 31 | // Access denied (Access token has expired) 32 | const refreshToken = getRefreshToken(); 33 | 34 | if (refreshToken) { 35 | return refresh(refreshToken, dispatch) 36 | .then(result => { 37 | dispatch(refreshSuccess(result)); 38 | 39 | fetch(buildRequest(url, init)).then(res => resolve(res)); 40 | }); 41 | } 42 | 43 | return reject(); 44 | 45 | case 403: 46 | // Forbidden 47 | if (response.url === buildApiUrl('auth/refresh')) { 48 | // Refresh token has expired, log out the user 49 | dispatch(push('/logout')); 50 | } 51 | 52 | return resolve(response); 53 | 54 | default: 55 | // Ok 56 | return resolve(response); 57 | } 58 | }) 59 | .catch(err => reject(err)); 60 | }); 61 | } 62 | 63 | /** 64 | * 65 | * @param {string} url 66 | * @param {Object} init 67 | * @returns {Request} 68 | */ 69 | export function buildRequest(url, init) { 70 | const request = new Request(buildApiUrl(url), { ...init, mode: 'cors' }); 71 | 72 | request.headers.set('Accept', 'application/json'); 73 | 74 | if (getIsAuthenticated()) { 75 | request.headers.set('Authorization', `Bearer ${getAccessToken()}`); 76 | } 77 | 78 | return request; 79 | } 80 | 81 | /** 82 | * 83 | * @param {string} token 84 | * @param {function} dispatch 85 | * @returns {Promise} 86 | */ 87 | function refresh(token, dispatch) { 88 | return new Promise((resolve, reject) => { 89 | const form = new FormData(); 90 | 91 | form.append('grant_type', 'refresh_token'); 92 | form.append('client_id', OAUTH_CLIENT_ID); 93 | form.append('client_secret', OAUTH_CLIENT_SECRET); 94 | form.append('refresh_token', token); 95 | 96 | fetchFromApi('auth/refresh', { method: 'POST', body: form }, dispatch) 97 | .then(response => response.status === 200 98 | ? response.json().then(data => resolve({ response, data })) 99 | : reject(response)) 100 | .catch(err => reject(err)); 101 | }); 102 | } 103 | 104 | /** 105 | * 106 | * @param {Object} query 107 | * @returns {string} 108 | */ 109 | export function buildQueryString(query) { 110 | const pairs = []; 111 | 112 | forEach(query, (value, key) => { 113 | pairs.push([key, value].join('=')); 114 | }); 115 | 116 | return pairs.length ? '?' + pairs.join('&') : ''; 117 | } 118 | 119 | /** 120 | * 121 | * @param {string} url 122 | * @returns {string} 123 | */ 124 | function buildApiUrl(url) { 125 | return [API_URL, url].join('/'); 126 | } 127 | -------------------------------------------------------------------------------- /src/helpers/auth.js: -------------------------------------------------------------------------------- 1 | /*eslint no-undef: 0*/ 2 | 3 | import { getStorageItem, setStorageItem, removeStorageItem } from './storage'; 4 | 5 | import { isUndefined } from 'lodash'; 6 | 7 | if (isUndefined(OAUTH_CLIENT_ID)) { 8 | throw new Error('OAUTH_CLIENT_ID must be set.'); 9 | } 10 | 11 | if (isUndefined(OAUTH_CLIENT_SECRET)) { 12 | throw new Error('OAUTH_CLIENT_SECRET must be set.'); 13 | } 14 | 15 | /** 16 | * 17 | * @param {Object} nextState 18 | * @param {function} replace 19 | */ 20 | export function requireAuth(nextState, replace) { 21 | if (!getIsAuthenticated()) { 22 | replace('login'); 23 | } 24 | } 25 | 26 | /** 27 | * 28 | * @param {Object} nextState 29 | * @param {function} replace 30 | */ 31 | export function requireGuest(nextState, replace) { 32 | if (getIsAuthenticated()) { 33 | replace('/'); 34 | } 35 | } 36 | 37 | /** 38 | * 39 | * @param {Object} session 40 | */ 41 | export function setSession(session) { 42 | setStorageItem('auth_session', session); 43 | } 44 | 45 | /** 46 | * 47 | * @param {Object} user 48 | */ 49 | export function setUser(user) { 50 | setStorageItem('auth_user', user); 51 | } 52 | /** 53 | * 54 | * @returns {Object} 55 | */ 56 | export function getSession() { 57 | return getStorageItem('auth_session'); 58 | } 59 | /** 60 | * 61 | * @returns {Object} 62 | */ 63 | export function getUser() { 64 | return getStorageItem('auth_user'); 65 | } 66 | 67 | /** 68 | * 69 | */ 70 | export function removeSession() { 71 | removeStorageItem('auth_session'); 72 | } 73 | 74 | /** 75 | * 76 | */ 77 | export function removeUser() { 78 | removeStorageItem('auth_user'); 79 | } 80 | 81 | /** 82 | * 83 | * @returns {boolean} 84 | */ 85 | export function getIsAuthenticated() { 86 | return Boolean(getAccessToken()); 87 | } 88 | 89 | /** 90 | * 91 | * @returns {string|null} 92 | */ 93 | export function getAccessToken() { 94 | return getStorageItem('auth_session.access_token'); 95 | } 96 | 97 | /** 98 | * 99 | * @returns {string|null} 100 | */ 101 | export function getRefreshToken() { 102 | return getStorageItem('auth_session.refresh_token'); 103 | } 104 | -------------------------------------------------------------------------------- /src/helpers/log.js: -------------------------------------------------------------------------------- 1 | /*eslint no-unused-expressions: 0*/ 2 | /*eslint no-undef: 0*/ 3 | 4 | import { isUndefined } from 'lodash'; 5 | 6 | if (isUndefined(IS_LOGGING)) { 7 | throw new Error('IS_LOGGING must be set.'); 8 | } 9 | 10 | if (isUndefined(LOGGING_LEVEL)) { 11 | throw new Error('LOGGING_LEVEL must be set.'); 12 | } 13 | 14 | export const LogLevels = { 15 | LOG: 1, 16 | INFO: 2, 17 | WARN: 3, 18 | ERROR: 4, 19 | DEBUG: 5 20 | }; 21 | 22 | export function log() { 23 | shouldLog(LogLevels.LOG) && console.log.apply(console, arguments); 24 | } 25 | 26 | export function info() { 27 | shouldLog(LogLevels.INFO) && console.info.apply(console, arguments); 28 | } 29 | 30 | export function warn() { 31 | shouldLog(LogLevels.WARN) && console.warn.apply(console, arguments); 32 | } 33 | 34 | export function error() { 35 | shouldLog(LogLevels.ERROR) && console.error.apply(console, arguments); 36 | } 37 | 38 | export function debug() { 39 | shouldLog(LogLevels.DEBUG) && console.debug.apply(console, arguments); 40 | } 41 | 42 | function shouldLog(logLevel) { 43 | return Boolean(IS_LOGGING) && LOGGING_LEVEL >= logLevel; 44 | } 45 | -------------------------------------------------------------------------------- /src/helpers/storage.js: -------------------------------------------------------------------------------- 1 | /*eslint no-undef: 0*/ 2 | 3 | import { get, isString, isUndefined } from 'lodash'; 4 | import { debug } from './log'; 5 | 6 | if (isUndefined(STORAGE_PREFIX)) { 7 | throw new Error('STORAGE_PREFIX must be set.'); 8 | } 9 | 10 | /** 11 | * 12 | * @param {string} key 13 | */ 14 | export function getStorageItem(key) { 15 | const [root, ...rest] = key.split('.'); 16 | const item = localStorage.getItem(buildStorageKey(root)); 17 | const value = isJson(item) ? JSON.parse(item) : item; 18 | return rest.length ? get(value, rest.join('.')) : value; 19 | } 20 | 21 | /** 22 | * 23 | * @param {string} key 24 | * @param {*} value 25 | */ 26 | export function setStorageItem(key, value) { 27 | if (!isString(value)) { 28 | value = JSON.stringify(value); 29 | } 30 | localStorage.setItem(buildStorageKey(key), value); 31 | debug('storage item set: %s -> %s', key, value); 32 | } 33 | 34 | /** 35 | * 36 | * @param {string} key 37 | */ 38 | export function removeStorageItem(key) { 39 | localStorage.removeItem(buildStorageKey(key)); 40 | debug('storage item removed: %s', key); 41 | } 42 | 43 | /** 44 | * 45 | * @param {string} key 46 | * @returns {string} 47 | */ 48 | function buildStorageKey(key) { 49 | return [STORAGE_PREFIX, key].join('.'); 50 | } 51 | 52 | /** 53 | * 54 | * @param {*} value 55 | * @returns {boolean} 56 | */ 57 | function isJson(value) { 58 | try { 59 | JSON.parse(value); 60 | } catch (e) { 61 | return false; 62 | } 63 | return true; 64 | } 65 | -------------------------------------------------------------------------------- /src/helpers/store.js: -------------------------------------------------------------------------------- 1 | import { combineReducers, applyMiddleware, createStore } from 'redux'; 2 | import { browserHistory } from 'react-router'; 3 | import { routerMiddleware } from 'react-router-redux'; 4 | import thunk from 'redux-thunk'; 5 | import apiMiddleware from '../middleware/api'; 6 | import * as reducers from '../state/reducers'; 7 | 8 | /** 9 | * 10 | * @param {*} initialState 11 | * @param {Object} handlers 12 | * @returns {function} 13 | * @see http://redux.js.org/docs/recipes/ReducingBoilerplate.html 14 | */ 15 | export function createReducer(initialState, handlers) { 16 | return function reducer(state = initialState, action) { 17 | if (handlers.hasOwnProperty(action.type)) { 18 | return handlers[action.type](state, action); 19 | } else { 20 | return state; 21 | } 22 | }; 23 | } 24 | 25 | /** 26 | * 27 | * @returns {function} 28 | */ 29 | export function buildStore() { 30 | const rootReducer = combineReducers(reducers); 31 | const routerHistoryMiddleware = routerMiddleware(browserHistory); 32 | 33 | return applyMiddleware(thunk, apiMiddleware, routerHistoryMiddleware)(createStore)(rootReducer); 34 | } 35 | -------------------------------------------------------------------------------- /src/main.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | React starter 9 | 10 | 11 |
12 | 13 | 14 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | /*eslint no-undef: 0*/ 2 | 3 | import React from 'react'; 4 | import { render } from 'react-dom'; 5 | import { Router, browserHistory } from 'react-router'; 6 | import { Provider } from 'react-redux'; 7 | import { syncHistoryWithStore } from 'react-router-redux'; 8 | 9 | import { buildStore } from './helpers/store'; 10 | import getRoutes from './routes'; 11 | import './main.scss'; 12 | 13 | const store = buildStore(); 14 | const history = syncHistoryWithStore(browserHistory, store); 15 | 16 | render( 17 | 18 | window.scrollTo(0, 0)} history={history}> 19 | {getRoutes()} 20 | 21 | , 22 | document.getElementById('root') 23 | ); 24 | -------------------------------------------------------------------------------- /src/main.scss: -------------------------------------------------------------------------------- 1 | @import "../node_modules/foundation-sites/scss/foundation"; 2 | 3 | @include foundation-flex-grid; 4 | @include foundation-flex-classes; 5 | @include foundation-everything; 6 | 7 | @import "components/login/login"; 8 | @import "components/hello/hello"; 9 | @import "components/navbar/navbar"; 10 | @import "components/not-found/not-found"; 11 | 12 | body { 13 | background: #f5f5f5; 14 | } 15 | -------------------------------------------------------------------------------- /src/middleware/api.js: -------------------------------------------------------------------------------- 1 | /*eslint consistent-return: 0*/ 2 | 3 | /** 4 | * 5 | * @param {function} dispatch 6 | * @param {function} getState 7 | * @returns {Promise} 8 | */ 9 | export default function apiMiddleware({ dispatch, getState }) { 10 | return next => action => { 11 | const { 12 | types, 13 | callApi, 14 | shouldCallApi = () => true, 15 | payload = {} 16 | } = action; 17 | 18 | if (!types) { 19 | // Normal action: pass it on 20 | return next(action); 21 | } 22 | 23 | if (!Array.isArray(types) || types.length !== 3 || !types.every(type => typeof type === 'string')) { 24 | throw new Error('Expected an array of three string types.'); 25 | } 26 | 27 | if (typeof callApi !== 'function') { 28 | throw new Error('Expected `callApi` to be a function.'); 29 | } 30 | 31 | if (!shouldCallApi(getState())) { 32 | return; 33 | } 34 | 35 | const [ requestType, successType, failureType ] = types; 36 | 37 | dispatch({ ...payload, type: requestType }); 38 | 39 | return callApi(dispatch).then( 40 | response => { 41 | response.json().then( 42 | data => dispatch({ ...payload, response, data, type: successType }) 43 | ); 44 | }, 45 | error => dispatch({ ...payload, error, type: failureType }) 46 | ); 47 | }; 48 | } 49 | -------------------------------------------------------------------------------- /src/routes.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { IndexRoute, Route } from 'react-router'; 3 | 4 | import { requireAuth, requireGuest } from './helpers/auth'; 5 | 6 | import { HelloContainer } from './components/hello/hello'; 7 | import { LoginContainer } from './components/login/login'; 8 | import { LogoutContainer } from './components/logout/logout'; 9 | import { NotFound } from './components/not-found/not-found'; 10 | 11 | export default () => ( 12 | 13 | 14 | 15 | 16 | 17 | 18 | ); 19 | 20 | -------------------------------------------------------------------------------- /src/state/auth/actions.js: -------------------------------------------------------------------------------- 1 | /*eslint no-undef: 0*/ 2 | 3 | import { fetchFromApi } from '../../helpers/api'; 4 | 5 | export const AuthActionTypes = { 6 | LOGIN_REQUEST: 'AUTH/LOGIN_REQUEST', 7 | LOGIN_SUCCESS: 'AUTH/LOGIN_SUCCESS', 8 | LOGIN_FAILURE: 'AUTH/LOGIN_FAILURE', 9 | LOGOUT: 'AUTH/LOGOUT', 10 | USER_LOAD_REQUEST: 'AUTH/USER_LOAD_REQUEST', 11 | USER_LOAD_SUCCESS: 'AUTH/USER_LOAD_SUCCESS', 12 | USER_LOAD_FAILURE: 'AUTH/USER_LOAD_FAILURE' 13 | }; 14 | 15 | /** 16 | * 17 | * @param {string} username 18 | * @param {string }password 19 | * @returns {Object} 20 | */ 21 | export function login(username, password) { 22 | const body = { 23 | grant_type: 'password', 24 | client_id: OAUTH_CLIENT_ID, 25 | client_secret: OAUTH_CLIENT_SECRET, 26 | username, 27 | password 28 | }; 29 | 30 | const init = { 31 | method: 'POST', 32 | body: JSON.stringify(body) 33 | }; 34 | 35 | return { 36 | types: [AuthActionTypes.LOGIN_REQUEST, AuthActionTypes.LOGIN_SUCCESS, AuthActionTypes.LOGIN_FAILURE], 37 | shouldCallApi: state => !state.auth.get('isAuthenticated'), 38 | callApi: dispatch => fetchFromApi('auth/login', init, dispatch) 39 | }; 40 | } 41 | 42 | /** 43 | * 44 | * @returns {Object} 45 | */ 46 | export function loadUser() { 47 | return { 48 | types: [AuthActionTypes.USER_LOAD_REQUEST, AuthActionTypes.USER_LOAD_SUCCESS, AuthActionTypes.USER_LOAD_FAILURE], 49 | shouldCallApi: state => state.auth.get('isAuthenticated'), 50 | callApi: dispatch => fetchFromApi('me', null, dispatch) 51 | }; 52 | } 53 | 54 | /** 55 | * 56 | * @param {Object} result 57 | * @returns {Object} 58 | */ 59 | export function refreshSuccess(result) { 60 | return { type: AuthActionTypes.LOGIN_SUCCESS, ...result }; 61 | } 62 | 63 | /** 64 | * 65 | * @returns {Object} 66 | */ 67 | export function logout() { 68 | return { type: AuthActionTypes.LOGOUT }; 69 | } 70 | -------------------------------------------------------------------------------- /src/state/auth/reducer.js: -------------------------------------------------------------------------------- 1 | /*eslint no-unused-vars: 0*/ 2 | 3 | import { fromJS, Map } from 'immutable'; 4 | import { createReducer } from '../../helpers/store'; 5 | import { AuthActionTypes } from './actions'; 6 | import { 7 | setSession, 8 | setUser, 9 | getSession, 10 | getIsAuthenticated, 11 | getUser, 12 | removeSession, 13 | removeUser 14 | } from '../../helpers/auth'; 15 | 16 | const initialState = fromJS({ 17 | session: getSession(), 18 | user: getUser(), 19 | isAuthenticated: getIsAuthenticated() 20 | }); 21 | 22 | /** 23 | * Called when login request is initiated. 24 | * 25 | * @param {Map} state 26 | * @param {Object} action 27 | * @returns {Map} 28 | */ 29 | export function handleLoginRequest(state, action) { 30 | return state.set('session', fromJS({ isLoading: true })); 31 | } 32 | 33 | /** 34 | * Called when login request is successful. 35 | * 36 | * @param {Map} state 37 | * @param {Object} action 38 | * @returns {Map} 39 | */ 40 | export function handleLoginSuccess(state, action) { 41 | if (action.response.status !== 200) { 42 | return handleLoginFailure(state, { error: action.data.message }); 43 | } 44 | 45 | setSession(action.data); 46 | 47 | return state 48 | .set('session', fromJS(action.data)) 49 | .set('isAuthenticated', true); 50 | } 51 | 52 | /** 53 | * Called when the login request fails. 54 | * 55 | * @param {Map} state 56 | * @param {Object} action 57 | * @returns {Map} 58 | */ 59 | export function handleLoginFailure(state, action) { 60 | return state.set('session', fromJS({ errorMessage: action.error })); 61 | } 62 | 63 | /** 64 | * Called when the user is logged out. 65 | * 66 | * @param {Map} state 67 | * @param {Object} action 68 | * @returns {Map} 69 | */ 70 | export function handleLogout(state, action) { 71 | removeSession(); 72 | removeUser(); 73 | 74 | return state 75 | .delete('session') 76 | .delete('user') 77 | .set('isAuthenticated', false); 78 | } 79 | 80 | /** 81 | * Called when a load request is initiated. 82 | * 83 | * @param {Map} state 84 | * @param {Object} action 85 | * @returns {Map} 86 | */ 87 | export function handleUserLoadRequest(state, action) { 88 | return state.set('user', fromJS({ isLoading: true })); 89 | } 90 | 91 | /** 92 | * Called when the load user is successful. 93 | * 94 | * @param {Map} state 95 | * @param {Object} action 96 | * @returns {Map} 97 | */ 98 | export function handleUserLoadSuccess(state, action) { 99 | if (action.response.status !== 200) { 100 | return handleUserLoadFailure(action, { error: action.data.message }); 101 | } 102 | 103 | setUser(action.data); 104 | 105 | return state.set('user', fromJS(action.data)); 106 | } 107 | 108 | /** 109 | * Called when a load request fails. 110 | * 111 | * @param {Map} state 112 | * @param {Object} action 113 | * @returns {Map} 114 | */ 115 | export function handleUserLoadFailure(state, action) { 116 | return state.set('user', fromJS({ errorMessage: action.error })); 117 | } 118 | 119 | export const authReducer = createReducer(initialState, { 120 | [AuthActionTypes.LOGIN_REQUEST]: handleLoginRequest, 121 | [AuthActionTypes.LOGIN_SUCCESS]: handleLoginSuccess, 122 | [AuthActionTypes.LOGIN_FAILURE]: handleLoginFailure, 123 | [AuthActionTypes.LOGOUT]: handleLogout, 124 | [AuthActionTypes.USER_LOAD_REQUEST]: handleUserLoadRequest, 125 | [AuthActionTypes.USER_LOAD_SUCCESS]: handleUserLoadSuccess, 126 | [AuthActionTypes.USER_LOAD_FAILURE]: handleUserLoadFailure 127 | }); 128 | -------------------------------------------------------------------------------- /src/state/reducers.js: -------------------------------------------------------------------------------- 1 | export { routerReducer as routing } from 'react-router-redux'; 2 | export { authReducer as auth } from './auth/reducer'; 3 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | import chai from 'chai'; 2 | import chaiImmutable from 'chai-immutable'; 3 | import chaiEnzyme from 'chai-enzyme'; 4 | import chaiJsx from 'chai-jsx'; 5 | import sinonChai from 'sinon-chai'; 6 | import { jsdom } from 'jsdom'; 7 | 8 | chai.use(chaiImmutable); 9 | chai.use(chaiEnzyme()); 10 | chai.use(chaiJsx); 11 | chai.use(sinonChai); 12 | 13 | global.document = jsdom(''); 14 | global.window = document.defaultView; 15 | global.navigator = global.window.navigator; 16 | -------------------------------------------------------------------------------- /webpack/config.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | 3 | module.exports = { 4 | entry: { 5 | app: [path.resolve(__dirname, '../src/main.js')] 6 | }, 7 | output: { 8 | path: path.resolve(__dirname, '../dist'), 9 | filename: '[name].[hash].js', 10 | publicPath: '/', 11 | sourceMapFilename: '[name].[hash].js.map', 12 | chunkFilename: '[id].chunk.js' 13 | }, 14 | resolve: { 15 | extensions: ['', '.js', '.jsx'] 16 | }, 17 | module: { 18 | preLoaders: [ 19 | { 20 | test: /\.jsx?$/, 21 | loader: 'source-map' 22 | } 23 | ], 24 | loaders: [ 25 | { 26 | test: /\.jsx?$/, 27 | loader: 'babel?cacheDirectory=true!eslint' 28 | }, 29 | { 30 | test: /\.json$/, 31 | loader: 'json' 32 | }, 33 | { 34 | test: /\.scss$/, 35 | loader: 'style!css!sass?sourceMap' 36 | }, 37 | { 38 | test: /\.(png|jpg|jpeg|gif)$/, 39 | loader: 'url?prefix=img/&limit=5000' 40 | }, 41 | { 42 | test: /\.(mp3|ogg|wav)$/, 43 | loader: 'url?prefix=audio/&limit=5000' 44 | }, 45 | { 46 | test: /\.woff(2)?(\?v=[0-9]\.[0-9]\.[0-9])?$/, 47 | loader: 'url?prefix=font/&limit=5000&mimetype=application/font-woff' 48 | }, 49 | { 50 | test: /\.(ttf|eot|svg)?(\?v=[0-9]\.[0-9]\.[0-9])?$/, 51 | loader: 'file' 52 | } 53 | ] 54 | } 55 | }; 56 | -------------------------------------------------------------------------------- /webpack/development.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var webpack = require('webpack'); 3 | var HtmlWebpackPlugin = require('html-webpack-plugin'); 4 | var helpers = require('./helpers'); 5 | 6 | var environment = helpers.parseDotenvConfig( 7 | require('dotenv').config(path.resolve(__dirname, '../.env')) 8 | ); 9 | 10 | var development = Object.assign({}, { 11 | devtool: 'source-map', 12 | plugins: [ 13 | new webpack.DefinePlugin(Object.assign({}, { 14 | 'process.env.NODE_ENV': '"development"' 15 | }, environment)), 16 | new webpack.ProvidePlugin({ 17 | 'fetch': 'imports?this=>global!exports?global.fetch!whatwg-fetch' 18 | }), 19 | new HtmlWebpackPlugin({ 20 | inject: 'body', 21 | template: 'src/main.html' 22 | }), 23 | new webpack.HotModuleReplacementPlugin(), 24 | new webpack.NoErrorsPlugin() 25 | ] 26 | }, require('./config')); 27 | 28 | development.entry.app.push('webpack-dev-server/client?http://localhost:8080'); 29 | development.entry.app.push('webpack/hot/only-dev-server'); 30 | 31 | module.exports = development; 32 | -------------------------------------------------------------------------------- /webpack/helpers.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * @param {Object} config 4 | * @return {Object} 5 | */ 6 | module.exports = { 7 | parseDotenvConfig: function(config) { 8 | const define = {}; 9 | for (var key in config) { 10 | if (config.hasOwnProperty(key)) { 11 | define[key] = JSON.stringify(config[key]); 12 | } 13 | } 14 | return define; 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /webpack/production.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var webpack = require('webpack'); 3 | var HtmlWebpackPlugin = require('html-webpack-plugin'); 4 | var helpers = require('./helpers'); 5 | 6 | var environment = helpers.parseDotenvConfig( 7 | require('dotenv').config(path.resolve(__dirname, '../.env')) 8 | ); 9 | 10 | module.exports = Object.assign({}, { 11 | plugins: [ 12 | new webpack.DefinePlugin(Object.assign({}, { 13 | 'process.env.NODE_ENV': '"production"' 14 | }, environment)), 15 | new webpack.ProvidePlugin({ 16 | 'fetch': 'imports?this=>global!exports?global.fetch!whatwg-fetch' 17 | }), 18 | new HtmlWebpackPlugin({ 19 | inject: 'body', 20 | template: 'src/main.html' 21 | }), 22 | new webpack.optimize.OccurenceOrderPlugin(), 23 | new webpack.optimize.UglifyJsPlugin({ 24 | compressor: { 25 | warnings: false 26 | } 27 | }) 28 | ] 29 | }, require('./config')); 30 | --------------------------------------------------------------------------------