├── .babelrc ├── .editorconfig ├── .eslintrc ├── .gitignore ├── .jshintrc ├── LICENSE ├── README.md ├── package.json ├── src ├── actions │ ├── actionTypes.js │ ├── ajaxStatusActions.js │ ├── authActions.js │ └── userActions.js ├── api │ ├── delay.js │ └── firebase.js ├── components │ ├── App.js │ ├── Layout.js │ ├── about │ │ └── AboutPage.js │ ├── admin │ │ └── AdminPage.js │ ├── common │ │ ├── AdminLink.js │ │ ├── Header.js │ │ ├── LoadingDots.js │ │ ├── LoginLink.js │ │ ├── LogoutLink.js │ │ ├── SelectInput.js │ │ └── TextInput.js │ ├── home │ │ └── HomePage.js │ ├── login │ │ ├── LoginForm.js │ │ └── LoginPage.js │ ├── protected │ │ └── ProtectedPage.js │ ├── registration │ │ ├── RegistrationForm.js │ │ └── RegistrationPage.js │ └── requireAuth.js ├── config.js ├── index.html ├── index.js ├── index.test.js ├── reducers │ ├── ajaxStatusReducer.js │ ├── authReducer.js │ ├── index.js │ ├── initialState.js │ ├── routesPermissionsReducer.js │ └── userReducer.js ├── routes.js ├── store │ ├── configureStore.dev.js │ ├── configureStore.js │ └── configureStore.prod.js └── styles │ └── styles.css ├── tools ├── srcServer.js ├── startMessage.js └── testSetup.js └── webpack.config.dev.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "stage-0", "react"], 3 | "plugins": ["react-hot-loader/babel"] 4 | } 5 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "eslint:recommended", 4 | "plugin:import/errors", 5 | "plugin:import/warnings" 6 | ], 7 | "plugins": [ 8 | "react" 9 | ], 10 | "parserOptions": { 11 | "ecmaVersion": 6, 12 | "sourceType": "module", 13 | "ecmaFeatures": { 14 | "jsx": true 15 | } 16 | }, 17 | "env": { 18 | "es6": true, 19 | "browser": true, 20 | "node": true, 21 | "jquery": true, 22 | "mocha": true 23 | }, 24 | "settings": { 25 | "import/ignore": [ 26 | "node_modules" 27 | ] 28 | }, 29 | "rules": { 30 | "quotes": 0, 31 | "no-console": 1, 32 | "no-debugger": 1, 33 | "no-var": 1, 34 | "semi": [1, "always"], 35 | "no-trailing-spaces": 0, 36 | "eol-last": 0, 37 | "no-unused-vars": 0, 38 | "no-underscore-dangle": 0, 39 | "no-alert": 0, 40 | "no-lone-blocks": 0, 41 | "jsx-quotes": 1, 42 | "react/display-name": [ 1, {"ignoreTranspilerName": false }], 43 | "react/forbid-prop-types": [1, {"forbid": ["any"]}], 44 | "react/jsx-boolean-value": 1, 45 | "react/jsx-closing-bracket-location": 0, 46 | "react/jsx-curly-spacing": 1, 47 | "react/jsx-indent-props": 0, 48 | "react/jsx-key": 1, 49 | "react/jsx-max-props-per-line": 0, 50 | "react/jsx-no-bind": 1, 51 | "react/jsx-no-duplicate-props": 1, 52 | "react/jsx-no-literals": 0, 53 | "react/jsx-no-undef": 1, 54 | "react/jsx-pascal-case": 1, 55 | "react/jsx-sort-prop-types": 0, 56 | "react/jsx-sort-props": 0, 57 | "react/jsx-uses-react": 1, 58 | "react/jsx-uses-vars": 1, 59 | "react/no-danger": 1, 60 | "react/no-did-mount-set-state": 1, 61 | "react/no-did-update-set-state": 1, 62 | "react/no-direct-mutation-state": 1, 63 | "react/no-multi-comp": 1, 64 | "react/no-set-state": 0, 65 | "react/no-unknown-property": 1, 66 | "react/prefer-es6-class": 1, 67 | "react/prop-types": 1, 68 | "react/react-in-jsx-scope": 1, 69 | "react/require-extension": 1, 70 | "react/self-closing-comp": 1, 71 | "react/sort-comp": 1, 72 | "react/wrap-multilines": 1 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | .DS_Store 4 | dist 5 | .idea 6 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node": true, 3 | "browser": true, 4 | "esnext": true, 5 | "newcap": false 6 | } 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Dan Abramov 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 | Firebase 3.0 Starter using React Redux 2 | ===================== 3 | 4 | This is a Firebase 3.0 start using React and Redux. 5 | 6 | It uses the latest version of libraries, including the brand new React Hot Loader ([still beta](https://github.com/gaearon/react-hot-loader/pull/240)) 7 | 8 | ## Stack 9 | 10 | - React 11 | - [X] React `15.1.0` 12 | - [X] React Hot Loader `3.0.0-beta.2` 13 | - [X] React Router `2.4.1` 14 | - Redux 15 | - [X] Redux `3.5.2` 16 | - [X] React Redux `4.4.5` 17 | - [X] React Router Redux `4.0.4` 18 | - [X] Redux Thunk `2.1.0` 19 | - [X] Redux Dev Tools 20 | - Webpack 21 | - [X] Webpack `1.13.1` 22 | - [X] Webpack Dev Middleware `1.6.1` 23 | - [X] Webpack Hot Middleware `2.10.0` 24 | - Firebase 25 | - [X] Firebase `3.0.3` 26 | - Linting 27 | - [X] Eslint `2.11.1` 28 | - Styles 29 | - [X] Bootstrap `3.3.6` 30 | - Testing 31 | - [X] Mocha `2.5.3` 32 | - [X] Enzyme `2.3.0` 33 | 34 | 35 | ## Features 36 | 37 | - Firebase: 38 | - Auth 39 | - [X] Authentication setup (Registration/Login) 40 | - [X] state.user sync with Firebase Auth 41 | - [X] Protected routes (needs to be logged in) 42 | - [X] Store users on `'/users/'` 43 | - [X] Admin flag on user (`'/isAdmin/' :: bool`) 44 | - [X] Admin Protected routes (needs to be logged in) 45 | - Database 46 | - [X] Set example 47 | - [X] Query example 48 | 49 | ## Usage 50 | 51 | ``` 52 | git clone git@github.com:douglascorrea/react-hot-redux-firebase-starter.git 53 | cd react-hot-redux-firebase-starter 54 | npm install 55 | npm start -s 56 | ``` 57 | 58 | ## Development Tasks 59 | 60 | - `npm start` run the web app with lint and tests in watch mode 61 | - `npm run lint` linting javascript code usig eslint 62 | - `npm run test` test using mocha and enzyme 63 | 64 | ## Roadmap 65 | 66 | Check our [roadmap issues](https://github.com/douglascorrea/react-hot-redux-firebase-starter/issues?q=is%3Aissue+is%3Aopen+label%3Aroadmap) 67 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-hot-redux-firebase-starter", 3 | "version": "0.1.0", 4 | "description": "Boilerplate for ReactJS, Redux, Hot Loading and Firebase 3.x", 5 | "scripts": { 6 | "prestart": "babel-node tools/startMessage.js", 7 | "start": "npm-run-all --parallel test:watch open:src lint:watch", 8 | "open:src": "babel-node tools/srcServer.js", 9 | "test": "mocha --reporter progress tools/testSetup.js src/*.test.js", 10 | "test:watch": "npm run test -- --watch", 11 | "lint": "node_modules/.bin/esw webpack.config.* tools src", 12 | "lint:watch": "npm run lint -- --watch" 13 | }, 14 | "keywords": [ 15 | "react", 16 | "reactjs", 17 | "boilerplate", 18 | "hot", 19 | "reload", 20 | "hmr", 21 | "live", 22 | "edit", 23 | "webpack", 24 | "firebase" 25 | ], 26 | "author": "Dan Abramov (http://github.com/gaearon)", 27 | "license": "MIT", 28 | "bugs": { 29 | "url": "https://github.com/gaearon/react-hot-boilerplate/issues" 30 | }, 31 | "homepage": "https://github.com/gaearon/react-hot-boilerplate", 32 | "devDependencies": { 33 | "babel-cli": "^6.9.0", 34 | "babel-core": "^6.9.1", 35 | "babel-eslint": "^6.0.4", 36 | "babel-loader": "^6.2.4", 37 | "babel-preset-es2015": "^6.9.0", 38 | "babel-preset-react": "^6.5.0", 39 | "babel-preset-stage-0": "^6.5.0", 40 | "babel-register": "6.9.0", 41 | "colors": "^1.1.2", 42 | "css-loader": "^0.23.1", 43 | "enzyme": "^2.3.0", 44 | "eslint": "^2.11.1", 45 | "eslint-plugin-import": "^1.8.1", 46 | "eslint-plugin-react": "^5.1.1", 47 | "eslint-watch": "^2.1.11", 48 | "expect": "^1.20.1", 49 | "express": "^4.13.4", 50 | "file-loader": "^0.8.5", 51 | "jsdom": "^9.2.1", 52 | "mocha": "^2.5.3", 53 | "nock": "^8.0.0", 54 | "npm-run-all": "^2.1.1", 55 | "open": "0.0.5", 56 | "react-addons-test-utils": "^15.1.0", 57 | "react-hot-loader": "^3.0.0-beta.2", 58 | "redux-immutable-state-invariant": "^1.2.3", 59 | "redux-mock-store": "^1.0.4", 60 | "style-loader": "^0.13.1", 61 | "url-loader": "^0.5.7", 62 | "webpack": "^1.13.1", 63 | "webpack-dev-middleware": "^1.6.1", 64 | "webpack-hot-middleware": "^2.10.0" 65 | }, 66 | "dependencies": { 67 | "bootstrap": "^3.3.6", 68 | "firebase": "^3.0.3", 69 | "jquery": "^2.2.4", 70 | "react": "^15.1.0", 71 | "react-dom": "^15.1.0", 72 | "react-loader": "^2.4.0", 73 | "react-redux": "^4.4.5", 74 | "react-router": "^2.4.1", 75 | "react-router-redux": "^4.0.4", 76 | "redux": "^3.5.2", 77 | "redux-thunk": "^2.1.0", 78 | "toastr": "^2.1.2" 79 | }, 80 | "repository": { 81 | "type": "git", 82 | "url": "https://github.com/gaearon/react-hot-boilerplate.git" 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/actions/actionTypes.js: -------------------------------------------------------------------------------- 1 | // ajax and loading actions 2 | export const BEGIN_AJAX_CALL = 'BEGIN_AJAX_CALL'; 3 | export const AJAX_CALL_ERROR = 'AJAX_CALL_ERROR'; 4 | 5 | // Auth actions 6 | export const AUTH_INITIALIZATION_DONE = 'AUTH_INITIALIZATION_DONE'; 7 | export const AUTH_LOGGED_IN_SUCCESS = 'AUTH_LOGGED_IN_SUCCESS'; 8 | export const AUTH_LOGGED_OUT_SUCCESS = 'AUTH_LOGGED_OUT_SUCCESS'; 9 | 10 | // User actions 11 | export const USER_CREATED_SUCCESS = 'USER_CREATED_SUCCESS'; 12 | export const USER_LOADED_SUCCESS = 'USER_LOADED_SUCCESS'; 13 | export const USER_IS_ADMIN_SUCCESS = 'USER_IS_ADMIN_SUCCESS'; 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/actions/ajaxStatusActions.js: -------------------------------------------------------------------------------- 1 | import * as types from './actionTypes'; 2 | 3 | export function beginAjaxCall() { 4 | return {type: types.BEGIN_AJAX_CALL}; 5 | } 6 | 7 | export function ajaxCallError() { 8 | return {type: types.AJAX_CALL_ERROR}; 9 | } 10 | 11 | -------------------------------------------------------------------------------- /src/actions/authActions.js: -------------------------------------------------------------------------------- 1 | import toastr from 'toastr'; 2 | 3 | import firebaseApi from '../api/firebase'; 4 | import * as types from './actionTypes'; 5 | import {push} from 'react-router-redux'; 6 | 7 | import {ajaxCallError, beginAjaxCall} from './ajaxStatusActions'; 8 | import {userLoadedSuccess, userCreated, userIsAdminSuccess} from './userActions'; 9 | 10 | export function authInitializedDone() { 11 | return { 12 | type: types.AUTH_INITIALIZATION_DONE 13 | }; 14 | } 15 | 16 | export function authLoggedInSuccess(userUID) { 17 | return { 18 | type: types.AUTH_LOGGED_IN_SUCCESS, userUID 19 | }; 20 | } 21 | 22 | export function authLoggedOutSuccess() { 23 | 24 | return {type: types.AUTH_LOGGED_OUT_SUCCESS}; 25 | } 26 | 27 | export function authInitialized(user) { 28 | return (dispatch) => { 29 | dispatch(authInitializedDone()); 30 | if (user) { 31 | dispatch(authLoggedIn(user.uid)); 32 | } else { 33 | dispatch(authLoggedOutSuccess()); 34 | } 35 | }; 36 | } 37 | 38 | export function authLoggedIn(userUID) { 39 | return (dispatch) => { 40 | dispatch(authLoggedInSuccess(userUID)); 41 | dispatch(beginAjaxCall()); 42 | firebaseApi.GetChildAddedByKeyOnce('/users', userUID) 43 | .then( 44 | user => { 45 | dispatch(userLoadedSuccess(user.val())); 46 | dispatch(push('/')); 47 | }) 48 | .catch( 49 | error => { 50 | dispatch(beginAjaxCall()); 51 | // @TODO better error handling 52 | throw(error); 53 | }); 54 | }; 55 | } 56 | 57 | export function createUserWithEmailAndPassword(user) { 58 | return (dispatch) => { 59 | dispatch(beginAjaxCall()); 60 | return firebaseApi.createUserWithEmailAndPassword(user).then(user => { 61 | dispatch(userCreated(user)); 62 | }).catch(error => { 63 | dispatch(ajaxCallError(error)); 64 | // @TODO better error handling 65 | throw(error); 66 | }); 67 | }; 68 | } 69 | 70 | export function signInWithEmailAndPassword(user) { 71 | return (dispatch) => { 72 | dispatch(beginAjaxCall()); 73 | return firebaseApi.signInWithEmailAndPassword(user) 74 | .then( 75 | user => { 76 | dispatch(authLoggedIn(user.uid)); 77 | }) 78 | .catch(error => { 79 | dispatch(ajaxCallError(error)); 80 | // @TODO better error handling 81 | throw(error); 82 | }); 83 | }; 84 | } 85 | 86 | export function signOut() { 87 | return (dispatch, getState) => { 88 | dispatch(beginAjaxCall()); 89 | return firebaseApi.authSignOut() 90 | .then( 91 | () => { 92 | dispatch(authLoggedOutSuccess()); 93 | if (getState().routesPermissions.requireAuth 94 | .filter(route => route === getState().routing.locationBeforeTransitions.pathname).toString()) { 95 | dispatch(push('/')); 96 | } 97 | }) 98 | .catch(error => { 99 | dispatch(ajaxCallError(error)); 100 | // @TODO better error handling 101 | throw(error); 102 | }); 103 | }; 104 | } 105 | 106 | 107 | function redirect(replace, pathname, nextPathName, error = false) { 108 | replace({ 109 | pathname: pathname, 110 | state: {nextPathname: nextPathName} 111 | }); 112 | if (error) { 113 | toastr.error(error); 114 | } 115 | } 116 | 117 | export function requireAuth(nextState, replace) { 118 | return (dispatch, getState) => { 119 | if (!getState().auth.isLogged) { 120 | redirect(replace, '/login', nextState.location.pathname, 'You need to be logged to access this page'); 121 | } 122 | }; 123 | } 124 | 125 | 126 | export function requireAdmin(nextState, replace, callback) { 127 | return (dispatch, getState) => { 128 | if (getState().auth.isLogged) { 129 | switch (getState().user.isAdmin) { 130 | case false: 131 | redirect(replace, '/login', nextState.location.pathname, 'You need to be logged to access this page'); 132 | break; 133 | case undefined: 134 | firebaseApi.GetChildAddedByKeyOnce('/isAdmin/', getState().auth.currentUserUID) 135 | .then( 136 | user => { 137 | if (user.exists() && user.val()) { 138 | dispatch(userIsAdminSuccess()); 139 | callback(); 140 | } else { 141 | redirect(replace, '/login', nextState.location.pathname, 'You need to be logged to access this page'); 142 | } 143 | }) 144 | .catch( 145 | error => { 146 | dispatch(ajaxCallError()); 147 | redirect(replace, '/login', nextState.location.pathname, 'You need to be logged to access this page'); 148 | callback(); 149 | // @TODO better error handling 150 | throw(error); 151 | }); 152 | break; 153 | case true: 154 | callback(); 155 | break; 156 | 157 | } 158 | } else { 159 | redirect(replace, '/login', nextState.location.pathname, 'You need to be logged to access this page'); 160 | callback(); 161 | } 162 | }; 163 | } 164 | -------------------------------------------------------------------------------- /src/actions/userActions.js: -------------------------------------------------------------------------------- 1 | import firebaseApi from '../api/firebase'; 2 | import * as types from './actionTypes'; 3 | 4 | import { authLoggedIn } from './authActions'; 5 | import {ajaxCallError, beginAjaxCall} from './ajaxStatusActions'; 6 | 7 | function extractUserProperties(firebaseUser) { 8 | 9 | const user = {}; 10 | const userProperties = [ 11 | 'displayName', 12 | 'email', 13 | 'emailVerified', 14 | 'isAnonymous', 15 | 'photoURL', 16 | 'providerData', 17 | 'providerId', 18 | 'refreshToken', 19 | 'uid', 20 | 'isAdmin' 21 | ]; 22 | 23 | userProperties.map((prop) => { 24 | if (prop in firebaseUser) { 25 | user[prop] = firebaseUser[prop]; 26 | } 27 | }); 28 | 29 | return user; 30 | } 31 | 32 | export function userCreated(user) { 33 | return (dispatch) => { 34 | firebaseApi.databaseSet('/users/' + user.uid, extractUserProperties(user)) 35 | .then( 36 | () => { 37 | dispatch(authLoggedIn(user.uid)); 38 | dispatch(userCreatedSuccess()); 39 | }) 40 | .catch( 41 | error => { 42 | dispatch(ajaxCallError(error)); 43 | // @TODO better error handling 44 | throw(error); 45 | }); 46 | }; 47 | } 48 | 49 | export function userCreatedSuccess() { 50 | return { 51 | type: types.USER_CREATED_SUCCESS 52 | }; 53 | } 54 | 55 | export function userLoadedSuccess(user) { 56 | return { 57 | type: types.USER_LOADED_SUCCESS, user: extractUserProperties(user) 58 | }; 59 | } 60 | 61 | export function userIsAdminSuccess() { 62 | return { 63 | type: types.USER_IS_ADMIN_SUCCESS 64 | }; 65 | } 66 | -------------------------------------------------------------------------------- /src/api/delay.js: -------------------------------------------------------------------------------- 1 | export default 0; 2 | -------------------------------------------------------------------------------- /src/api/firebase.js: -------------------------------------------------------------------------------- 1 | import * as firebase from 'firebase/firebase-browser'; 2 | import {firebaseConfig} from '../config'; 3 | 4 | 5 | class FirebaseApi { 6 | 7 | static initAuth() { 8 | firebase.initializeApp(firebaseConfig); 9 | return new Promise((resolve, reject) => { 10 | const unsub = firebase.auth().onAuthStateChanged( 11 | user => { 12 | unsub(); 13 | resolve(user); 14 | }, 15 | error => reject(error) 16 | ); 17 | }); 18 | } 19 | 20 | static createUserWithEmailAndPassword(user){ 21 | return firebase.auth().createUserWithEmailAndPassword(user.email, user.password); 22 | } 23 | 24 | static signInWithEmailAndPassword(user) { 25 | return firebase.auth().signInWithEmailAndPassword(user.email, user.password); 26 | } 27 | 28 | static authSignOut(){ 29 | return firebase.auth().signOut(); 30 | } 31 | 32 | static databasePush(path, value) { 33 | return new Promise((resolve, reject) => { 34 | firebase 35 | .database() 36 | .ref(path) 37 | .push(value, (error) => { 38 | if (error) { 39 | reject(error); 40 | } else { 41 | resolve(); 42 | } 43 | }); 44 | }); 45 | } 46 | 47 | static GetValueByKeyOnce(path, key) { 48 | return firebase 49 | .database() 50 | .ref(path) 51 | .orderByKey() 52 | .equalTo(key) 53 | .once('value'); 54 | } 55 | 56 | static GetChildAddedByKeyOnce(path, key) { 57 | return firebase 58 | .database() 59 | .ref(path) 60 | .orderByKey() 61 | .equalTo(key) 62 | .once('child_added'); 63 | } 64 | 65 | static databaseSet(path, value) { 66 | 67 | return firebase 68 | .database() 69 | .ref(path) 70 | .set(value); 71 | 72 | } 73 | } 74 | 75 | export default FirebaseApi; 76 | -------------------------------------------------------------------------------- /src/components/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import {Router} from 'react-router'; 3 | import routes from '../routes'; 4 | 5 | // If you use React Router, make this component 6 | // render with your routes. Currently, 7 | // only synchronous routes are hot reloaded, and 8 | // you will see a warning from on every reload. 9 | // You can ignore this warning. For details, see: 10 | // https://github.com/reactjs/react-router/issues/2182 11 | 12 | class App extends Component { 13 | render() { 14 | const { history, store } = this.props; 15 | return ( 16 | 17 | ); 18 | } 19 | } 20 | 21 | App.propTypes = { 22 | history: React.PropTypes.object.isRequired, 23 | store: React.PropTypes.object.isRequired 24 | }; 25 | 26 | export default App; 27 | -------------------------------------------------------------------------------- /src/components/Layout.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Header from './common/Header'; 3 | import {connect} from 'react-redux'; 4 | import {bindActionCreators} from 'redux'; 5 | import {signOut} from '../actions/authActions'; 6 | 7 | class Layout extends React.Component { 8 | 9 | constructor(props, context) { 10 | super(props, context); 11 | } 12 | 13 | render() { 14 | const {auth, actions, loading, user} = this.props; 15 | return ( 16 |
17 |
18 | {this.props.children} 19 |
20 | ); 21 | } 22 | } 23 | 24 | Layout.propTypes = { 25 | children: React.PropTypes.object, 26 | actions: React.PropTypes.object.isRequired, 27 | auth: React.PropTypes.object.isRequired, 28 | user: React.PropTypes.object.isRequired, 29 | loading: React.PropTypes.bool.isRequired 30 | }; 31 | 32 | function mapStateToProps(state, ownProps) { 33 | return { 34 | auth: state.auth, 35 | user: state.user, 36 | loading: state.ajaxCallsInProgress > 0 37 | }; 38 | } 39 | 40 | function mapDispatchToProps(dispatch) { 41 | return { 42 | actions: bindActionCreators({signOut}, dispatch) 43 | }; 44 | } 45 | 46 | export default connect(mapStateToProps, mapDispatchToProps)(Layout); 47 | -------------------------------------------------------------------------------- /src/components/about/AboutPage.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Link} from 'react-router'; 3 | 4 | const AboutPage = () => { 5 | return ( 6 |
7 |

About

8 |

Created by @douglas_correa

9 | Go to Home 10 |
11 | ); 12 | }; 13 | 14 | export default AboutPage; 15 | -------------------------------------------------------------------------------- /src/components/admin/AdminPage.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Link} from 'react-router'; 3 | 4 | const AdminPage = () => { 5 | return ( 6 |
7 |

You will only see this page if you are Admin

8 | Home 9 |
10 | ); 11 | }; 12 | 13 | export default AdminPage; 14 | -------------------------------------------------------------------------------- /src/components/common/AdminLink.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Link} from 'react-router'; 3 | 4 | const AdminLink = () => { 5 | return ( 6 | 7 | {" | "} 8 | Admin 9 | 10 | ); 11 | }; 12 | 13 | export default AdminLink; 14 | -------------------------------------------------------------------------------- /src/components/common/Header.js: -------------------------------------------------------------------------------- 1 | import React, {PropTypes} from 'react'; 2 | import {Link, IndexLink} from 'react-router'; 3 | import LoadingDots from './LoadingDots'; 4 | import LoginLink from './LoginLink'; 5 | import LogoutLink from './LogoutLink'; 6 | import AdminLink from './AdminLink'; 7 | 8 | const Header = ({loading, signOut, auth, user}) => { 9 | 10 | let loginLogoutLink = auth.isLogged ? : ; 11 | let adminLink = user.isAdmin ? : null; 12 | 13 | return ( 14 | 25 | ); 26 | }; 27 | 28 | Header.propTypes = { 29 | signOut: React.PropTypes.func.isRequired, 30 | auth: React.PropTypes.object.isRequired, 31 | user: React.PropTypes.object.isRequired, 32 | loading: PropTypes.bool.isRequired 33 | }; 34 | 35 | export default Header; 36 | -------------------------------------------------------------------------------- /src/components/common/LoadingDots.js: -------------------------------------------------------------------------------- 1 | import React, {PropTypes} from 'react'; 2 | 3 | class LoadingDots extends React.Component { 4 | constructor(props, context) { 5 | super(props, context); 6 | 7 | this.state = {frame: 1}; 8 | } 9 | 10 | componentDidMount() { 11 | this.interval = setInterval(() => { 12 | this.setState({ // eslint-disable-line react/no-did-mount-set-state 13 | frame: this.state.frame + 1 14 | }); 15 | }, this.props.interval); 16 | } 17 | 18 | componentWillUnmount() { 19 | clearInterval(this.interval); 20 | } 21 | 22 | render() { 23 | let dots = this.state.frame % (this.props.dots + 1); 24 | let text = ''; 25 | while (dots > 0) { 26 | text += '.'; 27 | dots--; 28 | } 29 | return {text} ; 30 | } 31 | } 32 | 33 | LoadingDots.defaultProps = { 34 | interval: 300, dots: 3 35 | }; 36 | 37 | LoadingDots.propTypes = { 38 | interval: PropTypes.number, 39 | dots: PropTypes.number 40 | }; 41 | 42 | export default LoadingDots; 43 | -------------------------------------------------------------------------------- /src/components/common/LoginLink.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Link} from 'react-router'; 3 | 4 | const LoginLink = () => { 5 | return ( 6 | 7 | Sign Up 8 | {" | "} 9 | Login 10 | 11 | ); 12 | }; 13 | 14 | export default LoginLink; 15 | -------------------------------------------------------------------------------- /src/components/common/LogoutLink.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const LogoutLink = ({signOut}) => { 4 | return Logout; 5 | }; 6 | 7 | LogoutLink.propTypes = { 8 | signOut: React.PropTypes.func.isRequired 9 | }; 10 | 11 | export default LogoutLink; 12 | -------------------------------------------------------------------------------- /src/components/common/SelectInput.js: -------------------------------------------------------------------------------- 1 | import React, {PropTypes} from 'react'; 2 | 3 | const SelectInput = ({name, label, onChange, defaultOption, value, error, options}) => { 4 | return ( 5 |
6 | 7 |
8 | {/* Note, value is set here rather than on the option - docs: https://facebook.github.io/react/docs/forms.html */} 9 | 20 | {error &&
{error}
} 21 |
22 |
23 | ); 24 | }; 25 | 26 | SelectInput.propTypes = { 27 | name: PropTypes.string.isRequired, 28 | label: PropTypes.string.isRequired, 29 | onChange: PropTypes.func.isRequired, 30 | defaultOption: PropTypes.string, 31 | value: PropTypes.string, 32 | error: PropTypes.string, 33 | options: PropTypes.arrayOf(PropTypes.object) 34 | }; 35 | 36 | export default SelectInput; 37 | -------------------------------------------------------------------------------- /src/components/common/TextInput.js: -------------------------------------------------------------------------------- 1 | import React, {PropTypes} from 'react'; 2 | 3 | const TextInput = ({name, label, onChange, placeholder, value, error}) => { 4 | let wrapperClass = 'form-group'; 5 | if (error && error.length > 0) { 6 | wrapperClass += " " + 'has-error'; 7 | } 8 | 9 | return ( 10 |
11 | 12 |
13 | 20 | {error &&
{error}
} 21 |
22 |
23 | ); 24 | }; 25 | 26 | TextInput.propTypes = { 27 | name: PropTypes.string.isRequired, 28 | label: PropTypes.string.isRequired, 29 | onChange: PropTypes.func.isRequired, 30 | placeholder: PropTypes.string, 31 | value: PropTypes.string, 32 | error: PropTypes.string 33 | }; 34 | 35 | export default TextInput; 36 | -------------------------------------------------------------------------------- /src/components/home/HomePage.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Link} from 'react-router'; 3 | 4 | const HomePage = () => { 5 | return ( 6 |
7 |

React Redux Firebase Starter

8 |

This is an starter project to make your life easier

9 | Learn more 10 |
11 | ); 12 | }; 13 | 14 | export default HomePage; 15 | -------------------------------------------------------------------------------- /src/components/login/LoginForm.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import TextInput from '../common/TextInput'; 3 | 4 | const LoginForm = ({user, onSave, onChange, saving}) => { 5 | return ( 6 |
7 |

Login

8 | 14 | 15 | 21 | 22 | 28 | 29 | ); 30 | }; 31 | 32 | LoginForm.propTypes = { 33 | onSave: React.PropTypes.func.isRequired, 34 | saving: React.PropTypes.bool, 35 | user: React.PropTypes.object.isRequired, 36 | onChange: React.PropTypes.func.isRequired 37 | }; 38 | 39 | export default LoginForm; 40 | -------------------------------------------------------------------------------- /src/components/login/LoginPage.js: -------------------------------------------------------------------------------- 1 | import React, {PropTypes} from 'react'; 2 | import {connect} from 'react-redux'; 3 | import {bindActionCreators} from 'redux'; 4 | import {signInWithEmailAndPassword} from '../../actions/authActions'; 5 | import LoginForm from './LoginForm'; 6 | import toastr from 'toastr'; 7 | 8 | export class RegistrationPage extends React.Component { 9 | constructor(props, context) { 10 | super(props, context); 11 | 12 | this.state = { 13 | user: { 14 | email: "", 15 | password: "" 16 | }, 17 | saving: false 18 | }; 19 | 20 | this.updateUserState = this.updateUserState.bind(this); 21 | this.createUser = this.createUser.bind(this); 22 | } 23 | 24 | updateUserState(event) { 25 | const field = event.target.name; 26 | let user = this.state.user; 27 | user[field] = event.target.value; 28 | return this.setState({user: user}); 29 | } 30 | 31 | createUser(event) { 32 | event.preventDefault(); 33 | 34 | this.setState({saving: true}); 35 | 36 | this.props.actions.signInWithEmailAndPassword(this.state.user) 37 | .then(user => toastr.success('You are logged in')) 38 | .catch(error => { 39 | toastr.error(error.message); 40 | this.setState({saving: false}); 41 | }); 42 | } 43 | 44 | render() { 45 | return ( 46 | 52 | ); 53 | } 54 | } 55 | 56 | RegistrationPage.propTypes = { 57 | actions: PropTypes.object.isRequired 58 | }; 59 | 60 | RegistrationPage.contextTypes = { 61 | router: PropTypes.object 62 | }; 63 | 64 | function mapStateToProps(state, ownProps) { 65 | return {}; 66 | } 67 | 68 | function mapDispatchToProps(dispatch) { 69 | return { 70 | actions: bindActionCreators({signInWithEmailAndPassword}, dispatch) 71 | }; 72 | } 73 | 74 | export default connect(mapStateToProps, mapDispatchToProps)(RegistrationPage); 75 | -------------------------------------------------------------------------------- /src/components/protected/ProtectedPage.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Link} from 'react-router'; 3 | import checkAuth from '../requireAuth'; 4 | const ProtectedPage = () => { 5 | return ( 6 |
7 |

You will only see this page if you are logged in

8 | Home 9 |
10 | ); 11 | }; 12 | 13 | export default checkAuth(ProtectedPage); 14 | -------------------------------------------------------------------------------- /src/components/registration/RegistrationForm.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import TextInput from '../common/TextInput'; 3 | 4 | const RegistrationForm = ({user, onSave, onChange, saving}) => { 5 | return ( 6 |
7 |

Create account

8 | 14 | 15 | 21 | 22 | 28 | 29 | ); 30 | }; 31 | 32 | RegistrationForm.propTypes = { 33 | onSave: React.PropTypes.func.isRequired, 34 | saving: React.PropTypes.bool, 35 | user: React.PropTypes.object.isRequired, 36 | onChange: React.PropTypes.func.isRequired 37 | }; 38 | 39 | export default RegistrationForm; 40 | -------------------------------------------------------------------------------- /src/components/registration/RegistrationPage.js: -------------------------------------------------------------------------------- 1 | import React, {PropTypes} from 'react'; 2 | import {connect} from 'react-redux'; 3 | import {bindActionCreators} from 'redux'; 4 | import {createUserWithEmailAndPassword} from '../../actions/authActions'; 5 | import RegistrationForm from './RegistrationForm'; 6 | import toastr from 'toastr'; 7 | 8 | export class RegistrationPage extends React.Component { 9 | constructor(props, context) { 10 | super(props, context); 11 | 12 | this.state = { 13 | user: { 14 | email: "", 15 | password: "" 16 | }, 17 | saving: false 18 | }; 19 | 20 | this.updateUserState = this.updateUserState.bind(this); 21 | this.createUser = this.createUser.bind(this); 22 | } 23 | 24 | updateUserState(event) { 25 | const field = event.target.name; 26 | let user = this.state.user; 27 | user[field] = event.target.value; 28 | return this.setState({user: user}); 29 | } 30 | 31 | createUser(event) { 32 | event.preventDefault(); 33 | 34 | this.setState({saving: true}); 35 | 36 | this.props.actions.createUserWithEmailAndPassword(this.state.user) 37 | .then((user) => toastr.success('User Created')) 38 | .catch(error => { 39 | toastr.error(error.message); 40 | this.setState({saving: false}); 41 | }); 42 | } 43 | 44 | render() { 45 | return ( 46 | 52 | ); 53 | } 54 | } 55 | 56 | RegistrationPage.propTypes = { 57 | actions: PropTypes.object.isRequired 58 | }; 59 | 60 | RegistrationPage.contextTypes = { 61 | router: PropTypes.object 62 | }; 63 | 64 | function mapStateToProps(state, ownProps) { 65 | return {}; 66 | } 67 | 68 | function mapDispatchToProps(dispatch) { 69 | return { 70 | actions: bindActionCreators({createUserWithEmailAndPassword}, dispatch) 71 | }; 72 | } 73 | 74 | export default connect(mapStateToProps, mapDispatchToProps)(RegistrationPage); 75 | -------------------------------------------------------------------------------- /src/components/requireAuth.js: -------------------------------------------------------------------------------- 1 | /*eslint no-invalid-this: "error"*/ 2 | /*eslint-env es6*/ 3 | import React, { PropTypes, Component } from 'react'; 4 | import {connect} from 'react-redux'; 5 | import toastr from 'toastr'; 6 | 7 | export default function (ComposedComponent){ 8 | class Authentication extends Component { 9 | componentWillMount(){ 10 | if(!this.props.authenticated) { 11 | this.context.router.push('/login'); 12 | toastr.error('You need to be logged to access this page'); 13 | } 14 | } 15 | componentWillUpdate(nextProps){ 16 | if(!nextProps.authenticated) { 17 | this.context.router.push('/login'); 18 | toastr.error('You need to be logged to access this page'); 19 | } 20 | } 21 | render(){ 22 | return ; 23 | } 24 | } 25 | Authentication.contextTypes = { 26 | router : PropTypes.object 27 | }; 28 | Authentication.propTypes = { 29 | authenticated : PropTypes.bool 30 | }; 31 | const mapStateToProps = (state) => ({ 32 | authenticated : state.auth.isLogged 33 | }); 34 | return connect(mapStateToProps)(Authentication); 35 | } 36 | -------------------------------------------------------------------------------- /src/config.js: -------------------------------------------------------------------------------- 1 | // Firebase 2 | 3 | export const firebaseConfig = { 4 | apiKey: "AIzaSyCNsG4Y4pkaVxqM809bpQeZ3wsFgVlCPcg", 5 | authDomain: "react-hot-redux-firebase-start.firebaseapp.com", 6 | databaseURL: "https://react-hot-redux-firebase-start.firebaseio.com", 7 | storageBucket: "react-hot-redux-firebase-start.appspot.com" 8 | }; 9 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | React Hot Redux Firebase Starter 5 | 6 | 7 |
8 |
9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | // modules 2 | import {AppContainer} from 'react-hot-loader'; 3 | import React from 'react'; 4 | import ReactDOM from 'react-dom'; 5 | import {Provider} from 'react-redux'; 6 | import {syncHistoryWithStore} from 'react-router-redux'; 7 | import {browserHistory} from 'react-router'; 8 | 9 | // api 10 | import FirebaseApi from './api/firebase'; 11 | 12 | // actions 13 | import {authInitialized} from './actions/authActions'; 14 | import {ajaxCallError, beginAjaxCall} from './actions/ajaxStatusActions'; 15 | 16 | // components 17 | import App from './components/App'; 18 | 19 | // Store 20 | import initialState from './reducers/initialState'; 21 | import configureStore from './store/configureStore'; //eslint-disable-line import/default 22 | 23 | // styles 24 | import './styles/styles.css'; //Webpack can import CSS files too! 25 | import '../node_modules/bootstrap/dist/css/bootstrap.min.css'; 26 | import '../node_modules/toastr/build/toastr.min.css'; 27 | 28 | // store initialization 29 | const store = configureStore(initialState); 30 | 31 | // Create an enhanced history that syncs navigation events with the store 32 | const history = syncHistoryWithStore(browserHistory, store); 33 | const rootEl = document.getElementById('root'); 34 | 35 | // Initialize Firebase Auth and then start the app 36 | store.dispatch(beginAjaxCall()); 37 | FirebaseApi.initAuth() 38 | .then( 39 | user => { 40 | store.dispatch(authInitialized(user)); 41 | 42 | ReactDOM.render( 43 | 44 | 45 | 46 | 47 | , 48 | rootEl 49 | ); 50 | 51 | if (module.hot) { 52 | module.hot.accept('./components/App', () => { 53 | // If you use Webpack 2 in ES modules mode, you can 54 | // use here rather than require() a . 55 | const NextApp = require('./components/App').default; 56 | ReactDOM.render( 57 | 58 | 59 | 60 | 61 | , 62 | rootEl 63 | ); 64 | }); 65 | } 66 | }) 67 | .catch( 68 | error => { 69 | store.dispatch(ajaxCallError()); 70 | console.error('error while initializing Firebase Auth'); // eslint-disable-line no-console 71 | console.error(error); // eslint-disable-line no-console 72 | }); 73 | -------------------------------------------------------------------------------- /src/index.test.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect'; 2 | 3 | describe('Our first test', () => { 4 | it('should pass', () => { 5 | expect(true).toEqual(true); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /src/reducers/ajaxStatusReducer.js: -------------------------------------------------------------------------------- 1 | import * as types from '../actions/actionTypes'; 2 | import initialState from './initialState'; 3 | 4 | function actionTypeEndsInSuccess(type) { 5 | return type.substring(type.length - 8) == '_SUCCESS'; 6 | } 7 | 8 | export default function ajaxStatusReducer(state = initialState.ajaxCallsInProgress, action) { 9 | if (action.type == types.BEGIN_AJAX_CALL) { 10 | return state + 1; 11 | } else if (action.type == types.AJAX_CALL_ERROR || 12 | actionTypeEndsInSuccess(action.type)) { 13 | return state - 1; 14 | } 15 | 16 | 17 | return state; 18 | } 19 | -------------------------------------------------------------------------------- /src/reducers/authReducer.js: -------------------------------------------------------------------------------- 1 | import * as types from '../actions/actionTypes'; 2 | import initialState from './initialState'; 3 | 4 | export default function authReducer(state = initialState.auth, action) { 5 | switch (action.type) { 6 | case types.AUTH_INITIALIZATION_DONE: 7 | return Object.assign({}, state, {initialized: true}); 8 | 9 | case types.AUTH_LOGGED_IN_SUCCESS: 10 | return Object.assign({}, state, { 11 | isLogged: true, 12 | currentUserUID: action.userUID 13 | }); 14 | 15 | case types.AUTH_LOGGED_OUT_SUCCESS: 16 | return Object.assign({}, state, { 17 | isLogged: false, 18 | currentUserUID: null 19 | }); 20 | default: 21 | return state; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/reducers/index.js: -------------------------------------------------------------------------------- 1 | import {combineReducers} from 'redux'; 2 | import user from './userReducer'; 3 | import routesPermissions from './routesPermissionsReducer'; 4 | import auth from './authReducer'; 5 | 6 | import ajaxCallsInProgress from './ajaxStatusReducer'; 7 | import { routerReducer } from 'react-router-redux'; 8 | 9 | 10 | const rootReducer = combineReducers({ 11 | routing: routerReducer, 12 | routesPermissions, 13 | user, 14 | auth, 15 | ajaxCallsInProgress 16 | }); 17 | 18 | export default rootReducer; 19 | -------------------------------------------------------------------------------- /src/reducers/initialState.js: -------------------------------------------------------------------------------- 1 | export default { 2 | routesPermissions: { 3 | requireAuth: [ 4 | '/admin' 5 | ], 6 | routesRequireAdmin: [ 7 | '/admin' 8 | ] 9 | }, 10 | routing: {}, 11 | user: { 12 | isAdmin: undefined 13 | }, 14 | auth: { 15 | isLogged: false, 16 | currentUserUID: null, 17 | initialized: false 18 | }, 19 | ajaxCallsInProgress: 0 20 | }; 21 | -------------------------------------------------------------------------------- /src/reducers/routesPermissionsReducer.js: -------------------------------------------------------------------------------- 1 | import initialState from './initialState'; 2 | 3 | export default function routesPermissions(state = initialState.auth, action) { 4 | switch (action.type) { 5 | default: 6 | return state; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/reducers/userReducer.js: -------------------------------------------------------------------------------- 1 | import * as types from '../actions/actionTypes'; 2 | import initialState from './initialState'; 3 | 4 | export default function userReducer(state = initialState.user, action) { 5 | switch (action.type) { 6 | case types.USER_LOADED_SUCCESS: 7 | return Object.assign({}, state, action.user); 8 | case types.USER_IS_ADMIN_SUCCESS: 9 | return Object.assign({}, state, {isAdmin: true}); 10 | case types.AUTH_LOGGED_OUT_SUCCESS: 11 | return initialState.user; 12 | default: 13 | return state; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/routes.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Route, IndexRoute} from 'react-router'; 3 | import Layout from './components/Layout'; 4 | import HomePage from './components/home/HomePage'; 5 | import AdminPage from './components/admin/AdminPage'; 6 | import ProtectedPage from './components/protected/ProtectedPage'; 7 | import AboutPage from './components/about/AboutPage'; 8 | import LoginPage from './components/login/LoginPage'; //eslint-disable-line import/no-named-as-default 9 | import RegistrationPage from './components/registration/RegistrationPage'; //eslint-disable-line import/no-named-as-default 10 | import {requireAdmin} from './actions/authActions'; 11 | 12 | 13 | export default function Routes(store) { 14 | 15 | 16 | const checkAdmin = (nextState, replace, callback) => { 17 | store.dispatch(requireAdmin(nextState, replace, callback)); 18 | }; 19 | 20 | return ( 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /src/store/configureStore.dev.js: -------------------------------------------------------------------------------- 1 | import {createStore, applyMiddleware, compose} from 'redux'; 2 | import { routerMiddleware } from 'react-router-redux'; 3 | import rootReducer from '../reducers'; 4 | import reduxImmutableStateInvariant from 'redux-immutable-state-invariant'; 5 | import thunk from 'redux-thunk'; 6 | import { browserHistory } from "react-router"; 7 | 8 | export default function configureStore(initialState) { 9 | return createStore( 10 | rootReducer, 11 | initialState, 12 | compose( 13 | applyMiddleware(thunk, reduxImmutableStateInvariant(), routerMiddleware(browserHistory)), 14 | window.devToolsExtension ? window.devToolsExtension() : f => f 15 | ) 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /src/store/configureStore.js: -------------------------------------------------------------------------------- 1 | if (process.env.NODE_ENV === 'production') { 2 | module.exports = require('./configureStore.prod'); 3 | } else { 4 | module.exports = require('./configureStore.dev'); 5 | } 6 | -------------------------------------------------------------------------------- /src/store/configureStore.prod.js: -------------------------------------------------------------------------------- 1 | import {createStore, applyMiddleware} from 'redux'; 2 | import rootReducer from '../reducers'; 3 | import thunk from 'redux-thunk'; 4 | 5 | export default function configureStore(initialState) { 6 | return createStore( 7 | rootReducer, 8 | initialState, 9 | applyMiddleware(thunk) 10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /src/styles/styles.css: -------------------------------------------------------------------------------- 1 | /* Styles */ 2 | html, body, #root, .container-loading { 3 | height: 100%; 4 | } 5 | 6 | #app { 7 | font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif; 8 | color: #4d4d4d; 9 | min-width: 550px; 10 | max-width: 850px; 11 | margin: 0 auto; 12 | } 13 | 14 | a.active { 15 | color: orange; 16 | } 17 | 18 | nav { 19 | padding-top: 20px; 20 | } 21 | 22 | .container-loading { 23 | display: table; 24 | table-layout: fixed; 25 | width: 100%; 26 | } 27 | 28 | .loading { 29 | width: 100px; 30 | height: 100px; 31 | margin: 0 auto; 32 | } 33 | 34 | .loading-row { 35 | background-color: #fff; 36 | min-height: 100%; 37 | height: 100%; 38 | display:table-cell; 39 | vertical-align:middle; 40 | text-align:center; 41 | width: 100%; 42 | } 43 | 44 | .loading div { 45 | width: inherit !important; 46 | } 47 | 48 | .loading svg { 49 | display: block; 50 | width: 100px; 51 | margin: 0 auto; 52 | } 53 | 54 | .h1-loading { 55 | color: #fff; 56 | font-size: 6rem; 57 | line-height: 8rem; 58 | } 59 | -------------------------------------------------------------------------------- /tools/srcServer.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import webpack from 'webpack'; 3 | import path from 'path'; 4 | import config from '../webpack.config.dev'; 5 | import open from 'open'; 6 | 7 | /* eslint-disable no-console */ 8 | 9 | const port = 3000; 10 | const app = express(); 11 | const compiler = webpack(config); 12 | 13 | app.use(require('webpack-dev-middleware')(compiler, { 14 | noInfo: true, 15 | publicPath: config.output.publicPath 16 | })); 17 | 18 | app.use(require('webpack-hot-middleware')(compiler)); 19 | 20 | app.get('*', function(req, res) { 21 | res.sendFile(path.join( __dirname, '../src/index.html')); 22 | }); 23 | 24 | app.listen(port, function(err) { 25 | if (err) { 26 | console.log(err); 27 | } else { 28 | open(`http://localhost:${port}`); 29 | } 30 | }); 31 | 32 | export default app; 33 | -------------------------------------------------------------------------------- /tools/startMessage.js: -------------------------------------------------------------------------------- 1 | import colors from 'colors'; 2 | 3 | /*eslint-disable no-console */ 4 | 5 | console.log('Starting app in dev mode...'.green); 6 | -------------------------------------------------------------------------------- /tools/testSetup.js: -------------------------------------------------------------------------------- 1 | // Tests are placed alongside files under test. 2 | // This file does the following: 3 | // 1. Registers babel for transpiling our code for testing 4 | // 2. Disables Webpack-specific features that Mocha doesn't understand. 5 | // 3. Requires jsdom so we can test via an in-memory DOM in Node 6 | // 4. Sets up global vars that mimic a browser. 7 | 8 | /*eslint-disable no-var*/ 9 | 10 | // This assures the .babelrc dev config (which includes 11 | // hot module reloading code) doesn't apply for tests. 12 | process.env.NODE_ENV = 'test'; 13 | 14 | // Register babel so that it will transpile ES6 to ES5 15 | // before our tests run. 16 | require('babel-register')(); 17 | 18 | // Disable webpack-specific features for tests since 19 | // Mocha doesn't know what to do with them. 20 | require.extensions['.css'] = function () {return null;}; 21 | require.extensions['.png'] = function () {return null;}; 22 | require.extensions['.jpg'] = function () {return null;}; 23 | 24 | // Configure JSDOM and set global variables 25 | // to simulate a browser environment for tests. 26 | var jsdom = require('jsdom').jsdom; 27 | 28 | var exposedProperties = ['window', 'navigator', 'document']; 29 | 30 | global.document = jsdom(''); 31 | global.window = document.defaultView; 32 | Object.keys(document.defaultView).forEach((property) => { 33 | if (typeof global[property] === 'undefined') { 34 | exposedProperties.push(property); 35 | global[property] = document.defaultView[property]; 36 | } 37 | }); 38 | 39 | global.navigator = { 40 | userAgent: 'node.js' 41 | }; 42 | 43 | documentRef = document; //eslint-disable-line no-undef 44 | -------------------------------------------------------------------------------- /webpack.config.dev.js: -------------------------------------------------------------------------------- 1 | let path = require('path'); 2 | let webpack = require('webpack'); 3 | 4 | const config = { 5 | devtool: 'source-map', 6 | entry: [ 7 | 'react-hot-loader/patch', 8 | 'webpack-hot-middleware/client?reload=false', 9 | './src/index' 10 | ], 11 | output: { 12 | path: path.join(__dirname, 'dist'), 13 | filename: 'bundle.js', 14 | publicPath: '/static/' 15 | }, 16 | plugins: [ 17 | new webpack.HotModuleReplacementPlugin() 18 | ], 19 | module: { 20 | loaders: [ 21 | {test: /\.js$/, include: path.join(__dirname, 'src'), loaders: ['babel']}, 22 | {test: /(\.css)$/, loaders: ['style', 'css']}, 23 | {test: /\.eot(\?v=\d+\.\d+\.\d+)?$/, loader: "file"}, 24 | {test: /\.woff(\?v=\d+\.\d+\.\d+)?$/, loader: "url?limit=10000&mimetype=application/font-woff"}, 25 | {test: /\.woff2(\?v=\d+\.\d+\.\d+)?$/, loader: "url?limit=10000&mimetype=application/font-woff"}, 26 | {test: /\.ttf(\?v=\d+\.\d+\.\d+)?$/, loader: "url?limit=10000&mimetype=application/octet-stream"}, 27 | {test: /\.svg(\?v=\d+\.\d+\.\d+)?$/, loader: "url?limit=10000&mimetype=image/svg+xml"} 28 | ] 29 | } 30 | }; 31 | 32 | export default config; 33 | --------------------------------------------------------------------------------