├── .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 |
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 | Increment if odd
28 | {' '}
29 | incrementAsync()}>Increment async
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 | +100
37 | -100
38 | HELLO action
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 |
18 |
55 |
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 |
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 |
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 |
40 |
Label
41 |
Something else
42 |
43 |
44 |
45 |
Label
46 |
Something else
47 |
48 |
49 |
50 |
Label
51 |
Something else
52 |
53 |
54 |
55 |
Label
56 |
Something else
57 |
58 |
59 |
60 |
Movies
61 |
62 |
63 | {' '} Reload movies
64 |
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 | Header
89 | Header
90 | Header
91 |
92 |
93 |
94 | {
95 | movies.map((movie) =>
96 |
97 | {movie.id}
98 | {movie.title}
99 | {movie.sprocketCount}
100 | {movie.owner}
101 | )
102 | }
103 |
104 |
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 |
--------------------------------------------------------------------------------