├── .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 | { fetchData(); }}>Fetch Data from API
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 |
--------------------------------------------------------------------------------