├── .babelrc ├── .editorconfig ├── .eslintrc ├── .gitignore ├── README.md ├── api ├── actions │ ├── auth.js │ └── movies.js ├── api.js └── server.js ├── devServer.js ├── package.json ├── src ├── components │ ├── AutoCounter │ │ ├── AutoCounter.js │ │ ├── AutoCounter.scss │ │ ├── AutoCounterItem.js │ │ └── colors.js │ ├── Counter.js │ ├── CounterMulti.jsx │ ├── CustomCounter.jsx │ ├── Header │ │ ├── Header.jsx │ │ └── Header.scss │ ├── SmartLink │ │ ├── SmartLink.js │ │ └── SmartLink.scss │ └── index.js ├── config.js ├── containers │ ├── CoreLayout.js │ ├── CountersPage │ │ └── CountersPage.jsx │ ├── LoginPage │ │ ├── LoginPage.jsx │ │ ├── LoginPage.scss │ │ └── loginForm │ │ │ ├── loginFormPlugin.js │ │ │ └── loginFormValidation.js │ ├── MoviesPage │ │ ├── MoviesPage.js │ │ └── MoviesPage.scss │ └── index.js ├── index.html ├── index.js ├── redux-base │ ├── configureStore.js │ ├── middleware │ │ └── promiseMiddleware.js │ └── modules │ │ ├── auth.js │ │ ├── counter.js │ │ ├── customCounter.js │ │ ├── movies.js │ │ ├── multiCounter.js │ │ └── reducer.js ├── routes.js ├── styles │ ├── _base.scss │ ├── main.scss │ └── vendor │ │ └── _normalize.scss └── utils │ └── DevTools.jsx ├── webpack.config.dev.js └── webpack.config.prod.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["react", "es2015", "stage-0"], 3 | "env": { 4 | "development": { 5 | "presets": ["react-hmre"] 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | # A special property that should be specified at the top of the file outside of 4 | # any sections. Set to true to stop .editor config file search on current file 5 | root = true 6 | 7 | # Indentation style 8 | # Possible values - tab, space 9 | indent_style = space 10 | 11 | # Indentation size in single-spaced characters 12 | # Possible values - an integer, tab 13 | indent_size = 2 14 | 15 | # Line ending file format 16 | # Possible values - lf, crlf, cr 17 | end_of_line = lf 18 | 19 | # File character encoding 20 | # Possible values - latin1, utf-8, utf-16be, utf-16le 21 | charset = utf-8 22 | 23 | # Denotes whether to trim whitespace at the end of lines 24 | # Possible values - true, false 25 | trim_trailing_whitespace = true 26 | 27 | # Denotes whether file should end with a newline 28 | # Possible values - true, false 29 | insert_final_newline = true 30 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "ecmaFeatures": { 3 | "jsx": true, 4 | "modules": true 5 | }, 6 | "extends": "eslint-config-airbnb", 7 | "env": { 8 | "browser": true, 9 | "node": true 10 | }, 11 | "parser": "babel-eslint", 12 | "rules": { 13 | "quotes": [2, "single"], 14 | "strict": [2, "never"], 15 | "babel/generator-star-spacing": 1, 16 | "babel/new-cap": 1, 17 | "babel/object-shorthand": 1, 18 | "babel/no-await-in-loop": 1, 19 | "react/jsx-uses-react": 2, 20 | "react/jsx-uses-vars": 2, 21 | "react/react-in-jsx-scope": 2, 22 | 23 | //overrides 24 | "max-len": [2, 300, 4], 25 | "comma-dangle": 0, 26 | "react/jsx-indent-props": 0, 27 | "react/jsx-no-bind": 0, 28 | "no-console": 0, 29 | "padded-blocks": 0, 30 | "no-debugger": 0 31 | }, 32 | "plugins": [ 33 | "babel", 34 | "react" 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | npm-debug.log 4 | dist -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-redux-router-crud-boilerplate 2 | Another redux example build on react transform boilerplate 3 | -------------------------------------------------------------------------------- /api/actions/auth.js: -------------------------------------------------------------------------------- 1 | export function login(req, res) { 2 | const credentials = req.body; 3 | if(credentials.userName==='admin@example.com' && credentials.password==='password'){ 4 | res.json({ 5 | userName: credentials.userName, 6 | role: 'ADMIN' 7 | }); 8 | }else{ 9 | // just demonstration of server-side validation 10 | res.status('401').send({ 11 | message : 'Invalid user/password', 12 | // userName - the same field name as used in form on client side 13 | validationErrors: { 14 | userName : 'Aha, server-side validation error', 15 | password: 'Use another password' 16 | } 17 | }); 18 | } 19 | } 20 | 21 | 22 | export function logout(req, res) { 23 | res.json({ 24 | 'userName': 'admin', 25 | 'role': 'ADMIN' 26 | }); 27 | } -------------------------------------------------------------------------------- /api/actions/movies.js: -------------------------------------------------------------------------------- 1 | const initialMovies = [ 2 | {id: 1, title: 'Red', sprocketCount: 7, owner: 'John'}, 3 | {id: 2, title: 'Taupe', sprocketCount: 1, owner: 'George'}, 4 | {id: 3, title: 'Green', sprocketCount: 8, owner: 'Ringo'}, 5 | {id: 4, title: 'Blue', sprocketCount: 2, owner: 'Paul'} 6 | ]; 7 | 8 | const getMovies = (req, res) => { 9 | let movies = req.session.movies; 10 | if (!movies) { 11 | movies = initialMovies; 12 | req.session.movies = movies; 13 | } 14 | return movies; 15 | } 16 | 17 | //----------GET ALL 18 | export function getAll(req, res){ 19 | console.info('----\n==> LOAD MOVIES'); 20 | return new Promise((resolve, reject) => { 21 | // make async call to database 22 | setTimeout(() => { 23 | if (Math.floor(Math.random() * 3) === 0) { 24 | reject('Movies load fails 33% of the time. You were unlucky.'); 25 | } else { 26 | resolve(getMovies(req)); 27 | } 28 | }, 1000); // simulate async load 29 | }); 30 | } -------------------------------------------------------------------------------- /api/api.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import session from 'express-session'; 3 | import bodyParser from 'body-parser'; 4 | import path from 'path'; 5 | 6 | import { login, logout } from './actions/auth'; 7 | import * as moviesController from './actions/movies'; 8 | 9 | var app = express(), 10 | router = express.Router(); 11 | 12 | var isProduction = process.env.NODE_ENV === 'production'; 13 | var port = isProduction ? process.env.PORT : 3001; 14 | 15 | app.use(session({ 16 | secret: 'react and redux rule!!!!', 17 | resave: false, 18 | saveUninitialized: false, 19 | cookie: { maxAge: 60000 } 20 | })); 21 | 22 | app.use(bodyParser.json({ type: 'application/json' })) 23 | 24 | app.use(function(req, res, next) { 25 | res.header('Access-Control-Allow-Origin', '*'); 26 | res.header('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE'); 27 | res.header('Access-Control-Allow-Headers', 'Content-Type'); 28 | 29 | next(); 30 | }); 31 | 32 | router.post('/login', login); 33 | router.post('/logout', logout); 34 | 35 | // actions added for movies 36 | router.route('/movies') 37 | .get( (req, res) => { 38 | moviesController.getAll(req, res) 39 | .then((result) => { 40 | res.json(result); 41 | }) 42 | .catch((reason) => { 43 | if (reason && reason.redirect) { 44 | res.redirect(reason.redirect); 45 | } else { 46 | console.error('API ERROR: ', reason); 47 | res.status(reason.status || 500).json(reason); 48 | } 49 | }); 50 | }); 51 | 52 | app.use('/api', router); 53 | 54 | app.listen(port, function () { 55 | console.log('Server running on port ' + port); 56 | }); -------------------------------------------------------------------------------- /api/server.js: -------------------------------------------------------------------------------- 1 | require('babel-register')(); 2 | 3 | // now ES6/7 features available in required files 4 | require('./api'); -------------------------------------------------------------------------------- /devServer.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var express = require('express'); 3 | var webpack = require('webpack'); 4 | var config = require('./webpack.config.dev'); 5 | 6 | var app = express(); 7 | var compiler = webpack(config); 8 | 9 | app.use(require('webpack-dev-middleware')(compiler, { 10 | noInfo: true, 11 | publicPath: config.output.publicPath 12 | })); 13 | 14 | app.use(require('webpack-hot-middleware')(compiler)); 15 | 16 | app.use(function(req, res) { 17 | res.sendFile(__dirname + '/src/index.html'); 18 | }); 19 | 20 | app.listen(3000, 'localhost', function(err) { 21 | if (err) { 22 | console.log(err); 23 | return; 24 | } 25 | 26 | console.log('Listening at http://localhost:3000'); 27 | }); 28 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-redux-router-crud-boilerplate", 3 | "version": "1.0.0", 4 | "description": "Another redux example build on react transform boilerplate.", 5 | "scripts": { 6 | "clean": "rimraf dist", 7 | "build:webpack": "NODE_ENV=production webpack --config webpack.config.prod.js", 8 | "build": "npm run clean && npm run build:webpack", 9 | "start": "npm run clean && node devServer.js", 10 | "api": "node ./api/server.js", 11 | "lint": "eslint src" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "https://github.com/jsdmc/react-redux-router-crud-boilerplate.git" 16 | }, 17 | "keywords": [ 18 | "react", 19 | "reactjs", 20 | "boilerplate", 21 | "hot", 22 | "reload", 23 | "hmr", 24 | "live", 25 | "edit", 26 | "webpack", 27 | "babel", 28 | "react-transform" 29 | ], 30 | "author": "Dmytro Chepunryi", 31 | "bugs": { 32 | "url": "https://github.com/jsdmc/react-redux-router-crud-boilerplate/issues" 33 | }, 34 | "homepage": "https://github.com/jsdmc/react-redux-router-crud-boilerplate", 35 | "dependencies": { 36 | "axios": "^0.9.1", 37 | "classnames": "^2.1.5", 38 | "multireducer": "^2.0.0", 39 | "react": "^0.14.0", 40 | "react-bootstrap": "^0.28.3", 41 | "react-dom": "^0.14.0", 42 | "react-redux": "^4.4.0", 43 | "react-router": "2.0.0", 44 | "redux": "^3.3.1", 45 | "redux-form": "^4.2.0", 46 | "react-router-redux": "^4.0.0", 47 | "redux-thunk": "^1.0.0" 48 | }, 49 | "devDependencies": { 50 | "autoprefixer-loader": "^3.1.0", 51 | "babel-core": "^6.5.2", 52 | "babel-eslint": "^5.0.0-beta10", 53 | "babel-loader": "^6.2.2", 54 | "babel-preset-es2015": "^6.3.13", 55 | "babel-preset-react": "^6.3.13", 56 | "babel-preset-react-hmre": "^1.0.0", 57 | "babel-preset-stage-0": "^6.5.0", 58 | "babel-register": "^6.5.2", 59 | "body-parser": "^1.14.1", 60 | "css-loader": "^0.23.1", 61 | "eslint": "^1.10.3", 62 | "eslint-config-airbnb": "^5.0.1", 63 | "eslint-loader": "^1.2.0", 64 | "eslint-plugin-babel": "^3.0.0", 65 | "eslint-plugin-react": "^3.11.3", 66 | "eventsource-polyfill": "^0.9.6", 67 | "express": "^4.13.3", 68 | "express-session": "^1.11.3", 69 | "extract-text-webpack-plugin": "^1.0.1", 70 | "file-loader": "^0.8.4", 71 | "node-sass": "^3.3.3", 72 | "redux-devtools": "^3.1.1", 73 | "redux-devtools-dock-monitor": "^1.0.1", 74 | "redux-devtools-log-monitor": "^1.0.4", 75 | "rimraf": "^2.4.3", 76 | "sass-loader": "^3.0.0", 77 | "style-loader": "^0.13.0", 78 | "webpack": "^1.12.9", 79 | "webpack-dev-middleware": "^1.4.0", 80 | "webpack-hot-middleware": "^2.6.0" 81 | }, 82 | "engines": { 83 | "node": "4.1.1" 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/components/AutoCounter/AutoCounter.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import AutoCounterItem from './AutoCounterItem'; 3 | import { NICE, SUPER_NICE } from './colors'; 4 | 5 | export default class AutoCounter extends Component { 6 | render() { 7 | const styles = require('./AutoCounter.scss'); 8 | return ( 9 |
10 |
11 | 12 | 13 |
14 |
15 | ); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/components/AutoCounter/AutoCounter.scss: -------------------------------------------------------------------------------- 1 | .app{ 2 | border-color: green; 3 | border-style: solid; 4 | } -------------------------------------------------------------------------------- /src/components/AutoCounter/AutoCounterItem.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | 3 | export default class AutoCounterItem extends Component { 4 | 5 | static propTypes = { 6 | increment: PropTypes.number.isRequired, 7 | color: PropTypes.string.isRequired, 8 | } 9 | 10 | constructor(props) { 11 | super(props); 12 | this.state = { counter: 0 }; 13 | this.interval = setInterval(() => this.tick(), 1000); 14 | } 15 | 16 | componentWillUnmount() { 17 | clearInterval(this.interval); 18 | } 19 | 20 | tick() { 21 | this.setState({ 22 | counter: this.state.counter + this.props.increment 23 | }); 24 | } 25 | 26 | render() { 27 | return ( 28 |
29 |

30 | Counter ({this.props.increment}): {this.state.counter} 31 |

32 |
33 | ); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/components/AutoCounter/colors.js: -------------------------------------------------------------------------------- 1 | export const NICE = 'pink'; 2 | export const SUPER_NICE = 'darkred'; 3 | -------------------------------------------------------------------------------- /src/components/Counter.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | // import { bindActionCreators } from 'redux'; 3 | import { connect } from 'react-redux'; 4 | import * as CounterActions from 'redux-base/modules/counter'; 5 | 6 | function mapStateToProps(state) { 7 | return { 8 | counter: state.counter 9 | }; 10 | } 11 | 12 | // function mapDispatchToProps(dispatch) { 13 | // return bindActionCreators(CounterActions, dispatch); 14 | // } 15 | 16 | export class Counter extends Component { 17 | render() { 18 | const { increment, incrementIfOdd, incrementAsync, decrement, counter } = this.props; 19 | return ( 20 |

21 | Clicked: {counter} times 22 | {' '} 23 | 24 | {' '} 25 | 26 | {' '} 27 | 28 | {' '} 29 | 30 |

31 | ); 32 | } 33 | } 34 | 35 | Counter.propTypes = { 36 | increment: PropTypes.func.isRequired, 37 | incrementIfOdd: PropTypes.func.isRequired, 38 | incrementAsync: PropTypes.func.isRequired, 39 | decrement: PropTypes.func.isRequired, 40 | counter: PropTypes.number.isRequired 41 | }; 42 | 43 | export default connect(mapStateToProps, CounterActions)(Counter); 44 | -------------------------------------------------------------------------------- /src/components/CounterMulti.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Counter } from './Counter'; 3 | import { connectMultireducer } from 'multireducer'; 4 | // import { multireducerBindActionCreators } from 'multireducer'; 5 | 6 | import * as CounterActions from 'redux-base/modules/multiCounter'; 7 | 8 | // function parameters are the same as in mapStateToProps from 'react-redux' 9 | // except 1st parameter which is always multireducer key 10 | const mapStateToProps = (key, state, ownProps) => { 11 | // 12 | console.log(ownProps.testProp); 13 | 14 | return { 15 | counter: state.multiCounters[key] 16 | }; 17 | }; 18 | 19 | // function parameters are the same as in mapDispatchToProps from 'react-redux' 20 | // except 1st parameter which is always multireducer key 21 | 22 | // const mapDispatchToProps = (key, dispatch, ownProps) => { 23 | // return multireducerBindActionCreators(key, CounterActions, dispatch); 24 | // }; 25 | 26 | export class CounterMulti extends Component { 27 | render() { 28 | 29 | return ( 30 |
31 | Multicounter 32 | 33 |
34 | ); 35 | } 36 | } 37 | 38 | // Can be mounted in both ways 39 | // 1st one is more flexible, because in mapDispatchToProps function 40 | // you can also return actions that should't be bound with multireducerKey for this component 41 | // 2nd way - see CustomCounter.jsx example 42 | 43 | // export default connectMultireducer(mapStateToProps, mapDispatchToProps)(CounterMulti); 44 | export default connectMultireducer(mapStateToProps, CounterActions)(CounterMulti); 45 | -------------------------------------------------------------------------------- /src/components/CustomCounter.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import { CounterMulti } from './CounterMulti'; 3 | import { connect } from 'react-redux'; 4 | import { bindActionCreators } from 'redux'; 5 | import { multireducerBindActionCreators } from 'multireducer'; 6 | import * as CounterActions from 'redux-base/modules/customCounter'; 7 | 8 | const customActions = { 9 | hello: () => { 10 | console.log('hello'); 11 | 12 | return { 13 | type: 'HELLO_ACTION' 14 | }; 15 | } 16 | }; 17 | 18 | const mapStateToProps = (state) => ({ 19 | counter: state.myNamespace.someCustomCounter 20 | }); 21 | 22 | const mapDispatchToProps = (dispatch) => ({ 23 | ...bindActionCreators(customActions, dispatch), 24 | ...multireducerBindActionCreators('customCounter', { ...CounterActions }, dispatch) 25 | }); 26 | 27 | class CustomCounter extends Component { 28 | render() { 29 | 30 | const { increment100, decrement100Async, hello, ...restProps } = this.props; 31 | 32 | return ( 33 |
34 | Custom counter 35 | 36 | 37 | 38 | 39 |
40 | ); 41 | } 42 | } 43 | 44 | CustomCounter.propTypes = { 45 | increment100: PropTypes.func.isRequired, 46 | decrement100Async: PropTypes.func.isRequired, 47 | hello: PropTypes.func.isRequired 48 | }; 49 | 50 | export default connect(mapStateToProps, mapDispatchToProps)(CustomCounter); 51 | -------------------------------------------------------------------------------- /src/components/Header/Header.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | // import { Link } from 'react-router'; 3 | import { connect } from 'react-redux'; 4 | import './Header.scss'; 5 | 6 | const mapStateToProps = (state) => ({ user: state.auth.user }); 7 | 8 | class Header extends Component { 9 | onLogoutClick(event) { 10 | event.preventDefault(); 11 | // this.props.handleLogout(); 12 | } 13 | 14 | render() { 15 | const { user } = this.props; 16 | return ( 17 | 56 | ); 57 | } 58 | } 59 | 60 | Header.propTypes = { 61 | user: PropTypes.object, 62 | }; 63 | 64 | export default connect(mapStateToProps)(Header); 65 | -------------------------------------------------------------------------------- /src/components/Header/Header.scss: -------------------------------------------------------------------------------- 1 | :global { 2 | .navbar-brand { 3 | position:relative; 4 | padding-left: 60; 5 | } 6 | 7 | .nav .header_fa { 8 | font-size: 1em; 9 | margin-right: 0.5em; 10 | } 11 | 12 | .navbar-default .navbar-nav>li>a:focus { 13 | outline: none; 14 | } 15 | } -------------------------------------------------------------------------------- /src/components/SmartLink/SmartLink.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import classnames from 'classnames'; 3 | import { Link } from 'react-router'; 4 | import { connect } from 'react-redux'; 5 | 6 | // Smart component that listens to url changes and rerenders inself 7 | // Decorator @connect no used for testability reasons. 8 | // Instead, function defined on top of component. It's more readable 9 | const mapStateToProps = (state) => ({ location: state.routing.locationBeforeTransitions }); 10 | 11 | class SmartLink extends Component { 12 | render() { 13 | // Load styles object and use generated class names. 14 | // Sinse the are unique by default (specified in webpack config) - you can be sure you will not breake global styles 15 | const styles = require('./SmartLink.scss'); 16 | 17 | const { title, url, location } = this.props; 18 | const active = location.pathname === url; 19 | return ( 20 |
  • 21 | {/* Objects literal features - Computed property names, Shorthand property names - ES6 22 | 23 | https://developer.mozilla.org/ru/docs/Web/JavaScript/Reference/Operators/Object_initializer 24 | 25 | For react router Link you can specify what class name to apply when route is active 26 | But it applies class to "a" tag. Bootstrap styles want "active" on "li". Let's do it 27 | 28 | https://github.com/rackt/react-router/blob/master/UPGRADE_GUIDE.md 29 | 30 | Functionality of SmartLink just shows how smart compoments should work. 31 | */} 32 | 33 | {title} 34 | 35 |
  • 36 | ); 37 | } 38 | } 39 | 40 | // static property moved out from class definition 41 | // Just looks better when it's at the bottom of the file. 42 | // When you'll define more properties - it takes to much space in the class definition 43 | SmartLink.propTypes = { 44 | title: PropTypes.string.isRequired, 45 | url: PropTypes.string.isRequired, 46 | location: PropTypes.object.isRequired 47 | }; 48 | 49 | export default connect(mapStateToProps)(SmartLink); 50 | -------------------------------------------------------------------------------- /src/components/SmartLink/SmartLink.scss: -------------------------------------------------------------------------------- 1 | .activeLink{ 2 | text-decoration: underline; 3 | font-size: 20px; 4 | } -------------------------------------------------------------------------------- /src/components/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Point of contact for component modules 3 | * 4 | * ie: import { AutoCounter, Counter } from 'components'; 5 | * 6 | */ 7 | 8 | export Header from './Header/Header'; 9 | export AutoCounter from './AutoCounter/AutoCounter'; 10 | export Counter from './Counter'; 11 | export CounterMulti from './CounterMulti'; 12 | export CustomCounter from './CustomCounter'; 13 | export SmartLink from './SmartLink/SmartLink'; 14 | -------------------------------------------------------------------------------- /src/config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | isProduction: process.env.NODE_ENV === 'production' 3 | }; 4 | -------------------------------------------------------------------------------- /src/containers/CoreLayout.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import { Header, SmartLink } from '../components'; 3 | import '../styles/main.scss'; 4 | 5 | export default class CoreLayout extends Component { 6 | 7 | static propTypes = { 8 | children: PropTypes.node 9 | } 10 | 11 | render() { 12 | return ( 13 |
    14 |
    15 |
    16 |
    17 |
    18 |
      19 | 20 | 21 | 22 | 23 |
    24 |
    25 |
    26 | {this.props.children} 27 |
    28 |
    29 |
    30 |
    31 | ); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/containers/CountersPage/CountersPage.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Counter, CounterMulti, CustomCounter } from 'components'; 3 | 4 | export class CountesPage extends Component { 5 | 6 | render() { 7 | 8 | return ( 9 |
    10 |
    11 | Global Counter 12 | 13 |
    14 |
    15 | Counter1 16 | 17 |
    18 |
    19 | Counter2 20 | 21 |
    22 |
    23 | Counter3 24 | 25 |
    26 |
    27 | 28 |
    29 |
    30 | ); 31 | } 32 | } 33 | 34 | export default CountesPage; 35 | -------------------------------------------------------------------------------- /src/containers/LoginPage/LoginPage.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import { reduxForm } from 'redux-form'; 3 | import { login } from 'redux-base/modules/auth'; 4 | import loginFormValidation from './loginForm/loginFormValidation'; 5 | import classnames from 'classnames'; 6 | import { browserHistory } from 'react-router'; 7 | 8 | // react binding for redux 9 | // https://github.com/rackt/react-redux 10 | const mapStateToProps = (state) => ({ 11 | user: state.auth.user, 12 | loginError: state.auth.loginError 13 | }); 14 | 15 | // super nice UX. Thanks to Redux-form all fields are validating when onChange and onBlur event occured 16 | // https://github.com/erikras/redux-form 17 | const reduxFormConfig = { 18 | form: 'loginForm', // the name of your form and the key to where your form's state will be mounted 19 | fields: ['userName', 'password', 'rememberMe'], // a list of all your fields in your form 20 | validate: loginFormValidation // a synchronous validation function 21 | }; 22 | 23 | // things above can be replaced with decorator connected to our component. 24 | /* 25 | @reduxForm({ 26 | form: 'loginForm', 27 | fields: ['userName', 'password', 'rememberMe'], 28 | validate: loginFormValidation 29 | }, 30 | state => ({ 31 | user: state.auth.user, 32 | loginError: state.auth.loginError 33 | })) 34 | */ 35 | 36 | class Login extends Component { 37 | 38 | // component will recieve new properies because we are listening for state.auth.user of global App state 39 | componentWillReceiveProps(nextProps) { 40 | if (nextProps.user) { 41 | // logged in, let's show home 42 | browserHistory.push('/counter'); 43 | } 44 | } 45 | 46 | // do some action when submitting form 47 | handleLogin(data) { 48 | this.props.dispatch(login(data.userName, data.password)); 49 | } 50 | 51 | render() { 52 | const styles = require('./LoginPage.scss'); 53 | 54 | // grab props related to redux-form 55 | const { user, loginError, fields: { userName, password, rememberMe }, handleSubmit } = this.props; 56 | 57 | const fieldClasses = (field, classes) => classnames(classes, { 58 | 'has-error': field.error && field.touched 59 | }); 60 | const errorBlock = (field) => (field.error && field.touched && {field.error}); 61 | 62 | return ( 63 |
    64 |
    65 |
    66 |
    67 |
    68 |

    Please Log in

    69 |
    70 |
    71 | 72 |
    73 |
    74 | 75 | { /* // will pass value, onBlur and onChange */ } 76 | 77 |
    78 | {errorBlock(userName)} 79 |
    80 | 81 |
    82 |
    83 | 84 | 85 |
    86 | {errorBlock(password)} 87 |
    88 | 89 |
    90 |
    91 | 94 |
    95 | {errorBlock(rememberMe)} 96 |
    97 | 98 | { 99 | !user && loginError && 100 |
    101 | {loginError.message} Hint: use admin@example.com/password to log in. 102 |
    103 | } 104 | 105 | 106 | 107 |
    108 |
    109 |
    110 |
    111 | ); 112 | } 113 | } 114 | 115 | Login.propTypes = { 116 | user: PropTypes.object, 117 | loginError: PropTypes.object, 118 | dispatch: PropTypes.func.isRequired, 119 | // redux-form related props 120 | fields: PropTypes.object.isRequired, 121 | handleSubmit: PropTypes.func.isRequired 122 | }; 123 | 124 | // export the wrapped with decorators component 125 | // of course '@' syntax can be used. But Sun approach helps to test component later 126 | export default reduxForm(reduxFormConfig, mapStateToProps)(Login); 127 | -------------------------------------------------------------------------------- /src/containers/LoginPage/LoginPage.scss: -------------------------------------------------------------------------------- 1 | .loginPage{ 2 | :global{ 3 | .panel-signin { 4 | margin-top: 25%; 5 | } 6 | 7 | .form-signin { 8 | max-width: auto; 9 | padding: 15px; 10 | margin: 0 auto; 11 | 12 | .checkbox { 13 | margin-bottom: 0px; 14 | } 15 | 16 | .form-control:focus { 17 | z-index: 2; 18 | } 19 | input[type="text"] { 20 | margin-bottom: 0px; 21 | } 22 | input[type="password"] { 23 | margin-bottom: 0px; 24 | } 25 | } 26 | 27 | .help-block { 28 | margin-bottom: 0; 29 | } 30 | 31 | .has-error .checkbox{ 32 | color: #333; 33 | } 34 | } 35 | } -------------------------------------------------------------------------------- /src/containers/LoginPage/loginForm/loginFormPlugin.js: -------------------------------------------------------------------------------- 1 | // import login action const 2 | import { LOGIN_FAIL } from 'redux-base/modules/auth'; 3 | 4 | export default { 5 | loginForm: (state, action) => { // <------ 'loginForm' is name of form given to connectReduxForm() 6 | switch (action.type) { 7 | case LOGIN_FAIL: { 8 | // and let's set error message we got from server 9 | // submitError - property of each field that transformed to 'error' 10 | const actionError = action.error.data; 11 | const invalidFields = {}; 12 | 13 | if (actionError && actionError.validationErrors) { 14 | 15 | const validationErrors = actionError.validationErrors; 16 | 17 | for (const fieldName of Object.keys(validationErrors)) { 18 | invalidFields[fieldName] = { ...state[fieldName], submitError: validationErrors[fieldName] }; 19 | } 20 | } 21 | // replace errors for invalid fields 22 | // and make any other changes with field values 23 | return { 24 | ...state, 25 | ...invalidFields, 26 | rememberMe: {} // <----- uncheck rememberMe 27 | }; 28 | } 29 | default: 30 | return state; 31 | } 32 | } 33 | }; 34 | -------------------------------------------------------------------------------- /src/containers/LoginPage/loginForm/loginFormValidation.js: -------------------------------------------------------------------------------- 1 | function checkEmail(value) { 2 | return /\S+@\S+\.\S+/i.test(value); 3 | } 4 | 5 | export default function validateLoginForm(data) { 6 | const errors = { }; 7 | 8 | if (!data.userName) { 9 | errors.userName = 'Required login'; 10 | } else if (!checkEmail(data.userName)) { 11 | errors.userName = 'Invalid email address'; 12 | } 13 | 14 | if (!data.password) { 15 | errors.password = 'Password required'; 16 | } else if (data.password.length < 5) { 17 | errors.password = 'Must be more than 5 characters'; 18 | } 19 | 20 | // just for demonstration 21 | if (!data.rememberMe) { 22 | errors.rememberMe = 'Required option'; 23 | } 24 | 25 | return errors; 26 | } 27 | -------------------------------------------------------------------------------- /src/containers/MoviesPage/MoviesPage.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { isLoaded, load } from 'redux-base/modules/movies'; 4 | 5 | const mapStateToProps = state => ({ 6 | movies: state.movies.data, 7 | error: state.movies.error, 8 | loading: state.movies.loading, 9 | isDataLoaded: isLoaded(state) 10 | }); 11 | 12 | const mapActionsToProps = { load }; 13 | 14 | export class MoviesPage extends Component { 15 | 16 | componentWillMount() { 17 | const { isDataLoaded, load: loadData } = this.props; 18 | 19 | if (!isDataLoaded) { 20 | loadData(); 21 | } 22 | } 23 | 24 | render() { 25 | const { movies, error, loading } = this.props; 26 | let refreshClassName = 'fa fa-refresh'; 27 | if (loading) { 28 | refreshClassName += ' fa-spin'; 29 | } 30 | 31 | const styles = require('./MoviesPage.scss'); 32 | 33 | return ( 34 |
    35 |

    Dashboard

    36 | 37 |
    38 |
    39 | Generic placeholder thumbnail 40 |

    Label

    41 | Something else 42 |
    43 |
    44 | Generic placeholder thumbnail 45 |

    Label

    46 | Something else 47 |
    48 |
    49 | Generic placeholder thumbnail 50 |

    Label

    51 | Something else 52 |
    53 |
    54 | Generic placeholder thumbnail 55 |

    Label

    56 | Something else 57 |
    58 |
    59 | 60 |

    Movies

    61 | 62 | 65 | 66 |

    67 | This widgets are stored in your session, so feel free to edit it and refresh. 68 |

    69 | 70 | {error && 71 |
    72 | 73 | {' '} 74 | {error.toString()} 75 |
    } 76 | 77 | {loading && 78 |
    79 | Loading...(here you can render spinner or whatever) 80 |
    } 81 | 82 |
    83 | {movies && movies.length && 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | { 95 | movies.map((movie) => 96 | 97 | 98 | 99 | 100 | 101 | ) 102 | } 103 | 104 |
    #HeaderHeaderHeader
    {movie.id}{movie.title}{movie.sprocketCount}{movie.owner}
    105 | } 106 |
    107 |
    108 | ); 109 | } 110 | } 111 | 112 | MoviesPage.propTypes = { 113 | movies: PropTypes.array, 114 | error: PropTypes.string, 115 | loading: PropTypes.bool, 116 | load: PropTypes.func.isRequired, 117 | isDataLoaded: PropTypes.bool.isRequired 118 | }; 119 | 120 | export default connect(mapStateToProps, mapActionsToProps)(MoviesPage); 121 | -------------------------------------------------------------------------------- /src/containers/MoviesPage/MoviesPage.scss: -------------------------------------------------------------------------------- 1 | .refreshBtn{ 2 | margin-bottom: 15px; 3 | } -------------------------------------------------------------------------------- /src/containers/index.js: -------------------------------------------------------------------------------- 1 | export CoreLayout from './CoreLayout'; 2 | export MoviesPage from './MoviesPage/MoviesPage'; 3 | export LoginPage from './LoginPage/LoginPage'; 4 | export CountersPage from './CountersPage/CountersPage'; 5 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | React Transform Boilerplate 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 |
    22 |
    23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | // react should be included when use ReactDom 2 | import React from 'react'; 3 | import ReactDOM from 'react-dom'; 4 | import { Router, browserHistory } from 'react-router'; 5 | import { syncHistoryWithStore } from 'react-router-redux'; 6 | 7 | import { Provider } from 'react-redux'; 8 | import configureStore from './redux-base/configureStore'; 9 | import getRoutes from './routes'; 10 | 11 | import config from 'config'; 12 | 13 | const store = configureStore(); 14 | const history = syncHistoryWithStore(browserHistory, store); 15 | 16 | let appRootComponent; 17 | 18 | if (!config.isProduction) { 19 | // Use require because imports can't be conditional. 20 | // In production, you should ensure process.env.NODE_ENV 21 | // is envified so that Uglify can eliminate this 22 | // module and its dependencies as dead code. 23 | const DevTools = require('utils/DevTools').default; 24 | appRootComponent = () => ( 25 | 26 |
    27 | 28 | 29 |
    30 |
    31 | ); 32 | } else { 33 | appRootComponent = () => ( 34 | 35 | 36 | 37 | ); 38 | } 39 | 40 | ReactDOM.render(appRootComponent(), document.getElementById('root')); 41 | -------------------------------------------------------------------------------- /src/redux-base/configureStore.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware, compose } from 'redux'; 2 | import thunk from 'redux-thunk'; 3 | import promiseMiddleware from './middleware/promiseMiddleware'; 4 | import reducer from './modules/reducer'; 5 | import config from 'config'; 6 | 7 | const middlewares = [ 8 | applyMiddleware( 9 | thunk, 10 | promiseMiddleware 11 | ) 12 | ]; 13 | 14 | // use only for dev mode 15 | if (!config.isProduction) { 16 | const DevTools = require('utils/DevTools').default; 17 | middlewares.push(DevTools.instrument()); 18 | } 19 | 20 | export default function configureStore(initialState) { 21 | const store = createStore( 22 | reducer, 23 | initialState, 24 | compose(...middlewares) 25 | ); 26 | 27 | if (!config.isProduction && module.hot) { 28 | // Enable Webpack hot module replacement for reducers 29 | module.hot.accept('./modules/reducer', () => { 30 | const nextReducer = require('./modules/reducer'); 31 | store.replaceReducer(nextReducer); 32 | }); 33 | } 34 | 35 | return store; 36 | } 37 | -------------------------------------------------------------------------------- /src/redux-base/middleware/promiseMiddleware.js: -------------------------------------------------------------------------------- 1 | export default function promiseMiddleware() { 2 | return next => action => { 3 | const { promise, types, ...rest } = action; 4 | 5 | if (!promise) { 6 | return next(action); 7 | } 8 | 9 | const [REQUEST, SUCCESS, FAILURE] = types; 10 | 11 | next({ ...rest, type: REQUEST }); 12 | 13 | return promise.then( 14 | (result) => next({ ...rest, result, type: SUCCESS }), 15 | (error) => next({ ...rest, error, type: FAILURE }) 16 | ).catch(error => { 17 | console.error('MIDDLEWARE ERROR:', error); 18 | next({ ...rest, error, type: FAILURE }); 19 | }); 20 | }; 21 | } 22 | -------------------------------------------------------------------------------- /src/redux-base/modules/auth.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | // --------------------------- Action constants -------------------------- 4 | 5 | // names for actions can be more specific 6 | const LOGIN = 'LOGIN'; 7 | const LOGIN_SUCCESS = 'LOGIN_SUCCESS'; 8 | // if other components need to react to some action of current module - export corresponding action type 9 | export const LOGIN_FAIL = 'LOGIN_FAIL'; 10 | 11 | const LOGOUT = 'LOGOUT'; 12 | const LOGOUT_SUCCESS = 'LOGOUT_SUCCESS'; 13 | const LOGOUT_FAIL = 'LOGOUT_FAIL'; 14 | 15 | // --------------------------- Reducer function -------------------------- 16 | 17 | const initialState = { 18 | // user: null, 19 | // password: null, 20 | // userRole: null, 21 | // loggingIn: false, 22 | // loggingOut: false, 23 | // loginError: null, 24 | }; 25 | 26 | export default function auth(state = initialState, action = {}) { 27 | switch (action.type) { 28 | case LOGIN: 29 | return { 30 | ...state, 31 | loggingIn: true 32 | }; 33 | 34 | case LOGIN_SUCCESS: 35 | return { 36 | ...state, 37 | loggingIn: false, 38 | user: action.result.data, 39 | role: action.result.data.role 40 | }; 41 | 42 | case LOGIN_FAIL: 43 | return { 44 | ...state, 45 | loggingIn: false, 46 | user: null, 47 | role: null, 48 | loginError: action.error 49 | }; 50 | case LOGOUT: 51 | return { 52 | ...state, 53 | loggingOut: true 54 | }; 55 | case LOGOUT_SUCCESS: 56 | return { 57 | ...state, 58 | loggingOut: false, 59 | user: null, 60 | userRole: null, 61 | loginError: null 62 | }; 63 | case LOGOUT_FAIL: 64 | return { 65 | ...state, 66 | loggingOut: false, 67 | logoutError: action.error 68 | }; 69 | default: 70 | return state; 71 | } 72 | } 73 | 74 | // --------------------------- Action functions -------------------------- 75 | 76 | export function login(userName, password) { 77 | return { 78 | types: [LOGIN, LOGIN_SUCCESS, LOGIN_FAIL], 79 | promise: axios 80 | .post('http://localhost:3001/api/login', { userName, password }) 81 | }; 82 | } 83 | 84 | export function logout(userName) { 85 | return { 86 | types: [LOGOUT, LOGOUT_SUCCESS, LOGOUT_FAIL], 87 | promise: axios 88 | .post('http://localhost:3001/api/logout', { userName }) 89 | }; 90 | } 91 | -------------------------------------------------------------------------------- /src/redux-base/modules/counter.js: -------------------------------------------------------------------------------- 1 | // --------------------------- Action constants -------------------------- 2 | export const INCREMENT_COUNTER = 'INCREMENT_COUNTER'; 3 | export const DECREMENT_COUNTER = 'DECREMENT_COUNTER'; 4 | 5 | // --------------------------- Reducer function -------------------------- 6 | export default function counter(state = 0, action = {}) { 7 | switch (action.type) { 8 | case INCREMENT_COUNTER: 9 | return state + 1; 10 | case DECREMENT_COUNTER: 11 | return state - 1; 12 | default: 13 | return state; 14 | } 15 | } 16 | 17 | // --------------------------- Action functions -------------------------- 18 | export function increment() { 19 | return { 20 | type: INCREMENT_COUNTER 21 | }; 22 | } 23 | 24 | export function decrement() { 25 | return { 26 | type: DECREMENT_COUNTER 27 | }; 28 | } 29 | 30 | export function incrementIfOdd() { 31 | return (dispatch, getState) => { 32 | const { counter: counterState } = getState(); 33 | 34 | if (counterState % 2 === 0) { 35 | return; 36 | } 37 | 38 | dispatch(increment()); 39 | }; 40 | } 41 | 42 | export function incrementAsync(delay = 1000) { 43 | return dispatch => { 44 | setTimeout(() => { 45 | dispatch(increment()); 46 | }, delay); 47 | }; 48 | } 49 | -------------------------------------------------------------------------------- /src/redux-base/modules/customCounter.js: -------------------------------------------------------------------------------- 1 | import baseCounter from './multiCounter'; 2 | 3 | // --------------------------- Action constants -------------------------- 4 | const INCREMENT100 = 'INCREMENT100'; 5 | const DECREMENT100_ASYNC = 'DECREMENT100_ASYNC'; 6 | 7 | // --------------------------- Reducer function -------------------------- 8 | export default function counter(state = 0, action = {}) { 9 | switch (action.type) { 10 | case INCREMENT100: 11 | return state + 100; 12 | case DECREMENT100_ASYNC: 13 | return state - 100; 14 | default: 15 | return baseCounter(state, action); 16 | } 17 | } 18 | 19 | import { increment } from './multiCounter'; 20 | // re-export other actions 21 | export { INCREMENT_COUNTER, DECREMENT_COUNTER, increment, decrement, incrementAsync } from './multiCounter'; 22 | 23 | // --------------------------- Action functions -------------------------- 24 | export function incrementIfOdd() { 25 | return (dispatch, getState) => { 26 | const { myNamespace: { someCustomCounter: counterState } } = getState(); 27 | 28 | if (counterState % 2 === 0) { 29 | return; 30 | } 31 | 32 | dispatch(increment()); 33 | }; 34 | } 35 | 36 | 37 | export function increment100() { 38 | return { 39 | type: INCREMENT100 40 | }; 41 | } 42 | 43 | // let's say it's some action imported from some duck or other module 44 | const anotherThunkAction = () => dispatch => { 45 | dispatch({ type: 'HELLO_ACTION_ASYNC' }); 46 | }; 47 | 48 | export function decrement100Async() { 49 | return (dispatch, getState, dispatchGlobal) => { 50 | 51 | const { someCustomCounter } = getState(); 52 | 53 | console.log(someCustomCounter); 54 | 55 | dispatch({ type: DECREMENT100_ASYNC }); 56 | 57 | // Lets decrement global counter 58 | dispatchGlobal({ type: 'DECREMENT_COUNTER' }); 59 | 60 | // we can dispatch thunks to global namespace 61 | // and from within the thunk action also will be dispatched as global - 62 | // means action type will contain reducer key specified for sonnected component 63 | dispatchGlobal(anotherThunkAction()); 64 | }; 65 | } 66 | -------------------------------------------------------------------------------- /src/redux-base/modules/movies.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | // --------------------------- Action constants -------------------------- 4 | const LOAD = 'movies/LOAD'; 5 | const LOAD_SUCCESS = 'movies/LOAD_SUCCESS'; 6 | const LOAD_FAIL = 'movies/LOAD_FAIL'; 7 | 8 | // --------------------------- Reducer function -------------------------- 9 | const initialState = { 10 | loaded: false, 11 | loading: true, 12 | data: null, 13 | error: null 14 | }; 15 | 16 | export default function reducer(state = initialState, action = {}) { 17 | switch (action.type) { 18 | case LOAD: 19 | return { 20 | ...state, 21 | loading: true 22 | }; 23 | case LOAD_SUCCESS: 24 | return { 25 | ...state, 26 | loading: false, 27 | loaded: true, 28 | data: action.result.data, 29 | error: null 30 | }; 31 | case LOAD_FAIL: 32 | return { 33 | ...state, 34 | loading: false, 35 | loaded: false, 36 | data: null, 37 | error: action.error.data 38 | }; 39 | default: 40 | return state; 41 | } 42 | } 43 | 44 | // --------------------------- Action functions -------------------------- 45 | export function isLoaded(globalState) { 46 | return globalState.movies && globalState.movies.loaded; 47 | } 48 | 49 | export function load() { 50 | return { 51 | types: [LOAD, LOAD_SUCCESS, LOAD_FAIL], 52 | promise: axios.get('http://localhost:3001/api/movies') 53 | }; 54 | } 55 | -------------------------------------------------------------------------------- /src/redux-base/modules/multiCounter.js: -------------------------------------------------------------------------------- 1 | // example of ducks composition 2 | export default from './counter'; 3 | 4 | import { increment } from './counter'; 5 | // skip incrementIfOdd actionCreator - we will redefine it 6 | export { INCREMENT_COUNTER, DECREMENT_COUNTER, increment, decrement, incrementAsync } from './counter'; 7 | 8 | export function incrementIfOdd() { 9 | return (dispatch, getState, globalDispatch, reducerKey) => { 10 | const { multiCounters: { [reducerKey]: counterState } } = getState(); 11 | 12 | if (counterState % 2 === 0) { 13 | return; 14 | } 15 | 16 | dispatch(increment()); 17 | }; 18 | } 19 | -------------------------------------------------------------------------------- /src/redux-base/modules/reducer.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | 3 | import counter from './counter'; 4 | import customCounter from './customCounter'; 5 | import auth from './auth'; 6 | import movies from './movies'; 7 | import { routerReducer } from 'react-router-redux'; 8 | import { reducer as formReducer } from 'redux-form'; 9 | import loginFormPlugin from 'containers/LoginPage/loginForm/loginFormPlugin'; 10 | 11 | import multireducer from 'multireducer'; 12 | 13 | export default combineReducers({ 14 | routing: routerReducer, 15 | auth, 16 | counter, 17 | multiCounters: multireducer({ 18 | counter1: counter, 19 | counter2: counter, 20 | counter3: counter 21 | }), 22 | myNamespace: combineReducers({ 23 | someCustomCounter: multireducer(customCounter, 'customCounter'), // <-- catch actions that contain the key 24 | }), 25 | form: formReducer.plugin(loginFormPlugin), 26 | movies 27 | }); 28 | -------------------------------------------------------------------------------- /src/routes.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Route, Redirect } from 'react-router'; 3 | import { CoreLayout, MoviesPage, LoginPage, CountersPage } from './containers'; 4 | import { Counter, AutoCounter } from './components'; 5 | 6 | export default (store) => { 7 | const requireLogin = (nextState, replace) => { 8 | const { auth: { user } } = store.getState(); 9 | if (!user) { 10 | // oops, not logged in, so can't be here! 11 | replace({ pathname: '/login' }); 12 | } 13 | }; 14 | 15 | return ( 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | ); 27 | }; 28 | -------------------------------------------------------------------------------- /src/styles/_base.scss: -------------------------------------------------------------------------------- 1 | // Settings go here 2 | 3 | // Base styles taken from 'dashboard.css' 4 | // http://getbootstrap.com/examples/dashboard/ 5 | /* 6 | * Base structure 7 | */ 8 | 9 | // Wrap into global scope. 10 | // Because by default local scope is set and each class have unique name, so we need to avoid this 11 | // if we want our example to look the same as dashboard example 12 | :global { 13 | /* Move down content because we have a fixed navbar that is 50px tall */ 14 | body { 15 | padding-top: 50px; 16 | } 17 | 18 | 19 | /* 20 | * Global add-ons 21 | */ 22 | 23 | .sub-header { 24 | padding-bottom: 10px; 25 | border-bottom: 1px solid #eee; 26 | } 27 | 28 | /* 29 | * Top navigation 30 | * Hide default border to remove 1px line. 31 | */ 32 | .navbar-fixed-top { 33 | border: 0; 34 | } 35 | 36 | /* 37 | * Sidebar 38 | */ 39 | 40 | /* Hide for mobile, show later */ 41 | .sidebar { 42 | display: none; 43 | } 44 | @media (min-width: 768px) { 45 | .sidebar { 46 | position: fixed; 47 | top: 51px; 48 | bottom: 0; 49 | left: 0; 50 | z-index: 1000; 51 | display: block; 52 | padding: 20px; 53 | overflow-x: hidden; 54 | overflow-y: auto; /* Scrollable contents if viewport is shorter than content. */ 55 | background-color: #f5f5f5; 56 | border-right: 1px solid #eee; 57 | } 58 | } 59 | 60 | /* Sidebar navigation */ 61 | .nav-sidebar { 62 | margin-right: -21px; /* 20px padding + 1px border */ 63 | margin-bottom: 20px; 64 | margin-left: -20px; 65 | } 66 | .nav-sidebar > li > a { 67 | padding-right: 20px; 68 | padding-left: 20px; 69 | } 70 | .nav-sidebar > .active > a, 71 | .nav-sidebar > .active > a:hover, 72 | .nav-sidebar > .active > a:focus { 73 | color: #fff; 74 | background-color: #428bca; 75 | } 76 | 77 | 78 | /* 79 | * Main content 80 | */ 81 | 82 | .main { 83 | padding: 20px; 84 | } 85 | @media (min-width: 768px) { 86 | .main { 87 | padding-right: 40px; 88 | padding-left: 40px; 89 | } 90 | } 91 | .main .page-header { 92 | margin-top: 0; 93 | } 94 | 95 | 96 | /* 97 | * Placeholder dashboard ideas 98 | */ 99 | 100 | .placeholders { 101 | margin-bottom: 30px; 102 | text-align: center; 103 | } 104 | .placeholders h4 { 105 | margin-bottom: 0; 106 | } 107 | .placeholder { 108 | margin-bottom: 20px; 109 | } 110 | .placeholder img { 111 | display: inline-block; 112 | border-radius: 50%; 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/styles/main.scss: -------------------------------------------------------------------------------- 1 | @import 'base'; 2 | @import 'vendor/normalize'; 3 | 4 | body{ 5 | background-color: #EEEAF5; 6 | } 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/styles/vendor/_normalize.scss: -------------------------------------------------------------------------------- 1 | /*! normalize.css v3.0.2 | MIT License | git.io/normalize */ 2 | 3 | /** 4 | * 1. Set default font family to sans-serif. 5 | * 2. Prevent iOS text size adjust after orientation change, without disabling 6 | * user zoom. 7 | */ 8 | 9 | html { 10 | font-family: sans-serif; /* 1 */ 11 | -ms-text-size-adjust: 100%; /* 2 */ 12 | -webkit-text-size-adjust: 100%; /* 2 */ 13 | } 14 | 15 | /** 16 | * Remove default margin. 17 | */ 18 | 19 | body { 20 | margin: 0; 21 | } 22 | 23 | /* HTML5 display definitions 24 | ========================================================================== */ 25 | 26 | /** 27 | * Correct `block` display not defined for any HTML5 element in IE 8/9. 28 | * Correct `block` display not defined for `details` or `summary` in IE 10/11 29 | * and Firefox. 30 | * Correct `block` display not defined for `main` in IE 11. 31 | */ 32 | 33 | article, 34 | aside, 35 | details, 36 | figcaption, 37 | figure, 38 | footer, 39 | header, 40 | hgroup, 41 | main, 42 | menu, 43 | nav, 44 | section, 45 | summary { 46 | display: block; 47 | } 48 | 49 | /** 50 | * 1. Correct `inline-block` display not defined in IE 8/9. 51 | * 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera. 52 | */ 53 | 54 | audio, 55 | canvas, 56 | progress, 57 | video { 58 | display: inline-block; /* 1 */ 59 | vertical-align: baseline; /* 2 */ 60 | } 61 | 62 | /** 63 | * Prevent modern browsers from displaying `audio` without controls. 64 | * Remove excess height in iOS 5 devices. 65 | */ 66 | 67 | audio:not([controls]) { 68 | display: none; 69 | height: 0; 70 | } 71 | 72 | /** 73 | * Address `[hidden]` styling not present in IE 8/9/10. 74 | * Hide the `template` element in IE 8/9/11, Safari, and Firefox < 22. 75 | */ 76 | 77 | [hidden], 78 | template { 79 | display: none; 80 | } 81 | 82 | /* Links 83 | ========================================================================== */ 84 | 85 | /** 86 | * Remove the gray background color from active links in IE 10. 87 | */ 88 | 89 | a { 90 | background-color: transparent; 91 | } 92 | 93 | /** 94 | * Improve readability when focused and also mouse hovered in all browsers. 95 | */ 96 | 97 | a:active, 98 | a:hover { 99 | outline: 0; 100 | } 101 | 102 | /* Text-level semantics 103 | ========================================================================== */ 104 | 105 | /** 106 | * Address styling not present in IE 8/9/10/11, Safari, and Chrome. 107 | */ 108 | 109 | abbr[title] { 110 | border-bottom: 1px dotted; 111 | } 112 | 113 | /** 114 | * Address style set to `bolder` in Firefox 4+, Safari, and Chrome. 115 | */ 116 | 117 | b, 118 | strong { 119 | font-weight: bold; 120 | } 121 | 122 | /** 123 | * Address styling not present in Safari and Chrome. 124 | */ 125 | 126 | dfn { 127 | font-style: italic; 128 | } 129 | 130 | /** 131 | * Address variable `h1` font-size and margin within `section` and `article` 132 | * contexts in Firefox 4+, Safari, and Chrome. 133 | */ 134 | 135 | h1 { 136 | font-size: 2em; 137 | margin: 0.67em 0; 138 | } 139 | 140 | /** 141 | * Address styling not present in IE 8/9. 142 | */ 143 | 144 | mark { 145 | background: #ff0; 146 | color: #000; 147 | } 148 | 149 | /** 150 | * Address inconsistent and variable font size in all browsers. 151 | */ 152 | 153 | small { 154 | font-size: 80%; 155 | } 156 | 157 | /** 158 | * Prevent `sub` and `sup` affecting `line-height` in all browsers. 159 | */ 160 | 161 | sub, 162 | sup { 163 | font-size: 75%; 164 | line-height: 0; 165 | position: relative; 166 | vertical-align: baseline; 167 | } 168 | 169 | sup { 170 | top: -0.5em; 171 | } 172 | 173 | sub { 174 | bottom: -0.25em; 175 | } 176 | 177 | /* Embedded content 178 | ========================================================================== */ 179 | 180 | /** 181 | * Remove border when inside `a` element in IE 8/9/10. 182 | */ 183 | 184 | img { 185 | border: 0; 186 | } 187 | 188 | /** 189 | * Correct overflow not hidden in IE 9/10/11. 190 | */ 191 | 192 | svg:not(:root) { 193 | overflow: hidden; 194 | } 195 | 196 | /* Grouping content 197 | ========================================================================== */ 198 | 199 | /** 200 | * Address margin not present in IE 8/9 and Safari. 201 | */ 202 | 203 | figure { 204 | margin: 1em 40px; 205 | } 206 | 207 | /** 208 | * Address differences between Firefox and other browsers. 209 | */ 210 | 211 | hr { 212 | -moz-box-sizing: content-box; 213 | box-sizing: content-box; 214 | height: 0; 215 | } 216 | 217 | /** 218 | * Contain overflow in all browsers. 219 | */ 220 | 221 | pre { 222 | overflow: auto; 223 | } 224 | 225 | /** 226 | * Address odd `em`-unit font size rendering in all browsers. 227 | */ 228 | 229 | code, 230 | kbd, 231 | pre, 232 | samp { 233 | font-family: monospace, monospace; 234 | font-size: 1em; 235 | } 236 | 237 | /* Forms 238 | ========================================================================== */ 239 | 240 | /** 241 | * Known limitation: by default, Chrome and Safari on OS X allow very limited 242 | * styling of `select`, unless a `border` property is set. 243 | */ 244 | 245 | /** 246 | * 1. Correct color not being inherited. 247 | * Known issue: affects color of disabled elements. 248 | * 2. Correct font properties not being inherited. 249 | * 3. Address margins set differently in Firefox 4+, Safari, and Chrome. 250 | */ 251 | 252 | button, 253 | input, 254 | optgroup, 255 | select, 256 | textarea { 257 | color: inherit; /* 1 */ 258 | font: inherit; /* 2 */ 259 | margin: 0; /* 3 */ 260 | } 261 | 262 | /** 263 | * Address `overflow` set to `hidden` in IE 8/9/10/11. 264 | */ 265 | 266 | button { 267 | overflow: visible; 268 | } 269 | 270 | /** 271 | * Address inconsistent `text-transform` inheritance for `button` and `select`. 272 | * All other form control elements do not inherit `text-transform` values. 273 | * Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera. 274 | * Correct `select` style inheritance in Firefox. 275 | */ 276 | 277 | button, 278 | select { 279 | text-transform: none; 280 | } 281 | 282 | /** 283 | * 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio` 284 | * and `video` controls. 285 | * 2. Correct inability to style clickable `input` types in iOS. 286 | * 3. Improve usability and consistency of cursor style between image-type 287 | * `input` and others. 288 | */ 289 | 290 | button, 291 | html input[type="button"], /* 1 */ 292 | input[type="reset"], 293 | input[type="submit"] { 294 | -webkit-appearance: button; /* 2 */ 295 | cursor: pointer; /* 3 */ 296 | } 297 | 298 | /** 299 | * Re-set default cursor for disabled elements. 300 | */ 301 | 302 | button[disabled], 303 | html input[disabled] { 304 | cursor: default; 305 | } 306 | 307 | /** 308 | * Remove inner padding and border in Firefox 4+. 309 | */ 310 | 311 | button::-moz-focus-inner, 312 | input::-moz-focus-inner { 313 | border: 0; 314 | padding: 0; 315 | } 316 | 317 | /** 318 | * Address Firefox 4+ setting `line-height` on `input` using `!important` in 319 | * the UA stylesheet. 320 | */ 321 | 322 | input { 323 | line-height: normal; 324 | } 325 | 326 | /** 327 | * It's recommended that you don't attempt to style these elements. 328 | * Firefox's implementation doesn't respect box-sizing, padding, or width. 329 | * 330 | * 1. Address box sizing set to `content-box` in IE 8/9/10. 331 | * 2. Remove excess padding in IE 8/9/10. 332 | */ 333 | 334 | input[type="checkbox"], 335 | input[type="radio"] { 336 | box-sizing: border-box; /* 1 */ 337 | padding: 0; /* 2 */ 338 | } 339 | 340 | /** 341 | * Fix the cursor style for Chrome's increment/decrement buttons. For certain 342 | * `font-size` values of the `input`, it causes the cursor style of the 343 | * decrement button to change from `default` to `text`. 344 | */ 345 | 346 | input[type="number"]::-webkit-inner-spin-button, 347 | input[type="number"]::-webkit-outer-spin-button { 348 | height: auto; 349 | } 350 | 351 | /** 352 | * 1. Address `appearance` set to `searchfield` in Safari and Chrome. 353 | * 2. Address `box-sizing` set to `border-box` in Safari and Chrome 354 | * (include `-moz` to future-proof). 355 | */ 356 | 357 | input[type="search"] { 358 | -webkit-appearance: textfield; /* 1 */ 359 | -moz-box-sizing: content-box; 360 | -webkit-box-sizing: content-box; /* 2 */ 361 | box-sizing: content-box; 362 | } 363 | 364 | /** 365 | * Remove inner padding and search cancel button in Safari and Chrome on OS X. 366 | * Safari (but not Chrome) clips the cancel button when the search input has 367 | * padding (and `textfield` appearance). 368 | */ 369 | 370 | input[type="search"]::-webkit-search-cancel-button, 371 | input[type="search"]::-webkit-search-decoration { 372 | -webkit-appearance: none; 373 | } 374 | 375 | /** 376 | * Define consistent border, margin, and padding. 377 | */ 378 | 379 | fieldset { 380 | border: 1px solid #c0c0c0; 381 | margin: 0 2px; 382 | padding: 0.35em 0.625em 0.75em; 383 | } 384 | 385 | /** 386 | * 1. Correct `color` not being inherited in IE 8/9/10/11. 387 | * 2. Remove padding so people aren't caught out if they zero out fieldsets. 388 | */ 389 | 390 | legend { 391 | border: 0; /* 1 */ 392 | padding: 0; /* 2 */ 393 | } 394 | 395 | /** 396 | * Remove default vertical scrollbar in IE 8/9/10/11. 397 | */ 398 | 399 | textarea { 400 | overflow: auto; 401 | } 402 | 403 | /** 404 | * Don't inherit the `font-weight` (applied by a rule above). 405 | * NOTE: the default cannot safely be changed in Chrome and Safari on OS X. 406 | */ 407 | 408 | optgroup { 409 | font-weight: bold; 410 | } 411 | 412 | /* Tables 413 | ========================================================================== */ 414 | 415 | /** 416 | * Remove most spacing between table cells. 417 | */ 418 | 419 | table { 420 | border-collapse: collapse; 421 | border-spacing: 0; 422 | } 423 | 424 | td, 425 | th { 426 | padding: 0; 427 | } 428 | -------------------------------------------------------------------------------- /src/utils/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 | 11 | 12 | 13 | ); 14 | -------------------------------------------------------------------------------- /webpack.config.dev.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var webpack = require('webpack'); 3 | // var ExtractTextPlugin = require('extract-text-webpack-plugin'); 4 | 5 | var ROOT_PATH = path.resolve(__dirname); 6 | 7 | module.exports = { 8 | devtool: 'source-map', 9 | entry: { 10 | bundle: [ 11 | 'eventsource-polyfill', // necessary for hot reloading with IE 12 | 'webpack-hot-middleware/client', 13 | './src/index' 14 | ] 15 | }, 16 | resolve : { 17 | extensions : ['', '.js', '.jsx'], 18 | alias: { 19 | 'containers' : path.resolve(ROOT_PATH, './src/containers'), 20 | 'components' : path.resolve(ROOT_PATH, './src/components'), 21 | 'redux-base' : path.resolve(ROOT_PATH, './src/redux-base'), 22 | 'utils' : path.resolve(ROOT_PATH, './src/utils'), 23 | 'config' : path.resolve(ROOT_PATH, './src/config') 24 | } 25 | }, 26 | output: { 27 | path: path.join(__dirname, 'dist'), 28 | //"entry" keys will be a bundle names 29 | filename: '[name].js', 30 | publicPath: '/' 31 | }, 32 | plugins: [ 33 | new webpack.HotModuleReplacementPlugin(), 34 | new webpack.NoErrorsPlugin(), 35 | // new ExtractTextPlugin('../dist/styles.css') 36 | ], 37 | module: { 38 | preLoaders: [ 39 | { 40 | test: /\.(js|jsx)$/, 41 | loaders: ['eslint'], 42 | include: path.resolve(ROOT_PATH, 'src'), 43 | exclude: /node_modules/ 44 | } 45 | ], 46 | loaders: [ 47 | { 48 | test: /\.(js|jsx)$/, 49 | loaders: ['babel'], 50 | include: path.resolve(ROOT_PATH, 'src'), 51 | exclude: /node_modules/ 52 | }, 53 | { 54 | test: /\.scss$/, 55 | loader: 'style!css?modules&importLoaders=2&sourceMap&localIdentName=[local]___[hash:base64:5]!autoprefixer?browsers=last 2 version!sass?outputStyle=expanded&sourceMap' 56 | // The query parameter modules enables the CSS Modules spec. (css-loader?modules) 57 | // https://github.com/css-modules/css-modules 58 | // This enables Local scoped CSS by default. (You can switch it off with :global(...) or :global for selectors and/or rules.) 59 | // https://github.com/css-modules/css-modules 60 | 61 | // Used without Extract text plugin for reloading purpose 62 | // now index.html has "styles.css" entry which is empty in dev mode. But it's ok) 63 | // loader: ExtractTextPlugin.extract('style', 'style!css?modules&importLoaders=2&sourceMap&localIdentName=[local]___[hash:base64:5]!autoprefixer?browsers=last 2 version!sass?outputStyle=expanded&sourceMap') 64 | } 65 | ] 66 | } 67 | }; 68 | -------------------------------------------------------------------------------- /webpack.config.prod.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var webpack = require('webpack'); 3 | var ExtractTextPlugin = require('extract-text-webpack-plugin'); 4 | 5 | var ROOT_PATH = path.resolve(__dirname); 6 | 7 | module.exports = { 8 | devtool: 'eval', 9 | entry: [ 10 | './src/index.js', 11 | './src/index.html' 12 | ], 13 | resolve : { 14 | extensions : ['', '.js', '.jsx'], 15 | alias: { 16 | 'containers' : path.resolve(ROOT_PATH, './src/containers'), 17 | 'components' : path.resolve(ROOT_PATH, './src/components'), 18 | 'redux-base' : path.resolve(ROOT_PATH, './src/redux-base'), 19 | 'utils' : path.resolve(ROOT_PATH, './src/utils'), 20 | 'config' : path.resolve(ROOT_PATH, './src/config') 21 | } 22 | }, 23 | output: { 24 | path: path.join(__dirname, 'dist'), 25 | filename: 'bundle.js', 26 | publicPath: '/' 27 | }, 28 | plugins: [ 29 | // writes to "styles.css" file 30 | new ExtractTextPlugin('styles.css'), 31 | new webpack.optimize.OccurenceOrderPlugin(), 32 | new webpack.DefinePlugin({ 33 | 'process.env': { 34 | 'NODE_ENV': JSON.stringify('production') 35 | } 36 | }), 37 | new webpack.optimize.UglifyJsPlugin({ 38 | compressor: { 39 | warnings: false 40 | } 41 | }) 42 | ], 43 | module: { 44 | preLoaders: [ 45 | { 46 | test: /\.(js|jsx)$/, 47 | loaders: ['eslint'], 48 | include: path.resolve(ROOT_PATH, 'src') 49 | } 50 | ], 51 | loaders: [ 52 | { 53 | test: /\.(js|jsx)$/, 54 | loaders: ['babel'], 55 | include: path.resolve(ROOT_PATH, 'src') 56 | }, 57 | { 58 | test: /\.scss$/, 59 | loader: ExtractTextPlugin.extract('style', 'css?modules&importLoaders=2&sourceMap!autoprefixer?browsers=last 2 version!sass?outputStyle=expanded&sourceMap=true&sourceMapContents=true') 60 | }, 61 | { 62 | test: /\.html$/, 63 | loader: "file?name=[name].[ext]", 64 | } 65 | ] 66 | } 67 | }; 68 | --------------------------------------------------------------------------------