├── .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 | --------------------------------------------------------------------------------