├── .babelrc ├── .eslintrc ├── .gitignore ├── LICENSE ├── README.md ├── dist ├── index.html └── styles.css ├── package.json ├── src ├── components │ ├── App │ │ ├── App.jsx │ │ └── package.json │ ├── Comment │ │ ├── Comment.jsx │ │ └── package.json │ ├── Content │ │ ├── Content.jsx │ │ └── package.json │ ├── DevTools │ │ ├── DevTools.jsx │ │ └── package.json │ ├── Post │ │ ├── Post.jsx │ │ └── package.json │ ├── Question │ │ ├── Question.jsx │ │ └── package.json │ └── User │ │ ├── User.jsx │ │ └── package.json ├── index.jsx └── redux │ ├── actions │ └── test.js │ ├── configureStore.js │ ├── middleware │ └── api.js │ └── reducers │ ├── data.js │ └── index.js └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "es2015", 4 | "react", 5 | "stage-0" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb", 3 | "parser": "babel-eslint", 4 | "rules": { 5 | "import/prefer-default-export": [0], 6 | "import/no-extraneous-dependencies": [0], 7 | "react/prefer-stateless-function": [1] 8 | }, 9 | "globals": { 10 | "document": true, 11 | "FileReader": true, 12 | "FormData": true, 13 | "history": true, 14 | "location": true, 15 | "window": true, 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | dist/bundle.js 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Yury Dymov 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JSON API React Redux Example 2 | Show case for [json-api-normalizer](https://github.com/yury-dymov/json-api-normalizer) and [redux-object](https://github.com/yury-dymov/redux-object). 3 | 4 | DEMO - [https://yury-dymov.github.io/json-api-react-redux-example/](https://yury-dymov.github.io/json-api-react-redux-example/) 5 | 6 | # Description 7 | This is a demo application for [HabraHabr article](https://habrahabr.ru/post/318958/). A lot more details can be found there. 8 | 9 | It represents, how data formatted with [JSON API](http://jsonapi.org/) can be further converted to a more redux-friendly format with [json-api-normalizer](https://github.com/yury-dymov/json-api-normalizer) library. 10 | 11 | The application uses [https://phoenix-json-api-example.herokuapp.com/api/test](https://phoenix-json-api-example.herokuapp.com/api/test) as a JSON API data source, developed with Phoenix Framework. Feel free to check the [API source code](https://github.com/yury-dymov/phoenix-json-api-example) if desired. 12 | 13 | You can also try the demo - [https://yury-dymov.github.io/json-api-react-redux-example/](https://yury-dymov.github.io/json-api-react-redux-example/). 14 | 15 | # Installation 16 | OS X, Linux, and Windows platforms are supported. [Node.js](https://nodejs.org/en/) should be installed, of course. 17 | 18 | * Clone the repo with `git clone https://github.com/yury-dymov/json-api-react-redux-example.git` 19 | * Install dependencies with `npm install` 20 | * Build the JS with `npm run build` 21 | * Run webpack-dev-server with `npm run webpack-dev-server` 22 | 23 | Now you can visit [`http://localhost:8050`](http://localhost:8050) from your browser. 24 | 25 | *Note: Internet access is required to make things work as a backend is initially deployed on Heroku. You may deploy it locally as described [here](https://github.com/yury-dymov/phoenix-json-api-example#installation) and change API_ROOT variable in `src/redux/middleware/api.js` to make things work with no Internet access if desired.* 26 | 27 | # License 28 | MIT (c) Yury Dymov. 29 | -------------------------------------------------------------------------------- /dist/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | json-api-normalizer Demo 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /dist/styles.css: -------------------------------------------------------------------------------- 1 | #react-view { 2 | padding: 20px; 3 | } 4 | 5 | .question { 6 | padding: 10px; 7 | font-size: 30px; 8 | font-weight: bold; 9 | font-style: italic; 10 | } 11 | 12 | .post { 13 | padding: 5px; 14 | margin-left: 10px; 15 | font-size: 20px; 16 | font-weight: normal; 17 | font-style: normal; 18 | } 19 | 20 | .comment { 21 | margin-left: 30px; 22 | font-size: 14px; 23 | } 24 | 25 | .comment::before { 26 | content: "[Comment] " 27 | } 28 | 29 | .user { 30 | font-weight: normal; 31 | font-style: italic; 32 | } 33 | 34 | .comment > .user { 35 | font-size: 14px; 36 | } 37 | 38 | .post > .user { 39 | font-size: 20px; 40 | } 41 | 42 | .btn { 43 | display: inline-block; 44 | margin-bottom: 0; 45 | font-weight: normal; 46 | text-align: center; 47 | vertical-align: middle; 48 | -ms-touch-action: manipulation; 49 | touch-action: manipulation; 50 | cursor: pointer; 51 | background-image: none; 52 | border: 1px solid transparent; 53 | white-space: nowrap; 54 | padding: 10px 15px; 55 | font-size: 15px; 56 | line-height: 1.42857143; 57 | border-radius: 4px; 58 | -webkit-user-select: none; 59 | -moz-user-select: none; 60 | -ms-user-select: none; 61 | user-select: none; 62 | } 63 | .btn:focus, 64 | .btn:active:focus, 65 | .btn.active:focus, 66 | .btn.focus, 67 | .btn:active.focus, 68 | .btn.active.focus { 69 | outline: 5px auto -webkit-focus-ring-color; 70 | outline-offset: -2px; 71 | } 72 | .btn:hover, 73 | .btn:focus, 74 | .btn.focus { 75 | color: #ffffff; 76 | text-decoration: none; 77 | } 78 | .btn:active, 79 | .btn.active { 80 | outline: 0; 81 | background-image: none; 82 | -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); 83 | box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); 84 | } 85 | .btn.disabled, 86 | .btn[disabled], 87 | fieldset[disabled] .btn { 88 | cursor: not-allowed; 89 | opacity: 0.65; 90 | filter: alpha(opacity=65); 91 | -webkit-box-shadow: none; 92 | box-shadow: none; 93 | } 94 | a.btn.disabled, 95 | fieldset[disabled] a.btn { 96 | pointer-events: none; 97 | } 98 | .btn-default { 99 | color: #ffffff; 100 | background-color: #95a5a6; 101 | border-color: #95a5a6; 102 | } 103 | .btn-default:focus, 104 | .btn-default.focus { 105 | color: #ffffff; 106 | background-color: #798d8f; 107 | border-color: #566566; 108 | } 109 | .btn-default:hover { 110 | color: #ffffff; 111 | background-color: #798d8f; 112 | border-color: #74898a; 113 | } 114 | .btn-default:active, 115 | .btn-default.active, 116 | .open > .dropdown-toggle.btn-default { 117 | color: #ffffff; 118 | background-color: #798d8f; 119 | border-color: #74898a; 120 | } 121 | .btn-default:active:hover, 122 | .btn-default.active:hover, 123 | .open > .dropdown-toggle.btn-default:hover, 124 | .btn-default:active:focus, 125 | .btn-default.active:focus, 126 | .open > .dropdown-toggle.btn-default:focus, 127 | .btn-default:active.focus, 128 | .btn-default.active.focus, 129 | .open > .dropdown-toggle.btn-default.focus { 130 | color: #ffffff; 131 | background-color: #687b7c; 132 | border-color: #566566; 133 | } 134 | .btn-default:active, 135 | .btn-default.active, 136 | .open > .dropdown-toggle.btn-default { 137 | background-image: none; 138 | } 139 | .btn-default.disabled:hover, 140 | .btn-default[disabled]:hover, 141 | fieldset[disabled] .btn-default:hover, 142 | .btn-default.disabled:focus, 143 | .btn-default[disabled]:focus, 144 | fieldset[disabled] .btn-default:focus, 145 | .btn-default.disabled.focus, 146 | .btn-default[disabled].focus, 147 | fieldset[disabled] .btn-default.focus { 148 | background-color: #95a5a6; 149 | border-color: #95a5a6; 150 | } 151 | .btn-default .badge { 152 | color: #95a5a6; 153 | background-color: #ffffff; 154 | } 155 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "json-api-react-redux-example", 3 | "version": "1.0.0", 4 | "description": "React Application With Redux And JSON API", 5 | "scripts": { 6 | "build": "better-npm-run build", 7 | "webpack-dev-server": "better-npm-run webpack-dev-server", 8 | "lint": "eslint src --ext '.js,.jsx'" 9 | }, 10 | "betterScripts": { 11 | "build": { 12 | "command": "webpack -p", 13 | "env": { 14 | "NODE_ENV": "production" 15 | } 16 | }, 17 | "webpack-dev-server": { 18 | "command": "webpack-dev-server --debug --hot --devtool eval-source-map --output-pathinfo --watch --colors --inline --content-base dist --port 8050 --host 0.0.0.0", 19 | "env": { 20 | "BABEL_ENV": "dev" 21 | } 22 | } 23 | }, 24 | "repository": { 25 | "type": "git", 26 | "url": "git+https://github.com/yury-dymov/json-api-react-redux-example.git" 27 | }, 28 | "author": "Yury Dymov", 29 | "license": "MIT", 30 | "bugs": { 31 | "url": "https://github.com/yury-dymov/json-api-react-redux-example/issues" 32 | }, 33 | "homepage": "https://github.com/yury-dymov/json-api-react-redux-example#readme", 34 | "devDependencies": { 35 | "babel-core": "^6.21.0", 36 | "babel-eslint": "^7.1.1", 37 | "babel-loader": "^6.2.10", 38 | "babel-polyfill": "^6.20.0", 39 | "babel-preset-es2015": "^6.18.0", 40 | "babel-preset-react": "^6.16.0", 41 | "babel-preset-stage-0": "^6.16.0", 42 | "better-npm-run": "0.0.13", 43 | "eslint": "^3.12.2", 44 | "eslint-config-airbnb": "^13.0.0", 45 | "eslint-loader": "^1.6.1", 46 | "eslint-plugin-import": "^2.2.0", 47 | "eslint-plugin-jsx-a11y": "^2.2.3", 48 | "eslint-plugin-react": "^6.8.0", 49 | "extract-text-webpack-plugin": "^1.0.1", 50 | "redux-devtools": "^3.3.1", 51 | "redux-devtools-dock-monitor": "^1.1.1", 52 | "redux-devtools-log-monitor": "^1.1.1", 53 | "webpack": "^1.14.0", 54 | "webpack-dev-server": "^1.16.2" 55 | }, 56 | "dependencies": { 57 | "bluebird": "^3.4.6", 58 | "isomorphic-fetch": "^2.2.1", 59 | "json-api-normalizer": "0.1.0", 60 | "react": "^15.4.1", 61 | "react-bootstrap": "^0.30.7", 62 | "react-bootstrap-button-loader": "^1.0.7", 63 | "react-dom": "^15.4.1", 64 | "react-redux": "^5.0.1", 65 | "redux": "^3.6.0", 66 | "redux-object": "0.0.2", 67 | "redux-thunk": "^2.1.0" 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/components/App/App.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Content from '../../components/Content'; 3 | import DevTools from '../../components/DevTools'; 4 | 5 | function App() { 6 | return ( 7 |
8 | 9 | 10 |
11 | ); 12 | } 13 | 14 | export default App; 15 | -------------------------------------------------------------------------------- /src/components/App/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "App", 3 | "version": "0.0.0", 4 | "private": true, 5 | "main": "./App" 6 | } 7 | -------------------------------------------------------------------------------- /src/components/Comment/Comment.jsx: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | import User from '../User'; 3 | 4 | const propTypes = { 5 | comment: PropTypes.object.isRequired, 6 | }; 7 | 8 | function Comment({ comment }) { 9 | return ( 10 |
11 | 12 | {comment.text} 13 |
14 | ); 15 | } 16 | 17 | Comment.propTypes = propTypes; 18 | 19 | export default Comment; 20 | -------------------------------------------------------------------------------- /src/components/Comment/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Comment", 3 | "version": "0.0.0", 4 | "private": true, 5 | "main": "./Comment" 6 | } 7 | -------------------------------------------------------------------------------- /src/components/Content/Content.jsx: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import Button from 'react-bootstrap-button-loader'; 4 | import build from 'redux-object'; 5 | import { test } from '../../redux/actions/test'; 6 | import Question from '../Question'; 7 | 8 | const propTypes = { 9 | dispatch: PropTypes.func.isRequired, 10 | questions: PropTypes.array.isRequired, 11 | loading: PropTypes.bool, 12 | }; 13 | 14 | function Content({ loading = false, dispatch, questions }) { 15 | function fetchData() { 16 | dispatch(test()); 17 | } 18 | 19 | const qWidgets = questions.map(q => ); 20 | 21 | return ( 22 |
23 | 24 | {qWidgets} 25 |
26 | ); 27 | } 28 | 29 | Content.propTypes = propTypes; 30 | 31 | function mapStateToProps(state) { 32 | if (state.data.meta['/test']) { 33 | const questions = (state.data.meta['/test'].data || []).map(object => build(state.data, 'question', object.id)); 34 | const loading = state.data.meta['/test'].loading; 35 | 36 | return { questions, loading }; 37 | } 38 | 39 | return { questions: [] }; 40 | } 41 | 42 | export default connect(mapStateToProps)(Content); 43 | -------------------------------------------------------------------------------- /src/components/Content/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Content", 3 | "version": "0.0.0", 4 | "private": true, 5 | "main": "./Content" 6 | } 7 | -------------------------------------------------------------------------------- /src/components/DevTools/DevTools.jsx: -------------------------------------------------------------------------------- 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 | export default createDevTools( 7 | 8 | 9 | , 10 | ); 11 | -------------------------------------------------------------------------------- /src/components/DevTools/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "DevTools", 3 | "version": "0.0.0", 4 | "private": true, 5 | "main": "./DevTools" 6 | } 7 | -------------------------------------------------------------------------------- /src/components/Post/Post.jsx: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | import Comment from '../Comment'; 3 | import User from '../User'; 4 | 5 | const propTypes = { 6 | post: PropTypes.object.isRequired, 7 | }; 8 | 9 | function Post({ post }) { 10 | const commentWidgets = post.comments.map(c => ); 11 | 12 | return ( 13 |
14 | 15 | {post.text} 16 | {commentWidgets} 17 |
18 | ); 19 | } 20 | 21 | Post.propTypes = propTypes; 22 | 23 | export default Post; 24 | -------------------------------------------------------------------------------- /src/components/Post/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Post", 3 | "version": "0.0.0", 4 | "private": true, 5 | "main": "./Post" 6 | } 7 | -------------------------------------------------------------------------------- /src/components/Question/Question.jsx: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | import Post from '../Post'; 3 | 4 | const propTypes = { 5 | question: PropTypes.object.isRequired, 6 | }; 7 | 8 | function Question({ question }) { 9 | const postWidgets = question.posts.map(post => ); 10 | 11 | return ( 12 |
13 | {question.text} 14 | {postWidgets} 15 |
16 | ); 17 | } 18 | 19 | Question.propTypes = propTypes; 20 | 21 | export default Question; 22 | -------------------------------------------------------------------------------- /src/components/Question/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Question", 3 | "version": "0.0.0", 4 | "private": true, 5 | "main": "./Question" 6 | } 7 | -------------------------------------------------------------------------------- /src/components/User/User.jsx: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | 3 | const propTypes = { 4 | user: PropTypes.object.isRequired, 5 | }; 6 | 7 | function User({ user }) { 8 | return {user.name}: ; 9 | } 10 | 11 | User.propTypes = propTypes; 12 | 13 | export default User; 14 | -------------------------------------------------------------------------------- /src/components/User/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "User", 3 | "version": "0.0.0", 4 | "private": true, 5 | "main": "./User" 6 | } 7 | -------------------------------------------------------------------------------- /src/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { Provider } from 'react-redux'; 4 | import App from './components/App'; 5 | import configureStore from './redux/configureStore'; 6 | 7 | const store = configureStore(); 8 | 9 | ReactDOM.render( 10 | 11 | 12 | , 13 | document.getElementById('react-view'), 14 | ); 15 | -------------------------------------------------------------------------------- /src/redux/actions/test.js: -------------------------------------------------------------------------------- 1 | import { CALL_API } from '../middleware/api'; 2 | 3 | export function test() { 4 | return { 5 | [CALL_API]: { 6 | endpoint: '/test', 7 | }, 8 | }; 9 | } 10 | -------------------------------------------------------------------------------- /src/redux/configureStore.js: -------------------------------------------------------------------------------- 1 | import { applyMiddleware, createStore, compose } from 'redux'; 2 | import thunk from 'redux-thunk'; 3 | import rootReducer from './reducers'; 4 | import api from './middleware/api'; 5 | import DevTools from '../components/DevTools'; 6 | 7 | export default function (initialState = {}) { 8 | const store = createStore(rootReducer, initialState, compose( 9 | applyMiddleware(thunk, api), 10 | DevTools.instrument(), 11 | )); 12 | 13 | if (module.hot) { 14 | module.hot.accept('./reducers', () => store.replaceReducer(require('./reducers').default)); // eslint-disable-line global-require, max-len 15 | } 16 | 17 | return store; 18 | } 19 | -------------------------------------------------------------------------------- /src/redux/middleware/api.js: -------------------------------------------------------------------------------- 1 | import fetch from 'isomorphic-fetch'; 2 | import normalize from 'json-api-normalizer'; 3 | 4 | const API_ROOT = 'https://phoenix-json-api-example.herokuapp.com/api'; 5 | 6 | export const API_DATA_REQUEST = 'API_DATA_REQUEST'; 7 | export const API_DATA_SUCCESS = 'API_DATA_SUCCESS'; 8 | export const API_DATA_FAILURE = 'API_DATA_FAILURE'; 9 | 10 | function callApi(endpoint, options = {}) { 11 | const fullUrl = (endpoint.indexOf(API_ROOT) === -1) ? API_ROOT + endpoint : endpoint; 12 | 13 | return fetch(fullUrl, options) 14 | .then(response => response.json() 15 | .then((json) => { 16 | if (!response.ok) { 17 | return Promise.reject(json); 18 | } 19 | 20 | return Object.assign({}, normalize(json, { endpoint })); 21 | }), 22 | ); 23 | } 24 | 25 | 26 | export const CALL_API = Symbol('Call API'); 27 | 28 | export default function (store) { 29 | return function nxt(next) { 30 | return function call(action) { 31 | const callAPI = action[CALL_API]; 32 | 33 | if (typeof callAPI === 'undefined') { 34 | return next(action); 35 | } 36 | 37 | let { endpoint } = callAPI; 38 | const { options } = callAPI; 39 | 40 | if (typeof endpoint === 'function') { 41 | endpoint = endpoint(store.getState()); 42 | } 43 | 44 | if (typeof endpoint !== 'string') { 45 | throw new Error('Specify a string endpoint URL.'); 46 | } 47 | 48 | const actionWith = (data) => { 49 | const finalAction = Object.assign({}, action, data); 50 | delete finalAction[CALL_API]; 51 | return finalAction; 52 | }; 53 | 54 | next(actionWith({ type: API_DATA_REQUEST, endpoint })); 55 | 56 | return callApi(endpoint, options || {}) 57 | .then( 58 | response => next(actionWith({ response, type: API_DATA_SUCCESS, endpoint })), 59 | error => next(actionWith({ type: API_DATA_FAILURE, error: error.message || 'Something bad happened' })), 60 | ); 61 | }; 62 | }; 63 | } 64 | -------------------------------------------------------------------------------- /src/redux/reducers/data.js: -------------------------------------------------------------------------------- 1 | import merge from 'lodash/merge'; 2 | import { API_DATA_REQUEST, API_DATA_SUCCESS } from '../middleware/api'; 3 | 4 | const initialState = { 5 | meta: {}, 6 | }; 7 | 8 | export default function (state = initialState, action) { 9 | switch (action.type) { 10 | case API_DATA_SUCCESS: 11 | return merge( 12 | {}, 13 | state, 14 | merge({}, action.response, { meta: { [action.endpoint]: { loading: false } } }), 15 | ); 16 | case API_DATA_REQUEST: 17 | return merge({}, state, { meta: { [action.endpoint]: { loading: true } } }); 18 | default: 19 | return state; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/redux/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import data from './data'; 3 | 4 | export default combineReducers({ 5 | data, 6 | }); 7 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | global.Promise = require('bluebird'); 2 | 3 | var webpack = require('webpack'); 4 | var path = require('path'); 5 | var ExtractTextPlugin = require('extract-text-webpack-plugin'); 6 | 7 | var publicPath = '/'; 8 | var cssName = 'styles.css'; 9 | var jsName = 'bundle.js'; 10 | 11 | var plugins = [ 12 | new webpack.DefinePlugin({ 13 | 'process.env': { 14 | BROWSER: JSON.stringify(true), 15 | NODE_ENV: JSON.stringify(process.env.NODE_ENV || 'development') 16 | } 17 | }), 18 | new ExtractTextPlugin(cssName) 19 | ]; 20 | 21 | if (process.env.NODE_ENV === 'production') { 22 | plugins.push(new webpack.optimize.DedupePlugin()); 23 | plugins.push(new webpack.optimize.OccurenceOrderPlugin()); 24 | } 25 | 26 | module.exports = { 27 | entry: ['babel-polyfill', './src/index'], 28 | debug: process.env.NODE_ENV !== 'production', 29 | resolve: { 30 | root: path.join(__dirname, 'src'), 31 | modulesDirectories: ['node_modules'], 32 | extensions: ['', '.js', '.jsx'] 33 | }, 34 | plugins, 35 | output: { 36 | path: `${__dirname}/dist`, 37 | filename: jsName, 38 | publicPath 39 | }, 40 | module: { 41 | loaders: [ 42 | { test: /\.jss?x?l?$/, loader: process.env.NODE_ENV !== 'production' ? 'babel!eslint-loader' : 'babel', exclude: [/node_modules/, /public/] }, 43 | ] 44 | }, 45 | eslint: { 46 | configFile: '.eslintrc' 47 | }, 48 | devtool: process.env.NODE_ENV !== 'production' ? 'source-map' : null, 49 | devServer: { 50 | headers: { 'Access-Control-Allow-Origin': '*' } 51 | } 52 | }; 53 | --------------------------------------------------------------------------------