├── .nvmrc
├── src
├── containers
│ ├── MainLayout
│ │ ├── styles.css
│ │ └── index.js
│ ├── root.prod.js
│ ├── AuthLayout
│ │ ├── styles.css
│ │ └── index.js
│ ├── NotFound
│ │ ├── styles.css
│ │ └── index.js
│ ├── root.js
│ ├── Home
│ │ ├── styles.css
│ │ └── index.js
│ ├── Login
│ │ ├── styles.css
│ │ └── index.js
│ ├── root.dev.js
│ ├── DevTools.js
│ ├── ProtectedRoute.js
│ ├── routes.js
│ └── ForgotPassword
│ │ └── index.js
├── global.css
├── components
│ ├── TextInput
│ │ ├── styles.css
│ │ ├── index.js
│ │ └── test
│ │ │ └── index-test.js
│ ├── PrimaryTextInput
│ │ ├── styles.css
│ │ ├── index.js
│ │ └── test
│ │ │ └── index-test.js
│ ├── InlineMessage
│ │ ├── styles.css
│ │ ├── index.js
│ │ └── test
│ │ │ └── index-test.js
│ ├── Button
│ │ ├── styles.css
│ │ ├── index.js
│ │ └── test
│ │ │ └── index-test.js
│ ├── InlineLink
│ │ ├── styles.css
│ │ ├── test
│ │ │ └── index-test.js
│ │ └── index.js
│ └── PrimaryButton
│ │ ├── styles.css
│ │ ├── index.js
│ │ └── test
│ │ └── index-test.js
├── store
│ ├── index.js
│ ├── configureStore.prod.js
│ └── configureStore.dev.js
├── colors.js
├── index.html
├── reducers
│ ├── counter.js
│ ├── index.js
│ ├── test
│ │ ├── counter_spec.js
│ │ └── auth_spec.js
│ └── auth.js
├── index.js
├── utils
│ ├── index.js
│ └── ApiClient.js
└── actions
│ └── auth.js
├── .gitignore
├── server.js
├── .travis.yml
├── tests.webpack.js
├── .eslintrc
├── server.prod.js
├── .babelrc
├── server.dev.js
├── README.md
├── webpack.config.js
├── karma.config.js
└── package.json
/.nvmrc:
--------------------------------------------------------------------------------
1 | 4
2 |
--------------------------------------------------------------------------------
/src/containers/MainLayout/styles.css:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/global.css:
--------------------------------------------------------------------------------
1 | :global(body) {
2 | font-family: helvetica;
3 | }
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | dist
2 | node_modules
3 | npm-debug.log
4 | .DS_Store
5 | *.orig
--------------------------------------------------------------------------------
/src/containers/root.prod.js:
--------------------------------------------------------------------------------
1 | import Routes from './routes';
2 |
3 | export default Routes;
4 |
--------------------------------------------------------------------------------
/src/containers/AuthLayout/styles.css:
--------------------------------------------------------------------------------
1 | .login {
2 | text-align: center;
3 | margin-top: 100px;
4 | }
5 |
6 | .content {
7 | margin-top: 50px;
8 | }
--------------------------------------------------------------------------------
/src/containers/NotFound/styles.css:
--------------------------------------------------------------------------------
1 | .container {
2 | padding: 10px;
3 | }
4 |
5 | .container h1 {
6 | color: $dainTree;
7 | font-weight: bold;
8 | }
--------------------------------------------------------------------------------
/server.js:
--------------------------------------------------------------------------------
1 | require('babel-core/register');
2 | if (process.env.NODE_ENV === 'production') {
3 | require('./server.prod');
4 | } else {
5 | require('./server.dev');
6 | }
7 |
--------------------------------------------------------------------------------
/src/containers/root.js:
--------------------------------------------------------------------------------
1 | if (process.env.NODE_ENV === 'production') {
2 | module.exports = require('./root.prod');
3 | } else {
4 | module.exports = require('./root.dev');
5 | }
6 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - '4.2'
4 | before_install:
5 | - npm i -g npm@^3.0.0
6 | script:
7 | - npm run lint
8 | - npm run test
9 | - npm run build
10 |
--------------------------------------------------------------------------------
/src/components/TextInput/styles.css:
--------------------------------------------------------------------------------
1 | .input {
2 | color: $white;
3 | padding: 18px 20px;
4 | border-radius: 8px;
5 | border: none;
6 | &:placeholder {
7 | color: $white;
8 | }
9 | }
--------------------------------------------------------------------------------
/src/store/index.js:
--------------------------------------------------------------------------------
1 | if (process.env.NODE_ENV === 'production') {
2 | module.exports = require('./configureStore.prod');
3 | } else {
4 | module.exports = require('./configureStore.dev');
5 | }
6 |
--------------------------------------------------------------------------------
/tests.webpack.js:
--------------------------------------------------------------------------------
1 | require('babel-polyfill');
2 | var context = require.context('./src', true, /-test\.js$/); // make sure you have your directory and regex test set correctly!
3 | context.keys().forEach(context);
--------------------------------------------------------------------------------
/src/colors.js:
--------------------------------------------------------------------------------
1 | const white = '#FFF';
2 | const rosso = '#D80B09';
3 | const mercury = '#E5E5E5';
4 | const dainTree = '#02202A';
5 | const wasabi = '#77971E';
6 |
7 | module.exports = {
8 | white,
9 | rosso,
10 | mercury,
11 | dainTree,
12 | wasabi
13 | };
14 |
--------------------------------------------------------------------------------
/src/containers/Home/styles.css:
--------------------------------------------------------------------------------
1 | .container {
2 | padding: 10px;
3 | width: 100%;
4 | }
5 |
6 | .container h1 {
7 | color: $dainTree;
8 | font-weight: bold;
9 | text-align: left;
10 | }
11 |
12 | .search-wrapper {
13 | width: 80%;
14 | margin: 30px auto;
15 | }
--------------------------------------------------------------------------------
/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Redux React Boilerplate
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/src/components/PrimaryTextInput/styles.css:
--------------------------------------------------------------------------------
1 | .input {
2 | color: $dainTree;
3 | background: $mercury;
4 | padding: 18px 20px;
5 | border-radius: 4px;
6 | border: none;
7 | display: block;
8 | margin: 10px auto;
9 | width: 75%;
10 | &:placeholder {
11 | color: $dainTree;
12 | }
13 | }
--------------------------------------------------------------------------------
/src/reducers/counter.js:
--------------------------------------------------------------------------------
1 | import {createReducer} from '../utils';
2 |
3 | const initialState = {count: 0};
4 |
5 | export default createReducer(initialState, {
6 | ['INCREMENT']: (state) => ({
7 | count: state.count + 1
8 | }),
9 | ['DECREMENT']: (state) => ({
10 | count: state.count - 1
11 | })
12 | });
13 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | /**
2 | * 0 = disabled, 1 = warn, 2 = error
3 | * For info on specific rules, see: http://eslint.org/docs/rules/
4 | */
5 | {
6 | "extends": "pebblecode",
7 | "rules": {
8 | "no-use-before-define": 0,
9 | "new-cap": 0 //We don't like this rule, Immutable and React in general has caps
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/src/components/InlineMessage/styles.css:
--------------------------------------------------------------------------------
1 | .error, .success {
2 | font-size: 10pt;
3 | display: block;
4 | text-align: center;
5 | width: 100%;
6 | padding: 4px;
7 | }
8 |
9 | .error span, .success span {
10 | width: 100%;
11 | }
12 |
13 | .error {
14 | color: $rosso;
15 | }
16 |
17 | .success {
18 | color: $wasabi;
19 | }
--------------------------------------------------------------------------------
/src/containers/Login/styles.css:
--------------------------------------------------------------------------------
1 | .container {
2 | margin: 0 auto;
3 | }
4 |
5 | .form {
6 | width: 400px;
7 | margin: 0 auto;
8 | }
9 |
10 | .form input {
11 | width: 360px;
12 | }
13 |
14 | .actions {
15 | margin-top: 30px;
16 | }
17 |
18 | .actions button {
19 | float: right;
20 | }
21 |
22 | .actions a {
23 | float: left;
24 | }
--------------------------------------------------------------------------------
/src/components/Button/styles.css:
--------------------------------------------------------------------------------
1 | .shrink {
2 | transition:all 0.5s ease-out;
3 | }
4 |
5 | .shrink:hover {
6 | transform:scale(0.9);
7 | opacity: 0.8;
8 | }
9 |
10 | .grow {
11 | transition:all 0.5s ease-out;
12 | }
13 |
14 | .grow:hover {
15 | transform:scale(1.1);
16 | opacity: 0.8;
17 | }
18 |
19 | .button {
20 | background: $mercury;
21 | }
--------------------------------------------------------------------------------
/src/containers/root.dev.js:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react';
2 | import DevTools from './DevTools';
3 | import Routes from './routes';
4 |
5 | class Root extends Component {
6 | render() {
7 | return (
8 |
9 |
10 |
11 |
12 | );
13 | }
14 | }
15 |
16 | export default Root;
17 |
--------------------------------------------------------------------------------
/src/components/TextInput/index.js:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react';
2 | import CssModules from 'react-css-modules';
3 |
4 | import styles from './styles.css';
5 |
6 | class TextInput extends Component {
7 | render() {
8 | return ;
9 | }
10 | }
11 |
12 | export default CssModules(TextInput, styles);
13 |
--------------------------------------------------------------------------------
/src/reducers/index.js:
--------------------------------------------------------------------------------
1 | import {combineReducers} from 'redux';
2 | import {routeReducer} from 'react-router-redux';
3 | import {reducer as formReducer} from 'redux-form';
4 |
5 | import counter from './counter';
6 | import auth from './auth';
7 |
8 | export default combineReducers({
9 | auth,
10 | counter,
11 | routing: routeReducer,
12 | form: formReducer
13 | });
14 |
--------------------------------------------------------------------------------
/src/components/PrimaryTextInput/index.js:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react';
2 | import TextInput from '../TextInput';
3 | import CssModules from 'react-css-modules';
4 |
5 | import styles from './styles.css';
6 |
7 | class PrimaryInput extends Component {
8 | render() {
9 | return ;
10 | }
11 | }
12 |
13 | export default CssModules(PrimaryInput, styles);
14 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import 'babel-polyfill';
2 | import React from 'react';
3 | import { Provider } from 'react-redux';
4 | import { render } from 'react-dom';
5 | import Root from './containers/root';
6 | import configureStore from './store';
7 |
8 | const store = configureStore();
9 | import './global.css';
10 |
11 | render(
12 |
13 | , document.getElementById('root'));
14 |
--------------------------------------------------------------------------------
/src/components/InlineLink/styles.css:
--------------------------------------------------------------------------------
1 | .link, .default{
2 | font-size: 9pt;
3 | cursor: pointer;
4 | margin: {top: 10px; bottom: 10px}
5 | cursor: pointer;
6 | }
7 |
8 | .default{
9 | color: $dainTree;
10 | }
11 |
12 | .white {
13 | color: $white;
14 | }
15 |
16 | .default:link, .white:link {
17 | text-decoration: none;
18 | }
19 |
20 | .default:hover, .white:hover {
21 | text-decoration: underline;
22 | }
23 |
24 |
--------------------------------------------------------------------------------
/src/containers/DevTools.js:
--------------------------------------------------------------------------------
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 |
10 |
11 |
12 | );
13 |
--------------------------------------------------------------------------------
/src/utils/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Utility method to create a reducer from a map,
3 | * with initial state
4 | * @param initialState
5 | * @param reducerMap
6 | * @returns {Function}
7 | */
8 | export function createReducer(initialState, reducerMap) {
9 | return (state = initialState, action) => {
10 | const reducer = reducerMap[action.type];
11 | return reducer
12 | ? reducer(state, action.payload)
13 | : state;
14 | };
15 | }
16 |
--------------------------------------------------------------------------------
/src/components/PrimaryButton/styles.css:
--------------------------------------------------------------------------------
1 | .button {
2 | text-transform: capitalize;
3 | color: $white;
4 | background: $dainTree;
5 | padding: 8px 15px;
6 | font-size: 10pt;
7 | border-radius: 3px;
8 | border: none;
9 | cursor: pointer;
10 | composes: grow from '../Button/styles.css';
11 | }
12 |
13 | .button span {
14 | padding: 5px;
15 | }
16 |
17 | .button img {
18 | padding-top: 5px;
19 | }
20 |
21 | .icon, .button span {
22 | float: left;
23 | }
24 |
--------------------------------------------------------------------------------
/src/containers/NotFound/index.js:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react';
2 |
3 | import styles from './styles.css';
4 |
5 | class NotFound extends Component {
6 |
7 | render() {
8 | return (
9 |
10 |
11 |
404 Page Not Found
12 |
13 |
14 | );
15 | }
16 | }
17 |
18 | NotFound.propTypes = {
19 | children: PropTypes.node
20 | };
21 |
22 | export default NotFound;
23 |
--------------------------------------------------------------------------------
/src/containers/AuthLayout/index.js:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react';
2 | import CssModules from 'react-css-modules';
3 |
4 | import styles from './styles.css';
5 | class AuthLayout extends Component {
6 | render() {
7 | return (
8 |
9 | {this.props.children}
10 |
11 |
);
12 | }
13 | }
14 |
15 | AuthLayout.propTypes = {
16 | children: React.PropTypes.node
17 | };
18 |
19 | export default CssModules(AuthLayout, styles);
20 |
--------------------------------------------------------------------------------
/src/components/Button/index.js:
--------------------------------------------------------------------------------
1 | import React, {Component, PropTypes} from 'react';
2 | import CssModules from 'react-css-modules';
3 |
4 | import styles from './styles.css';
5 |
6 | class Button extends Component {
7 | render() {
8 | const {children} = this.props;
9 |
10 | return ();
11 | }
12 | }
13 |
14 | Button.propTypes = {
15 | children: PropTypes.string.isRequired
16 | };
17 |
18 | export default CssModules(Button, styles);
19 |
--------------------------------------------------------------------------------
/src/components/InlineMessage/index.js:
--------------------------------------------------------------------------------
1 | import React, {Component, PropTypes} from 'react';
2 | import CssModules from 'react-css-modules';
3 |
4 | import styles from './styles.css';
5 |
6 | class InlineErrorMessage extends Component {
7 | render() {
8 | const {type, message} = this.props;
9 | return {message} ;
10 | }
11 | }
12 |
13 | InlineErrorMessage.propTypes = {
14 | message: PropTypes.string.isRequired,
15 | type: PropTypes.string.isRequired
16 | };
17 |
18 | export default CssModules(InlineErrorMessage, styles);
19 |
--------------------------------------------------------------------------------
/src/components/PrimaryButton/index.js:
--------------------------------------------------------------------------------
1 | import React, {Component, PropTypes} from 'react';
2 | import Button from '../Button';
3 | import CssModules from 'react-css-modules';
4 |
5 | import styles from './styles.css';
6 |
7 | class PrimaryButton extends Component {
8 | render() {
9 | const {children} = this.props;
10 |
11 | return ();
12 | }
13 | }
14 |
15 | PrimaryButton.propTypes = {
16 | children: PropTypes.string.isRequired,
17 | loading: PropTypes.bool
18 | };
19 |
20 | export default CssModules(PrimaryButton, styles);
21 |
--------------------------------------------------------------------------------
/server.prod.js:
--------------------------------------------------------------------------------
1 | /* eslint no-console:0 */
2 | import express from 'express';
3 | import path from 'path';
4 |
5 | const app = express();
6 |
7 | const port = process.env.PORT || 3000;
8 |
9 | app.use(express.static(path.join(__dirname, 'dist')));
10 |
11 | app.get('*', (request, response) => {
12 | response.sendFile(path.join(__dirname, 'dist/index.html'));
13 | });
14 |
15 | app.listen(port, (error) => {
16 | if (error) {
17 | console.error(error);
18 | process.exit(1);
19 | }
20 |
21 | console.log('Production Server');
22 | return console.info(`🌎 Listening on port ${port}`);
23 | });
24 |
--------------------------------------------------------------------------------
/src/utils/ApiClient.js:
--------------------------------------------------------------------------------
1 |
2 | export const login = (username, password) => { // eslint-disable-line
3 |
4 | return new Promise((resolve) => {
5 | setTimeout(() => {
6 | resolve({token: '123', username});
7 | }, 100);
8 | });
9 | };
10 |
11 | export const forgotPassword = (username) => { // eslint-disable-line
12 |
13 | return new Promise((resolve) => {
14 | setTimeout(() => {
15 | resolve();
16 | }, 100);
17 | });
18 | };
19 |
20 | export const logout = () => {
21 | return new Promise((resolve) => {
22 | setTimeout(() => {
23 | resolve();
24 | }, 100);
25 | });
26 | };
27 |
--------------------------------------------------------------------------------
/src/reducers/test/counter_spec.js:
--------------------------------------------------------------------------------
1 | import {expect} from 'chai';
2 | import reducer from '../counter';
3 |
4 | describe('Counter Reducer', () => {
5 | it('Should handle INCREMENT', () => {
6 | const initialState = {count: 0};
7 |
8 | const newState = reducer(initialState, {type: 'INCREMENT'});
9 |
10 | expect(newState).to.eql({
11 | count: 1
12 | });
13 | });
14 |
15 | it('Should handle DECREMENT', () => {
16 | const initialState = {count: 1};
17 |
18 | const newState = reducer(initialState, {type: 'DECREMENT'});
19 |
20 | expect(newState).to.eql({
21 | count: 0
22 | });
23 | });
24 | });
25 |
--------------------------------------------------------------------------------
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["es2015", "stage-0", "react"],
3 | "env": {
4 | // only enable it when process.env.NODE_ENV is 'development' or undefined
5 | "development": {
6 | "plugins": [["react-transform", {
7 | "transforms": [{
8 | "transform": "react-transform-hmr",
9 | // if you use React Native, pass "react-native" instead:
10 | "imports": ["react"],
11 | // this is important for Webpack HMR:
12 | "locals": ["module"]
13 | }]
14 | // note: you can put more transforms into array
15 | // this is just one of them!
16 | }]]
17 | }
18 | }
19 | }
--------------------------------------------------------------------------------
/src/components/TextInput/test/index-test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {shallow} from 'enzyme';
3 | import {expect} from 'chai';
4 | import sinon from 'sinon';
5 |
6 | import PrimaryTextInput from '../';
7 | // related: https://github.com/airbnb/enzyme/issues/76
8 |
9 | describe('', () => {
10 | it('Should handleChange', () =>{
11 | const handleChange = sinon.spy();
12 | const wrapper = shallow();
13 |
14 | wrapper.find('input').simulate('change', {target: {value: 'My new value'}});
15 |
16 | expect(handleChange.calledOnce).to.equal(true);
17 | });
18 | });
19 |
--------------------------------------------------------------------------------
/src/components/Button/test/index-test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {shallow} from 'enzyme';
3 | import {expect} from 'chai';
4 | import sinon from 'sinon';
5 |
6 | import Button from '../';
7 |
8 | describe('', () => {
9 | it('Should handleClick', () =>{
10 | const handleClick = sinon.spy();
11 | const wrapper = shallow();
12 |
13 | wrapper.find('button').simulate('click', {preventDefault: () => {}});
14 |
15 | expect(handleClick.calledOnce).to.equal(true);
16 | });
17 |
18 | it('Should render inner text', () => {
19 | const wrapper = shallow();
20 | expect(wrapper.text()).to.contain('hi');
21 | });
22 | });
23 |
--------------------------------------------------------------------------------
/src/components/PrimaryTextInput/test/index-test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {shallow} from 'enzyme';
3 | import {expect} from 'chai';
4 | import sinon from 'sinon';
5 |
6 | import PrimaryTextInput from '../';
7 | import TextInput from '../../TextInput';
8 | // related: https://github.com/airbnb/enzyme/issues/76
9 |
10 | describe('', () => {
11 | it('Should handleChange', () =>{
12 | const handleChange = sinon.spy();
13 | const wrapper = shallow();
14 |
15 | wrapper.find(TextInput).simulate('change', {target: {value: 'My new value'}});
16 |
17 | expect(handleChange.calledOnce).to.equal(true);
18 | });
19 | });
20 |
--------------------------------------------------------------------------------
/src/components/InlineLink/test/index-test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {shallow} from 'enzyme';
3 | import {expect} from 'chai';
4 | import sinon from 'sinon';
5 |
6 | import InlineLink from '../';
7 |
8 | describe('', () => {
9 | it('Should handleClick', () =>{
10 | const handleClick = sinon.spy();
11 | const wrapper = shallow( hi! );
12 |
13 | wrapper.find('a').simulate('click', {preventDefault: () => {}});
14 |
15 | expect(handleClick.calledOnce).to.equal(true);
16 | });
17 |
18 | it('Should render inner text', () => {
19 | const wrapper = shallow( hi );
20 | expect(wrapper.text()).to.contain('hi');
21 | });
22 | });
23 |
--------------------------------------------------------------------------------
/src/components/PrimaryButton/test/index-test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {shallow} from 'enzyme';
3 | import {expect} from 'chai';
4 | import sinon from 'sinon';
5 |
6 | import PrimaryButton from '../';
7 | import Button from '../../Button';
8 |
9 | describe('', () => {
10 | it('Should handleClick', () =>{
11 | const handleClick = sinon.spy();
12 | const wrapper = shallow( hi! );
13 |
14 | wrapper.find(Button).simulate('click', {preventDefault: () => {}});
15 |
16 | expect(handleClick.calledOnce).to.equal(true);
17 | });
18 |
19 | it('Should render inner text', () => {
20 | const wrapper = shallow();
21 | expect(wrapper.text()).to.contain('hi');
22 | });
23 | });
24 |
--------------------------------------------------------------------------------
/src/containers/Home/index.js:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react';
2 | import { connect } from 'react-redux';
3 | import CssModules from 'react-css-modules';
4 |
5 | import styles from './styles.css';
6 |
7 | class Home extends Component {
8 |
9 | render() {
10 | const {auth: {username}} = this.props;
11 |
12 | return (
13 |
14 |
15 |
Hi {username}
16 |
17 |
18 | );
19 | }
20 | }
21 |
22 | Home.propTypes = {
23 | children: PropTypes.node,
24 | auth: PropTypes.shape({
25 | username: PropTypes.string
26 | })
27 | };
28 |
29 | const mapStateToProps = ({auth}) => ({
30 | auth
31 | });
32 |
33 | export default connect(mapStateToProps)(CssModules(Home, styles));
34 |
--------------------------------------------------------------------------------
/src/components/InlineLink/index.js:
--------------------------------------------------------------------------------
1 | import React, {Component, PropTypes} from 'react';
2 | import CssModules from 'react-css-modules';
3 |
4 | import styles from './styles.css';
5 |
6 | class InlineLink extends Component {
7 | _handleClick(e) {
8 | e.preventDefault();
9 | this.props.onClick();
10 | }
11 |
12 | render() {
13 | const {color} = this.props;
14 | return {this.props.children} ;
15 | }
16 | }
17 |
18 | InlineLink.propTypes = {
19 | children: PropTypes.string.isRequired,
20 | color: PropTypes.string,
21 | onClick: PropTypes.func
22 | };
23 |
24 | InlineLink.defaultProps = {
25 | onClick: () => {},
26 | color: 'default'
27 | };
28 |
29 | export default CssModules(InlineLink, styles);
30 |
--------------------------------------------------------------------------------
/src/components/InlineMessage/test/index-test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {shallow} from 'enzyme';
3 | import {expect} from 'chai';
4 |
5 | import InlineLink from '../';
6 |
7 | describe('', () => {
8 | it('Should render success inner text', () => {
9 | const wrapper = shallow();
10 | expect(wrapper.text()).to.contain('hi');
11 | });
12 |
13 | it('Should render error inner text', () => {
14 | const wrapper = shallow();
15 | expect(wrapper.text()).to.contain('hi');
16 | });
17 |
18 | it('Should render success inner text', () => {
19 | const wrapper = shallow();
20 | expect(wrapper.text()).to.contain('hi');
21 | });
22 | });
23 |
--------------------------------------------------------------------------------
/server.dev.js:
--------------------------------------------------------------------------------
1 | /* eslint no-console:0 */
2 | import path from 'path';
3 | import express from 'express';
4 | import webpack from 'webpack';
5 | import config from './webpack.config';
6 | import opn from 'opn';
7 | const app = express();
8 | const compiler = webpack(config);
9 | const port = 3000;
10 |
11 | let opened = false;
12 | app.use(require('webpack-dev-middleware')(compiler, {
13 | noInfo: true,
14 | publicPath: config.output.publicPath
15 | }));
16 |
17 | app.use(require('webpack-hot-middleware')(compiler));
18 |
19 | app.get('*', (req, res) => {
20 | res.sendFile(path.join(__dirname, 'src/index.html'));
21 | });
22 |
23 | app.listen(port, (err) => {
24 | if (err) {
25 | console.log(err);
26 | return;
27 | }
28 |
29 | if (!opened) {
30 | opn(`http://localhost:${port}`);
31 | opened = true;
32 | }
33 | console.log('Dev Server');
34 |
35 | console.log(`🌎 Listening on port ${port}`);
36 | });
37 |
--------------------------------------------------------------------------------
/src/store/configureStore.prod.js:
--------------------------------------------------------------------------------
1 | import { createStore, applyMiddleware, compose } from 'redux';
2 | import { syncHistory } from 'react-router-redux';
3 | import thunk from 'redux-thunk';
4 | import {hashHistory} from 'react-router';
5 | import createLogger from 'redux-logger';
6 |
7 | import rootReducer from '../reducers';
8 | /**
9 | * Method to create stores based on a set of passed
10 | * reducers
11 | * @param initialState
12 | * @returns {*}
13 | */
14 | export default function configureStore(initialState) {
15 |
16 | const logger = createLogger();
17 | const reduxRouterMiddleware = syncHistory(hashHistory);
18 | const middleware = applyMiddleware(thunk, logger, reduxRouterMiddleware);
19 |
20 | const createStoreWithMiddleware = compose(
21 | middleware
22 | );
23 |
24 | const store = createStoreWithMiddleware(createStore)(rootReducer, initialState);
25 |
26 | reduxRouterMiddleware.listenForReplays(store);
27 |
28 | return store;
29 | }
30 |
--------------------------------------------------------------------------------
/src/containers/ProtectedRoute.js:
--------------------------------------------------------------------------------
1 | import React, { PropTypes } from 'react';
2 | import { routeActions } from 'react-router-redux';
3 | import { connect } from 'react-redux';
4 |
5 | const mapStateToProps = ({auth}) => {
6 | return {
7 | auth
8 | };
9 | };
10 |
11 | export default (Component) => {
12 | class ProtectedRoute extends React.Component {
13 | componentWillMount() {
14 | const {auth, dispatch} = this.props;
15 |
16 | if (!auth.token) {
17 | const redirectAfterLogin = this.props.location.pathname;
18 | dispatch(routeActions.push(`/login?next=${redirectAfterLogin}`));
19 | }
20 | }
21 |
22 | render() {
23 | return ();
24 | }
25 |
26 | }
27 |
28 | ProtectedRoute.propTypes = {
29 | dispatch: PropTypes.func.isRequired,
30 | auth: PropTypes.object.isRequired,
31 | location: PropTypes.shape({
32 | pathname: PropTypes.string.isRequired
33 | })
34 | };
35 |
36 | return connect(mapStateToProps)(ProtectedRoute);
37 | };
38 |
--------------------------------------------------------------------------------
/src/containers/MainLayout/index.js:
--------------------------------------------------------------------------------
1 | import React, {Component, PropTypes} from 'react';
2 | import {connect} from 'react-redux';
3 | import {bindActionCreators} from 'redux';
4 |
5 | import * as actionCreators from '../../actions/auth';
6 |
7 | class MainLayout extends Component {
8 | render() {
9 | const {auth: {username}, actions: {logout}} = this.props;
10 | return ();
16 | }
17 | }
18 |
19 | MainLayout.propTypes = {
20 | children: PropTypes.node,
21 | auth: PropTypes.shape({
22 | username: PropTypes.string
23 | }),
24 | actions: PropTypes.shape({
25 | logout: PropTypes.func.isRequired
26 | })
27 | };
28 |
29 |
30 | const mapStateToProps = ({auth}) => ({auth});
31 |
32 | const mapDispatchToProps = (dispatch) => ({
33 | actions: bindActionCreators(actionCreators, dispatch)
34 | });
35 |
36 | export default connect(mapStateToProps, mapDispatchToProps)(MainLayout);
37 |
--------------------------------------------------------------------------------
/src/containers/routes.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { Router, Route, IndexRoute, hashHistory} from 'react-router';
3 |
4 | import ProtectedRoute from './ProtectedRoute';
5 | import AuthLayout from './AuthLayout';
6 | import MainLayout from './MainLayout';
7 | import Login from './Login';
8 | import ForgotPassword from './ForgotPassword';
9 | import Home from './Home';
10 | import NotFound from './NotFound';
11 |
12 | class Root extends Component {
13 | render() {
14 | return (
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 | );
27 | }
28 | }
29 |
30 | export default Root;
31 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # React Redux Boilerplate
2 | 
3 | 
4 | [](https://david-dm.org/export-mike/react-redux-boilerplate)
5 |
6 | A react, redux, webpack, css modules, postcss, karma and mocha boilerplate. Complete with super simple authentication flow, tests on reducers and components. Related to [blogpost](http://pebblecode.com/blog/react-redux-unit-testing/). Complete with redux dev tools. ctrl-h and ctrl-q shortcuts :)
7 |
8 | ## Installation
9 | #### npm3 and node v4+ required
10 | ``` git clone git@github.com:export-mike/react-redux-boilerplate.git```
11 |
12 | ```npm i ```
13 |
14 | ``` npm start ```
15 |
16 | new shell:
17 | ``` npm run test:watch ```
18 |
19 | ### other commands:
20 | test mocha only:
21 | ``` npm run mocha:watch ```
22 |
23 | test karma only:
24 | ``` npm run karma:watch ```
25 |
26 | for CI use ```npm test ```
27 |
28 | Build:
29 | ``` npm run build ```
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/src/store/configureStore.dev.js:
--------------------------------------------------------------------------------
1 | import { createStore, applyMiddleware, compose } from 'redux';
2 | import { syncHistory } from 'react-router-redux';
3 | import thunk from 'redux-thunk';
4 | import {hashHistory} from 'react-router';
5 | import { persistState } from 'redux-devtools';
6 |
7 | import rootReducer from '../reducers';
8 | import DevTools from '../containers/DevTools';
9 | /**
10 | * Method to create stores based on a set of passed
11 | * reducers
12 | * @param initialState
13 | * @returns {*}
14 | */
15 | export default function configureStore(initialState) {
16 |
17 | const reduxRouterMiddleware = syncHistory(hashHistory);
18 | const middleware = applyMiddleware(thunk, reduxRouterMiddleware);
19 |
20 | const createStoreWithMiddleware = compose(
21 | middleware,
22 | // Required! Enable Redux DevTools with the monitors you chose
23 | DevTools.instrument(),
24 | // Optional. Lets you write ?debug_session= in address bar to persist debug sessions
25 | persistState(getDebugSessionKey())
26 | );
27 |
28 | const store = createStoreWithMiddleware(createStore)(rootReducer, initialState);
29 |
30 | reduxRouterMiddleware.listenForReplays(store);
31 |
32 | if (module.hot) {
33 | module.hot.accept('../reducers', () => {
34 | const nextRootReducer = require('../reducers').default;
35 | store.replaceReducer(nextRootReducer);
36 | });
37 | }
38 |
39 | return store;
40 | }
41 |
42 |
43 | function getDebugSessionKey() {
44 | // You can write custom logic here!
45 | // By default we try to read the key from ?debug_session= in the address bar
46 | const matches = window.location.href.match(/[?&]debug_session=([^&]+)\b/);
47 | return (matches && matches.length > 0) ? matches[1] : null;
48 | }
49 |
--------------------------------------------------------------------------------
/src/reducers/auth.js:
--------------------------------------------------------------------------------
1 | import {createReducer} from '../utils';
2 | import {
3 | AUTH, AUTH_SUCCESS, AUTH_FAIL,
4 | AUTH_LOGOUT, AUTH_LOGOUT_SUCCESS, AUTH_LOGOUT_FAIL,
5 | AUTH_FORGOT, AUTH_FORGOT_FAIL, AUTH_FORGOT_SUCCESS
6 | } from '../actions/auth';
7 |
8 | export default createReducer({
9 | token: null,
10 | username: '',
11 | tokenExpires: null,
12 | authFetch: false,
13 | authFailed: false,
14 | errorMessage: null,
15 | error: null,
16 | logoutFetch: false,
17 | forgot: {
18 | authForgetFetch: false,
19 | error: null,
20 | errorMessage: null,
21 | success: false
22 | }
23 | }, {
24 | [AUTH]: (state, {username}) => {
25 | return Object.assign({}, state, {
26 | authFetch: true,
27 | username
28 | });
29 | },
30 | [AUTH_SUCCESS]: (state, {token, expires}) => {
31 | return Object.assign({}, state, {
32 | token,
33 | tokenExpires: expires,
34 | authFetch: false
35 | });
36 | },
37 | [AUTH_FAIL]: (state, {ErrorMessage, error}) => {
38 | return Object.assign({}, state, {
39 | errorMessage: ErrorMessage, // ensure it works this is the format of the phone app api.
40 | error,
41 | authFetch: false
42 | });
43 | },
44 | [AUTH_LOGOUT]: (state) => {
45 | return Object.assign({}, state, {
46 | logoutFetch: true
47 | });
48 | },
49 | [AUTH_LOGOUT_SUCCESS]: (state) => {
50 | return Object.assign({}, state, {
51 | logoutFetch: false,
52 | token: null,
53 | username: null
54 | });
55 | },
56 | [AUTH_LOGOUT_FAIL]: (state) => {
57 | return Object.assign({}, state, {
58 | logoutFetchFail: true
59 | });
60 | },
61 | [AUTH_FORGOT]: (state) => {
62 | return Object.assign({}, state, {
63 | forgot: {
64 | authForgetFetch: true,
65 | error: null,
66 | errorMessage: null,
67 | success: false
68 | }
69 | });
70 | },
71 | [AUTH_FORGOT_SUCCESS]: (state) => {
72 | return Object.assign({}, state, {
73 | forgot: {
74 | authForgetFetch: false,
75 | success: true,
76 | error: null,
77 | errorMessage: null
78 | }
79 | });
80 | },
81 | [AUTH_FORGOT_FAIL]: (state, {error, message}) => {
82 | return Object.assign({}, state, {
83 | forgot: {
84 | authForgetFetch: false,
85 | error,
86 | errorMessage: message,
87 | success: false
88 | }
89 | });
90 | }
91 | });
92 |
--------------------------------------------------------------------------------
/src/actions/auth.js:
--------------------------------------------------------------------------------
1 | import { routeActions } from 'react-router-redux';
2 | import * as Api from '../utils/ApiClient';
3 | export const AUTH = 'AUTH';
4 | export const AUTH_SUCCESS = 'AUTH_SUCCESS';
5 | export const AUTH_FAIL = 'AUTH_FAIL';
6 | export const AUTH_FORGOT = 'AAUTH_FORGOT';
7 | export const AUTH_FORGOT_FAIL = 'AUTH_FORGOT_FAIL';
8 | export const AUTH_FORGOT_SUCCESS = 'AUTH_FORGOT_SUCCESS';
9 | export const AUTH_LOGOUT = 'AUTH_LOGOUT';
10 | export const AUTH_LOGOUT_SUCCESS = 'AUTH_LOGOUT_SUCCESS';
11 | export const AUTH_LOGOUT_FAIL = 'AUTH_LOGOUT_FAIL';
12 |
13 | const auth = (username) => ({type: AUTH, payload: {username}});
14 |
15 | const loggedIn = (data) => ({
16 | type: AUTH_SUCCESS,
17 | payload: data
18 | });
19 |
20 | const failedLogin = (data) => ({
21 | type: AUTH_FAIL,
22 | payload: data
23 | });
24 |
25 |
26 | const _logout = () => ({ type: AUTH_LOGOUT });
27 |
28 | const _logoutFail = (err) => ({
29 | type: AUTH_LOGOUT_FAIL,
30 | payload: err
31 | });
32 |
33 | const _logoutSuccess = () => ({
34 | type: AUTH_LOGOUT_SUCCESS
35 | });
36 |
37 | const forgot = (data) => ({
38 | type: AUTH_FORGOT,
39 | payload: data
40 | });
41 |
42 | const failedForgotPassword = (data) => ({
43 | type: AUTH_FORGOT_FAIL,
44 | payload: data
45 | });
46 |
47 | const successForgotPassword = (data) => ({
48 | type: AUTH_FORGOT_SUCCESS,
49 | payload: data
50 | });
51 |
52 | export const login = ({username, password}) => {
53 |
54 | return (dispatch) => {
55 | dispatch(auth(username));
56 |
57 | Api.login(username, password)
58 | .then((data) => {
59 | dispatch(loggedIn(data));
60 | sessionStorage.setItem('token', data.token);
61 | dispatch(routeActions.push('/'));
62 | })
63 | .catch( (error) => dispatch(failedLogin(error)));
64 | };
65 |
66 | };
67 |
68 | export const logout = () => (dispatch) => {
69 | dispatch(_logout());
70 | Api.logout()
71 | .then(() => {
72 | dispatch(_logoutSuccess());
73 | dispatch(routeActions.replace('/login'));
74 | sessionStorage.removeItem('token');
75 | })
76 | .catch((err) => dispatch(_logoutFail(err)));
77 | };
78 |
79 | export const loginCheckToken = () => {
80 | const token = sessionStorage.getItem('token');
81 |
82 | return (dispatch) => {
83 | dispatch(loggedIn({token}));
84 | };
85 | };
86 |
87 | export const forgotPassword = ({username}) => {
88 | return (dispatch) => {
89 | dispatch(forgot());
90 |
91 | Api.forgotPassword(username)
92 | .then((data) => {
93 | dispatch(successForgotPassword(data));
94 | })
95 | .catch( error => dispatch(failedForgotPassword(error)));
96 | };
97 | };
98 |
--------------------------------------------------------------------------------
/src/containers/ForgotPassword/index.js:
--------------------------------------------------------------------------------
1 | import React, {Component, PropTypes} from 'react';
2 | import {connect} from 'react-redux';
3 | import {bindActionCreators} from 'redux';
4 | import {reduxForm} from 'redux-form';
5 |
6 | import * as actionCreators from '../../actions/auth';
7 | import PrimaryTextInput from '../../components/PrimaryTextInput';
8 | import PrimaryButton from '../../components/PrimaryButton';
9 | import InlineMessage from '../../components/InlineMessage';
10 |
11 | import styles from '../Login/styles.css';
12 |
13 | const mapStateToProps = ({auth}) => {
14 | return {
15 | auth
16 | };
17 | };
18 |
19 | const mapDispatchToProps = (dispatch) => ({
20 | actions: bindActionCreators(actionCreators, dispatch)
21 | });
22 |
23 | const validate = ({username}) => {
24 | const errors = {};
25 | if (!username) {
26 | errors.username = 'Required';
27 | } else if (username.length <= 6) {
28 | errors.username = 'Username must be 6 characters or more';
29 | }
30 |
31 | return errors;
32 | };
33 |
34 | class ForgotPassword extends Component {
35 |
36 | render() {
37 | const {
38 | fields: {
39 | username
40 | },
41 | auth: {forgot: {authForgetFetch, errorMessage, success}},
42 | handleSubmit,
43 | actions: {
44 | forgotPassword
45 | }
46 | } = this.props;
47 |
48 | return (
49 |
59 | );
60 | }
61 | }
62 |
63 | ForgotPassword.propTypes = {
64 | dispatch: PropTypes.func.isRequired,
65 | fields: PropTypes.shape({
66 | username: PropTypes.object
67 | }),
68 | handleSubmit: PropTypes.func,
69 | submitting: PropTypes.bool,
70 |
71 | // Our defined Props
72 | auth: PropTypes.shape({
73 | forgot: PropTypes.shape({
74 | authForgetFetch: PropTypes.bool.isRequired,
75 | errorMessage: PropTypes.string,
76 | success: PropTypes.bool.isRequired
77 | })
78 | }),
79 | actions: PropTypes.object
80 | };
81 |
82 |
83 | export default connect(mapStateToProps, mapDispatchToProps)(reduxForm({
84 | form: 'forgot-password',
85 | fields: ['username'],
86 | validate
87 | })(ForgotPassword));
88 |
--------------------------------------------------------------------------------
/src/containers/Login/index.js:
--------------------------------------------------------------------------------
1 | import React, {Component, PropTypes} from 'react';
2 | import {connect} from 'react-redux';
3 | import {bindActionCreators} from 'redux';
4 | import {routeActions} from 'react-router-redux';
5 | import {reduxForm} from 'redux-form';
6 | import CssModules from 'react-css-modules';
7 |
8 | import * as actionCreators from '../../actions/auth';
9 | import PrimaryTextInput from '../../components/PrimaryTextInput';
10 | import PrimaryButton from '../../components/PrimaryButton';
11 | import InlineMessage from '../../components/InlineMessage';
12 | import InlineLink from '../../components/InlineLink';
13 |
14 | import styles from './styles.css';
15 |
16 | const mapStateToProps = ({auth}) => ({auth});
17 |
18 | const mapDispatchToProps = (dispatch) => ({
19 | actions: bindActionCreators(actionCreators, dispatch)
20 | });
21 |
22 | const validate = ({username, password}) => {
23 | const errors = {};
24 | if (!username) {
25 | errors.username = 'Required';
26 | } else if (username.length <= 6) {
27 | errors.username = 'Username must be 6 characters or more';
28 | }
29 |
30 | if (!password) {
31 | errors.password = 'Required';
32 | } else if (password.length <= 8) {
33 | errors.password = 'password must be 8 characters or more';
34 | }
35 |
36 | return errors;
37 | };
38 |
39 | class Login extends Component {
40 |
41 | render() {
42 | const {
43 | fields: {
44 | username,
45 | password
46 | },
47 | auth: {authFetch},
48 | handleSubmit,
49 | dispatch,
50 | actions: {
51 | login
52 | }
53 | } = this.props;
54 |
55 | return (
56 |
68 | );
69 | }
70 | }
71 |
72 | Login.propTypes = {
73 | dispatch: PropTypes.func.isRequired,
74 | fields: PropTypes.shape({
75 | username: PropTypes.object,
76 | password: PropTypes.object
77 | }),
78 | handleSubmit: PropTypes.func,
79 | submitting: PropTypes.bool,
80 |
81 | // Our defined Props
82 | auth: PropTypes.shape({
83 | errorMessage: PropTypes.string,
84 | authFetch: PropTypes.bool.isRequired,
85 | authFailed: PropTypes.bool.isRequired
86 | }),
87 | actions: PropTypes.object
88 | };
89 |
90 |
91 | export default connect(mapStateToProps, mapDispatchToProps)(reduxForm({
92 | form: 'login',
93 | fields: ['username', 'password'],
94 | validate
95 | })(CssModules(Login, styles)));
96 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const webpack = require('webpack');
4 | const HtmlWebpackPlugin = require('html-webpack-plugin');
5 | const ExtractTextPlugin = require('extract-text-webpack-plugin');
6 | const DEV = process.env.NODE_ENV !== 'production';
7 |
8 | const config = {
9 | entry: ['./src/index.js'],
10 | debug: DEV,
11 | devtool: DEV ? 'source-map' : 'source-map',
12 | target: 'web',
13 | output: {
14 | path: __dirname + '/dist',
15 | publicPath: '/',
16 | filename: '[name].js'
17 | },
18 | module: {
19 | loaders: [{
20 | test: /\.jsx?$/,
21 | exclude: /(node_modules)/,
22 | loaders: ['babel']
23 | }, {
24 | test: /\.jpe?g$|\.gif$|\.png$|\.ico$/,
25 | loader: 'url-loader?name=[path][name].[ext]&context=./src'
26 | }, {
27 | test: /\.html/,
28 | loader: 'file?name=[name].[ext]'
29 | }, {
30 | test: /\.css$/,
31 | // or ?sourceMap&modules&importLoaders=1!postcss-loader
32 | loader: DEV ? 'style!css?modules&importLoaders=1&localIdentName=[name]__[local]___[hash:base64:5]!postcss-loader' : ExtractTextPlugin.extract('style', 'css?modules&importLoaders=1&localIdentName=[name]__[local]___[hash:base64:5]!postcss-loader')
33 | // 'style-loader!css-loader?modules&importLoaders=1!postcss-loader'
34 | },
35 | { test: /\.json/, loader: 'json'},
36 | {test: /\.eot(\?v=\d+\.\d+\.\d+)?$/, loader: 'url?mimetype=application/vnd.ms-fontobject'},
37 | {test: /\.woff(2)?(\?v=\d+\.\d+\.\d+)?$/, loader: 'url?limit=10000&mimetype=application/font-woff' },
38 | {test: /\.ttf(\?v=\d+\.\d+\.\d+)?$/, loader: 'url?limit=10000&mimetype=application/octet-stream' },
39 | {test: /.svg(\?v=\d+\.\d+\.\d+)?$|.svg$/, loader: 'file?name=[path][name].[ext]&context=./src&mimetype=image/svg+xml'}
40 | ]
41 | },
42 | plugins: [
43 | // Output our index.html and inject the script tag
44 | new HtmlWebpackPlugin({
45 | template: './src/index.html',
46 | inject: 'body'
47 | }),
48 | // Without this, Webpack would output styles inside JS - we prefer a separate CSS file
49 | new ExtractTextPlugin('styles.css'),
50 |
51 | new webpack.DefinePlugin({
52 | 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV)
53 | })
54 | ],
55 | postcss: () => {
56 | return [
57 | require('precss'),
58 | require('postcss-advanced-variables')({
59 | variables: require('./src/colors')
60 | }),
61 | require('autoprefixer')({ browsers: ['last 2 versions'] })
62 | ];
63 | }
64 | };
65 |
66 | if (DEV) {
67 | console.log('dev build');
68 | config.entry.push('webpack-hot-middleware/client');
69 |
70 | config.plugins.push(
71 | new webpack.HotModuleReplacementPlugin(),
72 | new webpack.NoErrorsPlugin()
73 | );
74 | } else {
75 | console.log('production build');
76 | // Minify JS for production
77 | config.plugins.push(
78 | new webpack.optimize.UglifyJsPlugin({
79 | compress: {
80 | warnings: false,
81 | unused: true,
82 | dead_code: true
83 | }
84 | }),
85 | new webpack.DefinePlugin({
86 | 'process.env': {
87 | 'NODE_ENV': JSON.stringify('"production"')
88 | }
89 | })
90 | );
91 | }
92 |
93 | module.exports = config;
94 |
--------------------------------------------------------------------------------
/karma.config.js:
--------------------------------------------------------------------------------
1 | const webpack = require('webpack');
2 | // See issues for details on parts of this config.
3 | // https://github.com/airbnb/enzyme/issues/47
4 | // had issues loading sinon as its a dep of enzyme
5 | var argv = require('minimist')(process.argv.slice(2));
6 |
7 |
8 | module.exports = (config) => {
9 | config.set({
10 | browsers: [ 'PhantomJS' ], // run in Chrome
11 | singleRun: argv.watch ? false : true, // just run once by default
12 | frameworks: [ 'mocha' ], // use the mocha test framework
13 | files: [
14 | 'tests.webpack.js' // just load this file
15 | ],
16 | preprocessors: {
17 | 'tests.webpack.js': [ 'webpack', 'sourcemap' ] // preprocess with webpack and our sourcemap loader
18 | },
19 | reporters: [ 'dots' ], // report results in this format
20 | webpack: { // kind of a copy of your webpack config
21 | devtool: 'inline-source-map', // just do inline source maps instead of the default
22 | module: {
23 | preLoaders: [{
24 | test: /\.(js|jsx)$/,
25 | include: /src/,
26 | exclude: /node_modules/,
27 | loader: 'isparta'
28 | }],
29 | loaders: [{
30 | test: /\.jsx?$/,
31 | exclude: /(node_modules)/,
32 | loaders: ['babel']
33 | }, {
34 | test: /\.jpe?g$|\.gif$|\.png$|\.ico$/,
35 | loader: 'url-loader?name=[path][name].[ext]&context=./src'
36 | }, {
37 | test: /\.html/,
38 | loader: 'file?name=[name].[ext]'
39 | }, {
40 | test: /\.css$/,
41 | // or ?sourceMap&modules&importLoaders=1!postcss-loader
42 | loader: 'style-loader!css-loader?modules&importLoaders=1!postcss-loader'
43 | }, {
44 | test: /\.json$/,
45 | loader: 'json'
46 | },
47 | {test: /\.eot(\?v=\d+\.\d+\.\d+)?$/, loader: 'file?mimetype=application/vnd.ms-fontobject'},
48 | {test: /\.woff(2)?(\?v=\d+\.\d+\.\d+)?$/, loader: 'url?limit=10000&mimetype=application/font-woff' },
49 | {test: /\.ttf(\?v=\d+\.\d+\.\d+)?$/, loader: 'url?limit=10000&mimetype=application/octet-stream' },
50 | {test: /.svg(\?v=\d+\.\d+\.\d+)?$|.svg$/, loader: 'url?name=[path][name].[ext]&context=./src&mimetype=image/svg+xml'},
51 | {
52 | test: /sinon\.js$/,
53 | loader: 'imports?define=>false,require=>false'
54 | }
55 | ]
56 | },
57 | postcss: () => {
58 | return [
59 | require('precss'),
60 | require('postcss-simple-vars')({
61 | variables: () => {
62 | return require('./src/colors');
63 | }
64 | }),
65 | require('autoprefixer')({ browsers: ['last 2 versions'] })
66 | ];
67 | },
68 | isparta: {
69 | embedSource: true,
70 | noAutoWrap: true
71 | // these babel options will be passed only to isparta and not to babel-loader
72 | },
73 | externals: {
74 | jsdom: 'window',
75 | cheerio: 'window',
76 | 'react/lib/ExecutionEnvironment': true,
77 | 'react/lib/ReactContext': 'window',
78 | 'text-encoding': 'window'
79 | },
80 | resolve: {
81 | alias: {
82 | sinon: 'sinon/pkg/sinon'
83 | }
84 | }
85 | },
86 |
87 | webpackServer: {
88 | noInfo: false // please don't spam the console when running in karma!
89 | }
90 | });
91 | };
92 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-redux-boilerplate",
3 | "version": "1.0.2",
4 | "description": "An upto date react redux boilerplate",
5 | "main": "server.js",
6 | "scripts": {
7 | "build": "NODE_ENV=production webpack --colors --progress",
8 | "lint": "eslint --ext .js --ext .jsx ./test ./src; exit 0",
9 | "start": "node server --colors",
10 | "test": "npm run lint && npm run mocha && npm run karma",
11 | "test:watch": "npm run mocha:watch & npm run karma:watch",
12 | "mocha": "mocha --compilers js:babel-core/register src/reducers --recursive",
13 | "mocha:watch": "npm run mocha -- --watch",
14 | "karma": "NODE_ENV=production karma start karma.config.js",
15 | "karma:watch": "npm run karma -- --watch"
16 | },
17 | "repository": {
18 | "type": "git",
19 | "url": "git+https://github.com/export-mike/react-redux-boilerplate.git"
20 | },
21 | "author": "mike james ",
22 | "license": "UNLICENSED",
23 | "bugs": {
24 | "url": "https://github.com/export-mike/react-redux-boilerplate/issues"
25 | },
26 | "homepage": "https://github.com/export-mike/react-redux-boilerplate#readme",
27 | "dependencies": {
28 | "autoprefixer": "^6.3.1",
29 | "babel-core": "^6.4.5",
30 | "babel-eslint": "^5.0.0-beta6",
31 | "babel-loader": "^6.2.1",
32 | "babel-polyfill": "^6.3.14",
33 | "css-loader": "^0.23.1",
34 | "eslint": "^1.10.3",
35 | "eslint-config-pebblecode": "^1.5.0",
36 | "eslint-plugin-react": "^3.15.0",
37 | "express": "^4.13.3",
38 | "extract-text-webpack-plugin": "^1.0.1",
39 | "file-loader": "^0.8.5",
40 | "fork-placeholders.js": "^4.0.1",
41 | "history": "^1.17.0",
42 | "html-webpack-plugin": "^1.7.0",
43 | "open-sans-fontface": "^1.4.0",
44 | "path": "^0.12.7",
45 | "postcss-loader": "^0.8.0",
46 | "precss": "^1.4.0",
47 | "react": "^0.14.6",
48 | "react-css-modules": "^3.6.8",
49 | "react-dom": "^0.14.6",
50 | "react-hot-loader": "^1.3.0",
51 | "react-redux": "^4.0.6",
52 | "react-router": "2.0.0-rc5",
53 | "react-router-redux": "^2.1.0",
54 | "redux": "^3.3.1",
55 | "redux-form": "^4.1.5",
56 | "redux-logger": "^2.3.2",
57 | "redux-thunk": "^1.0.3",
58 | "style-loader": "^0.13.0",
59 | "url-loader": "^0.5.7",
60 | "webpack": "^1.12.10"
61 | },
62 | "devDependencies": {
63 | "babel-plugin-react-transform": "^2.0.0",
64 | "babel-polyfill": "^6.3.14",
65 | "babel-preset-es2015": "^6.3.13",
66 | "babel-preset-react": "^6.3.13",
67 | "babel-preset-react-hmre": "^1.0.0",
68 | "babel-preset-stage-0": "^6.3.13",
69 | "chai": "^3.4.1",
70 | "enzyme": "^1.4.1",
71 | "imports-loader": "^0.6.5",
72 | "isparta-loader": "^2.0.0",
73 | "json-loader": "^0.5.4",
74 | "karma": "^0.13.19",
75 | "karma-chrome-launcher": "^0.2.2",
76 | "karma-mocha": "^0.2.1",
77 | "karma-phantomjs-launcher": "^1.0.0",
78 | "karma-sourcemap-loader": "^0.3.7",
79 | "karma-webpack": "^1.7.0",
80 | "minimist": "^1.2.0",
81 | "mocha": "^2.3.4",
82 | "opn": "^3.0.3",
83 | "phantomjs-prebuilt": "^2.1.3",
84 | "pre-commit": "^1.1.2",
85 | "react-addons-test-utils": "^0.14.7",
86 | "react-transform-catch-errors": "^1.0.1",
87 | "react-transform-hmr": "^1.0.1",
88 | "redux-devtools": "^3.0.1",
89 | "redux-devtools-dock-monitor": "^1.0.1",
90 | "redux-devtools-log-monitor": "^1.0.2",
91 | "sinon": "^1.17.3",
92 | "webpack-dev-middleware": "^1.5.1",
93 | "webpack-dev-server": "^1.14.0",
94 | "webpack-hot-middleware": "^2.6.2",
95 | "webpack-hot-server": "^0.2.2",
96 | "webpack-merge": "^0.7.3"
97 | },
98 | "pre-commit": [
99 | "test"
100 | ]
101 | }
102 |
--------------------------------------------------------------------------------
/src/reducers/test/auth_spec.js:
--------------------------------------------------------------------------------
1 | import {expect} from 'chai';
2 | import reducer from '../auth';
3 |
4 | describe('auth reducer', () => {
5 |
6 | it('Handles AUTH', () => {
7 | const initialState = {
8 | token: null,
9 | tokenExpires: null,
10 | username: null,
11 | authFetch: false,
12 | authFailed: false,
13 | errorMessage: null,
14 | error: null,
15 | forgot: {
16 | authForgetFetch: false,
17 | error: null,
18 | errorMessage: null,
19 | success: false
20 | }
21 | };
22 |
23 | const newState = reducer(initialState, {type: 'AUTH', payload: {username: 'admin'}});
24 |
25 | expect(newState).to.eql({
26 | token: null,
27 | tokenExpires: null,
28 | username: 'admin',
29 | authFetch: true,
30 | authFailed: false,
31 | errorMessage: null,
32 | error: null,
33 | forgot: {
34 | authForgetFetch: false,
35 | error: null,
36 | errorMessage: null,
37 | success: false
38 | }
39 | });
40 |
41 | });
42 |
43 | it('Handles AUTH_SUCCESS', () => {
44 |
45 |
46 | const initialState = {
47 | token: null,
48 | tokenExpires: null,
49 | username: 'admin',
50 | authFetch: true,
51 | authFailed: false,
52 | errorMessage: null,
53 | error: null,
54 | forgot: {
55 | authForgetFetch: false,
56 | error: null,
57 | errorMessage: null,
58 | success: false
59 | }
60 | };
61 |
62 | const expireDate = new Date();
63 |
64 | const newState = reducer(initialState, {
65 | type: 'AUTH_SUCCESS',
66 | payload: {
67 | token: '1234',
68 | expires: expireDate
69 | }
70 | });
71 |
72 | expect(newState).to.eql({
73 | token: '1234',
74 | tokenExpires: expireDate,
75 | username: 'admin',
76 | authFetch: false,
77 | authFailed: false,
78 | errorMessage: null,
79 | error: null,
80 | forgot: {
81 | authForgetFetch: false,
82 | error: null,
83 | errorMessage: null,
84 | success: false
85 | }
86 | });
87 |
88 | });
89 |
90 | it('Handles AUTH_FAIL', () => {
91 |
92 | const initialState = {
93 | token: null,
94 | tokenExpires: null,
95 | username: null,
96 | authFetch: true,
97 | authFailed: false,
98 | errorMessage: null,
99 | error: null,
100 | forgot: {
101 | authForgetFetch: false,
102 | error: null,
103 | errorMessage: null,
104 | success: false
105 | }
106 | };
107 |
108 | const error = new Error();
109 |
110 | const newState = reducer(initialState, {
111 | type: 'AUTH_FAIL',
112 | payload: {
113 | ErrorMessage: 'An Error Occurred',
114 | error
115 | }
116 | });
117 |
118 | expect(newState).to.eql({
119 | token: null,
120 | tokenExpires: null,
121 | username: null,
122 | authFetch: false,
123 | authFailed: false,
124 | errorMessage: 'An Error Occurred',
125 | error: error,
126 | forgot: {
127 | authForgetFetch: false,
128 | error: null,
129 | errorMessage: null,
130 | success: false
131 | }
132 | });
133 | });
134 |
135 | it('Handles AUTH_FORGOT', () => {
136 |
137 | const initialState = {
138 | token: null,
139 | tokenExpires: null,
140 | username: null,
141 | authFetch: true,
142 | authFailed: false,
143 | errorMessage: null,
144 | error: null,
145 | forgot: {
146 | authForgetFetch: false,
147 | error: null,
148 | errorMessage: null,
149 | success: false
150 | }
151 | };
152 |
153 | const newState = reducer(initialState, {type: 'AUTH_FORGOT'});
154 |
155 | expect(newState).to.eql({
156 | token: null,
157 | tokenExpires: null,
158 | username: null,
159 | authFetch: true,
160 | authFailed: false,
161 | errorMessage: null,
162 | error: null,
163 | forgot: {
164 | authForgetFetch: false,
165 | error: null,
166 | errorMessage: null,
167 | success: false
168 | }
169 | });
170 |
171 | });
172 |
173 | it('Handles AUTH_FORGOT_SUCCESS', () => {
174 |
175 | const initialState = {
176 | token: null,
177 | tokenExpires: null,
178 | username: null,
179 | authFetch: true,
180 | authFailed: false,
181 | errorMessage: null,
182 | error: null,
183 | forgot: {
184 | authForgetFetch: false,
185 | error: null,
186 | errorMessage: null,
187 | success: false
188 | }
189 | };
190 |
191 | const newState = reducer(initialState, {type: 'AUTH_FORGOT_SUCCESS'});
192 |
193 | expect(newState).to.eql({
194 | token: null,
195 | tokenExpires: null,
196 | username: null,
197 | authFetch: true,
198 | authFailed: false,
199 | errorMessage: null,
200 | error: null,
201 | forgot: {
202 | authForgetFetch: false,
203 | error: null,
204 | errorMessage: null,
205 | success: true
206 | }
207 | });
208 | });
209 |
210 | it('Handles AUTH_FORGOT_FAIL', () => {
211 |
212 | const initialState = {
213 | token: null,
214 | tokenExpires: null,
215 | username: null,
216 | authFetch: true,
217 | authFailed: false,
218 | errorMessage: null,
219 | error: null,
220 | forgot: {
221 | authForgetFetch: false,
222 | error: null,
223 | errorMessage: null,
224 | success: false
225 | }
226 | };
227 |
228 | const error = new Error();
229 |
230 | const newState = reducer(initialState, {
231 | type: 'AUTH_FORGOT_FAIL',
232 | payload: {
233 | message: 'Something Wrong Happened',
234 | error
235 | }
236 | });
237 |
238 | expect(newState).to.eql({
239 | token: null,
240 | tokenExpires: null,
241 | username: null,
242 | authFetch: true,
243 | authFailed: false,
244 | errorMessage: null,
245 | error: null,
246 | forgot: {
247 | authForgetFetch: false,
248 | error: error,
249 | errorMessage: 'Something Wrong Happened',
250 | success: false
251 | }
252 | });
253 | });
254 |
255 | it('Handles auth AUTH_LOGOUT', () => {
256 | const initialState = {
257 | token: '1234',
258 | tokenExpires: null,
259 | username: 'admin',
260 | authFetch: false,
261 | authFailed: false,
262 | errorMessage: null,
263 | error: null,
264 | logoutFetch: false,
265 | forgot: {
266 | authForgetFetch: false,
267 | error: null,
268 | errorMessage: null,
269 | success: false
270 | }
271 | };
272 |
273 | const newState = reducer(initialState, {
274 | type: 'AUTH_LOGOUT'
275 | });
276 |
277 | expect(newState).to.eql({
278 | token: '1234',
279 | tokenExpires: null,
280 | username: 'admin',
281 | authFetch: false,
282 | authFailed: false,
283 | errorMessage: null,
284 | error: null,
285 | logoutFetch: true,
286 | forgot: {
287 | authForgetFetch: false,
288 | error: null,
289 | errorMessage: null,
290 | success: false
291 | }
292 | });
293 | });
294 |
295 | it('Handles auth AUTH_LOGOUT_SUCCESS', () => {
296 | const initialState = {
297 | token: '1234',
298 | tokenExpires: null,
299 | username: 'username',
300 | authFetch: false,
301 | authFailed: false,
302 | errorMessage: null,
303 | error: null,
304 | logoutFetch: true,
305 | forgot: {
306 | authForgetFetch: false,
307 | error: null,
308 | errorMessage: null,
309 | success: false
310 | }
311 | };
312 |
313 | const newState = reducer(initialState, {
314 | type: 'AUTH_LOGOUT_SUCCESS'
315 | });
316 |
317 | expect(newState).to.eql({
318 | token: null,
319 | tokenExpires: null,
320 | username: null,
321 | authFetch: false,
322 | authFailed: false,
323 | errorMessage: null,
324 | error: null,
325 | logoutFetch: false,
326 | forgot: {
327 | authForgetFetch: false,
328 | error: null,
329 | errorMessage: null,
330 | success: false
331 | }
332 | });
333 | });
334 |
335 | it('Handles auth AUTH_LOGOUT_FAIL', () => {
336 | const initialState = {
337 | token: '1234',
338 | tokenExpires: null,
339 | username: 'admin',
340 | authFetch: false,
341 | authFailed: false,
342 | errorMessage: null,
343 | error: null,
344 | logoutFetch: false,
345 | logoutFetchFail: false,
346 | forgot: {
347 | authForgetFetch: false,
348 | error: null,
349 | errorMessage: null,
350 | success: false
351 | }
352 | };
353 |
354 | const newState = reducer(initialState, {
355 | type: 'AUTH_LOGOUT_FAIL'
356 | });
357 |
358 | expect(newState).to.eql({
359 | token: '1234',
360 | tokenExpires: null,
361 | username: 'admin',
362 | authFetch: false,
363 | authFailed: false,
364 | errorMessage: null,
365 | error: null,
366 | logoutFetch: false,
367 | logoutFetchFail: true,
368 | forgot: {
369 | authForgetFetch: false,
370 | error: null,
371 | errorMessage: null,
372 | success: false
373 | }
374 | });
375 | });
376 | });
377 |
--------------------------------------------------------------------------------