├── .gitignore
├── README.md
├── package.json
├── src
├── actions
│ ├── login.js
│ └── todo.js
├── components
│ ├── login.js
│ ├── root.js
│ └── todo-list.js
├── constants
│ └── action-types.js
├── index.html
├── index.js
├── middleware
│ └── api.js
├── reducers
│ ├── root.js
│ ├── todos.js
│ └── user.js
├── store
│ └── index.js
└── utils
│ └── http.js
└── webpack.config.js
/.gitignore:
--------------------------------------------------------------------------------
1 | # Created by .ignore support plugin (hsz.mobi)
2 | ### OSX template
3 | .DS_Store
4 | .AppleDouble
5 | .LSOverride
6 |
7 | # Icon must end with two \r
8 | Icon
9 |
10 | # Thumbnails
11 | ._*
12 |
13 | # Files that might appear in the root of a volume
14 | .DocumentRevisions-V100
15 | .fseventsd
16 | .Spotlight-V100
17 | .TemporaryItems
18 | .Trashes
19 | .VolumeIcon.icns
20 |
21 | # Directories potentially created on remote AFP share
22 | .AppleDB
23 | .AppleDesktop
24 | Network Trash Folder
25 | Temporary Items
26 | .apdisk
27 | ### Node template
28 | # Logs
29 | logs
30 | *.log
31 | npm-debug.log*
32 |
33 | # Runtime data
34 | pids
35 | *.pid
36 | *.seed
37 |
38 | # Directory for instrumented libs generated by jscoverage/JSCover
39 | lib-cov
40 |
41 | # Coverage directory used by tools like istanbul
42 | coverage
43 |
44 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
45 | .grunt
46 |
47 | # node-waf configuration
48 | .lock-wscript
49 |
50 | # Compiled binary addons (http://nodejs.org/api/addons.html)
51 | build/Release
52 |
53 | # Dependency directory
54 | # https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git
55 | node_modules
56 | ### JetBrains template
57 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio
58 |
59 | *.iml
60 |
61 | ## Directory-based project format:
62 | .idea/
63 | # if you remove the above rule, at least ignore the following:
64 |
65 | # User-specific stuff:
66 | # .idea/workspace.xml
67 | # .idea/tasks.xml
68 | # .idea/dictionaries
69 |
70 | # Sensitive or high-churn files:
71 | # .idea/dataSources.ids
72 | # .idea/dataSources.xml
73 | # .idea/sqlDataSources.xml
74 | # .idea/dynamic.xml
75 | # .idea/uiDesigner.xml
76 |
77 | # Gradle:
78 | # .idea/gradle.xml
79 | # .idea/libraries
80 |
81 | # Mongo Explorer plugin:
82 | # .idea/mongoSettings.xml
83 |
84 | ## File-based project format:
85 | *.ipr
86 | *.iws
87 |
88 | ## Plugin-specific files:
89 |
90 | # IntelliJ
91 | /out/
92 |
93 | # mpeltonen/sbt-idea plugin
94 | .idea_modules/
95 |
96 | # JIRA plugin
97 | atlassian-ide-plugin.xml
98 |
99 | # Crashlytics plugin (for Android Studio and IntelliJ)
100 | com_crashlytics_export_strings.xml
101 | crashlytics.properties
102 | crashlytics-build.properties
103 |
104 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Backand React Redux example
2 |
3 | To run:
4 |
5 | ```
6 | git clone https://github.com/backand/react_example.git
7 | npm start
8 | ```
9 |
10 | Navigate to [localhost:8080](http://localhost:8080) to see the basic app in action!
11 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "backand-react-demo",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "src/index.js",
6 | "scripts": {
7 | "start": "npm i && webpack-dev-server"
8 | },
9 | "keywords": [
10 | "backand",
11 | "react"
12 | ],
13 | "author": "",
14 | "license": "ISC",
15 | "devDependencies": {
16 | "babel-core": "^6.18.0",
17 | "babel-loader": "6.2.4",
18 | "babel-preset-es2015": "6.6.0",
19 | "babel-preset-react": "6.5.0",
20 | "css-loader": "0.23.1",
21 | "file-loader": "0.8.5",
22 | "style-loader": "0.13.1",
23 | "url-loader": "0.5.7",
24 | "webpack": "1.12.14",
25 | "webpack-dev-server": "1.14.1"
26 | },
27 | "dependencies": {
28 | "bootstrap": "3.3.6",
29 | "classnames": "2.2.3",
30 | "react": "0.14.7",
31 | "react-dom": "0.14.7",
32 | "react-redux": "4.4.1",
33 | "redux": "3.3.1",
34 | "superagent": "1.8.3"
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/actions/login.js:
--------------------------------------------------------------------------------
1 | import {
2 | GET_AUTH_TOKEN_SIMPLE,
3 | LOGIN_SUCCESS,
4 | LOGIN_FAILURE
5 | } from 'constants/action-types';
6 |
7 | export function getAuthTokenSimple(username, password) {
8 | return { type: GET_AUTH_TOKEN_SIMPLE, payload: { username, password, type: 'token' } };
9 | }
10 |
11 | export function useAnonymousAuth() {
12 | return loginSuccess("589603b5-ea43-4551-8162-fe3b2f655e86", 'anonymous');
13 | }
14 |
15 | export function loginSuccess(accessToken, username, authType) {
16 | return { type: LOGIN_SUCCESS, payload: { accessToken, username, authType } };
17 | }
18 |
19 | export function loginFailure(message) {
20 | return { type: LOGIN_FAILURE, payload: { message } };
21 | }
22 |
--------------------------------------------------------------------------------
/src/actions/todo.js:
--------------------------------------------------------------------------------
1 | import {
2 | TODO_GET_ITEMS,
3 | TODO_POST_ITEM,
4 | TODO_GET_ITEMS_SUCCESS,
5 | TODO_POST_ITEM_SUCCESS
6 | } from 'constants/action-types';
7 |
8 | export function getItems() {
9 | return { type: TODO_GET_ITEMS };
10 | }
11 |
12 | export function getItemsSuccess(payload) {
13 | return { type: TODO_GET_ITEMS_SUCCESS, payload };
14 | }
15 |
16 | export function postItem(todo) {
17 | return { type: TODO_POST_ITEM, payload: { todo } }
18 | }
19 |
20 | export function postItemSuccess(id, description) {
21 | const todo = { id, description };
22 | return { type: TODO_POST_ITEM_SUCCESS, payload: { todo } };
23 | }
24 |
--------------------------------------------------------------------------------
/src/components/login.js:
--------------------------------------------------------------------------------
1 | import React, {Component, PropTypes} from 'react';
2 | import {connect} from 'react-redux';
3 | import cx from 'classnames';
4 |
5 | import {getAuthTokenSimple, useAnonymousAuth} from 'actions/login';
6 |
7 | export class Login extends Component {
8 |
9 | render() {
10 |
11 | const errorClasses = cx(
12 | this.props.authError ? 'alert alert-danger' : null
13 | );
14 |
15 | return (
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
31 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 | );
43 | }
44 |
45 | _getAuthTokenSimple() {
46 | const username = this.refs.username.value;
47 | const password = this.refs.password.value;
48 |
49 | this.props.getAuthTokenSimple(username, password);
50 | }
51 |
52 | }
53 |
54 | Login.PropTypes = {
55 | authStatus: PropTypes.string.required,
56 | authType: PropTypes.string.required,
57 | authError: PropTypes.bool.required,
58 | username: PropTypes.string.required
59 | };
60 |
61 | const mapStateToProps = (state, action) => ({
62 | authStatus: state.user.authStatus,
63 | authType: state.user.authType,
64 | authError: state.user.authError,
65 | username: state.user.username
66 | });
67 |
68 | export default connect(mapStateToProps, {
69 | getAuthTokenSimple,
70 | useAnonymousAuth
71 | })(Login);
72 |
--------------------------------------------------------------------------------
/src/components/root.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import Login from 'components/login';
4 | import TodoList from 'components/todo-list';
5 |
6 | export const Root = () => {
7 | return (
8 |
9 |
Hello, Backand!
10 |
11 |
12 |
13 |
14 |
15 | )
16 | };
17 |
--------------------------------------------------------------------------------
/src/components/todo-list.js:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react';
2 | import {connect} from 'react-redux';
3 |
4 | import {getItems, postItem} from 'actions/todo';
5 |
6 | export class TodoList extends Component {
7 |
8 | render() {
9 | return (
10 |
11 |
12 |
13 |
ADD TODO:
14 |
15 |
18 |
21 |
22 |
23 |
24 | { this.renderTodos() }
25 |
26 |
27 |
28 |
29 | );
30 | }
31 |
32 | renderTodos() {
33 | return this.props.todos
34 | .map((todo) => (
36 | { todo.description }
37 | )
38 | );
39 | }
40 |
41 | postItem() {
42 | const todo = this.refs.todo;
43 |
44 | this.props.postItem(todo.value);
45 | todo.value = '';
46 | }
47 | }
48 |
49 | const mapStateToProps = (state) => ({
50 | todos: state.todos
51 | });
52 |
53 | export default connect(mapStateToProps, { getItems, postItem })(TodoList);
54 |
--------------------------------------------------------------------------------
/src/constants/action-types.js:
--------------------------------------------------------------------------------
1 | export const GET_AUTH_TOKEN_SIMPLE = 'GET_AUTH_TOKEN_SIMPLE';
2 | export const LOGIN_SUCCESS = 'LOGIN_SUCCESS';
3 | export const LOGIN_FAILURE = 'LOGIN_FAILURE';
4 |
5 | export const TODO_GET_ITEMS = 'TODO_GET_ITEMS';
6 | export const TODO_GET_ITEMS_SUCCESS = 'TODO_GET_ITEMS_SUCCESS';
7 |
8 | export const TODO_POST_ITEM = 'TODO_POST_ITEM';
9 | export const TODO_POST_ITEM_SUCCESS = 'TODO_POST_ITEM_SUCCESS';
10 |
--------------------------------------------------------------------------------
/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Backand React Redux Demo
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import 'bootstrap/dist/css/bootstrap.min.css';
2 |
3 | import React from 'react';
4 | import ReactDOM from 'react-dom';
5 | import {Provider} from 'react-redux';
6 | import {store} from 'store';
7 |
8 | import {Root} from 'components/root';
9 |
10 | ReactDOM.render((
11 |
12 |
13 |
14 | ), document.getElementById('root'));
15 |
--------------------------------------------------------------------------------
/src/middleware/api.js:
--------------------------------------------------------------------------------
1 | import {GET_AUTH_TOKEN_SIMPLE, TODO_GET_ITEMS, TODO_POST_ITEM} from 'constants/action-types';
2 | import {HTTP} from 'utils/http';
3 | import {loginSuccess, loginFailure} from 'actions/login';
4 | import {getItemsSuccess, postItemSuccess} from 'actions/todo';
5 |
6 | const apiUrl = 'https://api.backand.com';
7 | const appName = 'reactexample';
8 |
9 | export function APIMiddleware({ dispatch, getState }) {
10 | return next => action => {
11 | const state = getState();
12 | const authHeader = HTTP.getAuthHeader(state.user.authType, state.user.accessToken);
13 | const contentHeader = { 'Content-Type': 'application/x-www-form-urlencoded' };
14 |
15 | switch (action.type) {
16 |
17 | case GET_AUTH_TOKEN_SIMPLE:
18 | const { username, password } = action.payload;
19 | const data = { username, password, appName, grant_type: 'password' };
20 |
21 | HTTP.post(`${apiUrl}/token`, data, contentHeader)
22 | .then((data) => dispatch(loginSuccess(data.access_token, data.username, action.payload.type)))
23 | .catch((err) => dispatch(loginFailure(err.body.error_description)));
24 | break;
25 |
26 | case TODO_GET_ITEMS:
27 | HTTP.get(`${apiUrl}/1/objects/todo?returnObject=true`, authHeader)
28 | .then((data) => dispatch(getItemsSuccess(data)))
29 | .catch((err) => console.log(`Error: ${err}`));
30 | break;
31 |
32 | case TODO_POST_ITEM:
33 | const todo = { description: action.payload.todo };
34 | HTTP.post(`${apiUrl}/1/objects/todo?returnObject=true`, todo, authHeader)
35 | .then((data) => dispatch(postItemSuccess(data.id, data.description)))
36 | .catch((err) => console.log(`Error: ${err}`));
37 | break;
38 | }
39 |
40 | return next(action);
41 | };
42 | }
43 |
--------------------------------------------------------------------------------
/src/reducers/root.js:
--------------------------------------------------------------------------------
1 | import {combineReducers} from 'redux';
2 | import {user} from 'reducers/user';
3 | import {todos} from 'reducers/todos';
4 |
5 | export const rootReducer = combineReducers({
6 | user,
7 | todos
8 | });
9 |
--------------------------------------------------------------------------------
/src/reducers/todos.js:
--------------------------------------------------------------------------------
1 | import {TODO_GET_ITEMS_SUCCESS, TODO_POST_ITEM_SUCCESS} from 'constants/action-types';
2 |
3 | const initialState = [];
4 |
5 | export function todos(state = initialState, action) {
6 |
7 | switch (action.type) {
8 | case TODO_GET_ITEMS_SUCCESS:
9 | return [...action.payload.data];
10 |
11 | case TODO_POST_ITEM_SUCCESS:
12 | return [action.payload.todo, ...state];
13 | }
14 |
15 | return state;
16 | }
17 |
--------------------------------------------------------------------------------
/src/reducers/user.js:
--------------------------------------------------------------------------------
1 | import {LOGIN_SUCCESS, LOGIN_FAILURE} from 'constants/action-types';
2 |
3 | const initialState = { accessToken: null, authType: 'N/A' };
4 |
5 | export function user(state = initialState, action) {
6 |
7 | switch (action.type) {
8 |
9 | case LOGIN_SUCCESS:
10 | const { accessToken, username, authType } = action.payload;
11 | return Object.assign({}, state, {
12 | accessToken,
13 | authType,
14 | authStatus: 'Sign in as ',
15 | username
16 | });
17 |
18 | case LOGIN_FAILURE:
19 | return Object.assign({}, state, {
20 | authStatus: action.payload.message,
21 | authError: true
22 | });
23 | }
24 |
25 | return state;
26 |
27 | }
28 |
--------------------------------------------------------------------------------
/src/store/index.js:
--------------------------------------------------------------------------------
1 | import {createStore, applyMiddleware} from 'redux';
2 | import {rootReducer} from 'reducers/root';
3 |
4 | import {APIMiddleware} from 'middleware/api';
5 |
6 | export const store = createStore(rootReducer, applyMiddleware(APIMiddleware));
7 |
--------------------------------------------------------------------------------
/src/utils/http.js:
--------------------------------------------------------------------------------
1 | import superagent from 'superagent';
2 |
3 | export class HTTP {
4 |
5 | static get(url, headers) {
6 | return new Promise((resolve, reject) => {
7 | const request = superagent.get(url);
8 |
9 | for (let key in headers) {
10 | request.set(key, headers[key]);
11 | }
12 |
13 | request.end((err, resp) => {
14 | if (err) {
15 | reject(err.response);
16 | return;
17 | }
18 |
19 | resolve(resp.body);
20 | });
21 | });
22 |
23 | }
24 |
25 | static post(url, data, headers) {
26 | return new Promise((resolve, reject) => {
27 | const request = superagent.post(url)
28 | .send(data);
29 |
30 | for (let key in headers) {
31 | request.set(key, headers[key]);
32 | }
33 |
34 | request.end((err, resp) => {
35 | if (err) {
36 | reject(err.response);
37 | return;
38 | }
39 |
40 | resolve(resp.body);
41 | });
42 | });
43 | }
44 |
45 | static getAuthHeader(authType, token) {
46 | if (authType === 'anonymous') {
47 | return { AnonymousToken: token };
48 | }
49 |
50 | return { Authorization: `Bearer ${token}` };
51 | }
52 |
53 | }
54 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | var path = require('path');
2 | var webpack = require('webpack');
3 | var appPath = path.join(__dirname, 'src');
4 | var exclude = /node_modules/;
5 |
6 | var config = {
7 |
8 | entry: ['index'],
9 |
10 | resolve: {
11 | root: appPath,
12 | extensions: ['', '.webpack.config.js', '.js', '.jsx']
13 | },
14 |
15 | plugins: [
16 | new webpack.HotModuleReplacementPlugin(),
17 | new webpack.NoErrorsPlugin()
18 | ],
19 |
20 | module: {
21 | loaders: [
22 |
23 | {
24 | test: /\.jsx?/,
25 | loader: 'babel',
26 | exclude: exclude,
27 | query: {
28 | presets: ['react', 'es2015']
29 | }
30 | },
31 |
32 | {
33 | test: /\.(css|scss)/,
34 | loader: 'style!css'
35 | },
36 |
37 | { test: /\.(ttf|eot|svg|otf)$/, loader: "file" },
38 | { test: /\.woff(2)?$/, loader: "url?limit=8192&minetype=application/font-woff"}
39 | ]
40 | },
41 |
42 | devServer: {
43 | contentBase: appPath,
44 | colors: true,
45 | noInfo: true,
46 | inline: true
47 | },
48 |
49 | devtool: 'inline-source-map'
50 |
51 | };
52 |
53 | module.exports = config;
54 |
--------------------------------------------------------------------------------