├── tests ├── routes │ ├── Home │ │ ├── index.js │ │ ├── UserList │ │ │ ├── index.js │ │ │ ├── modules │ │ │ │ ├── reducers.spec.js │ │ │ │ └── actions.spec.js │ │ │ ├── components │ │ │ │ └── User.spec.js │ │ │ └── UserList.spec.js │ │ └── Report │ │ │ └── Report.spec.js │ └── Login │ │ ├── index.js │ │ └── LoginForm │ │ ├── modules │ │ └── actions.spec.js │ │ └── LoginForm.spec.js ├── store │ ├── createStore.spec.js │ └── reducers │ │ └── currentUser.spec.js ├── testHelper.js ├── components │ ├── Footer │ │ └── Footer.spec.js │ ├── Loading │ │ └── Loading.spec.js │ ├── Root │ │ └── Root.spec.js │ ├── Header │ │ └── Header.spec.js │ └── Forms │ │ └── LoginForm │ │ └── LoginForm.spec.js └── layouts │ └── CoreLayout │ └── CoreLayout.spec.js ├── src ├── components │ ├── Footer │ │ ├── Footer.scss │ │ ├── index.js │ │ └── Footer.js │ ├── Root │ │ ├── index.js │ │ └── Root.js │ ├── Header │ │ ├── index.js │ │ ├── Header.scss │ │ └── Header.js │ ├── Loading │ │ ├── index.js │ │ ├── Loading.js │ │ └── Loading.scss │ └── Forms │ │ └── LoginForm │ │ ├── index.js │ │ ├── LoginForm.scss │ │ └── LoginForm.js ├── static │ ├── styles │ │ ├── vendor │ │ │ └── .gitkeep │ │ ├── core.scss │ │ ├── module │ │ │ └── _panel.scss │ │ └── base │ │ │ └── _normalize.scss │ ├── robots.txt │ ├── logo.png │ ├── favicon.ico │ ├── redux.icns │ └── humans.txt ├── routes │ ├── Home │ │ ├── UserList │ │ │ ├── UserList.scss │ │ │ ├── index.js │ │ │ ├── modules │ │ │ │ ├── reducers.js │ │ │ │ └── actions.js │ │ │ ├── components │ │ │ │ ├── User.scss │ │ │ │ └── User.js │ │ │ └── UserList.js │ │ ├── Report │ │ │ ├── index.js │ │ │ ├── Report.scss │ │ │ └── Report.js │ │ └── index.js │ ├── Login │ │ ├── index.scss │ │ ├── LoginForm │ │ │ ├── index.js │ │ │ ├── modules │ │ │ │ └── actions.js │ │ │ └── LoginForm.js │ │ └── index.js │ ├── NotFound │ │ └── index.js │ └── index.js ├── layouts │ └── CoreLayout │ │ ├── CoreLayout.scss │ │ ├── index.js │ │ └── CoreLayout.js ├── index.html ├── store │ ├── reducers.js │ ├── createStore.js │ └── reducers │ │ └── currentUser.js ├── modules │ ├── refetch.js │ ├── currentUser.js │ └── fetch.js └── main.js ├── cli ├── theme │ ├── font-awesome.config.less │ ├── font-awesome.config.js │ ├── font-awesome.config.prod.js │ └── extract-style-loader.js ├── server.js ├── client.js ├── webpack.dev.js ├── webpack.test.js ├── webpack.prod.js └── webpack.config.js ├── postcss.config.js ├── .gitignore ├── .babelrc ├── .eslintrc.json ├── README.md ├── .bootstraprc ├── .stylelintrc └── package.json /tests/routes/Home/index.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/routes/Login/index.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/Footer/Footer.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/static/styles/vendor/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/store/createStore.spec.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /cli/theme/font-awesome.config.less: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/routes/Home/UserList/UserList.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/routes/Home/UserList/index.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = {}; 2 | -------------------------------------------------------------------------------- /src/static/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: 3 | -------------------------------------------------------------------------------- /src/routes/Login/index.scss: -------------------------------------------------------------------------------- 1 | .login { 2 | overflow: auto; 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /dist 3 | /build 4 | /coverage 5 | *.swp 6 | *.swo 7 | -------------------------------------------------------------------------------- /src/components/Root/index.js: -------------------------------------------------------------------------------- 1 | import Root from './Root' 2 | 3 | export default Root 4 | -------------------------------------------------------------------------------- /src/layouts/CoreLayout/CoreLayout.scss: -------------------------------------------------------------------------------- 1 | .mainContainer { 2 | padding: 20px 0; 3 | } 4 | -------------------------------------------------------------------------------- /src/components/Footer/index.js: -------------------------------------------------------------------------------- 1 | import Footer from './Footer' 2 | 3 | export default Footer 4 | -------------------------------------------------------------------------------- /src/components/Header/index.js: -------------------------------------------------------------------------------- 1 | import Header from './Header' 2 | 3 | export default Header 4 | -------------------------------------------------------------------------------- /src/routes/Home/Report/index.js: -------------------------------------------------------------------------------- 1 | import Report from './Report' 2 | 3 | export default Report 4 | -------------------------------------------------------------------------------- /src/static/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MQuy/redux-boilerplate/HEAD/src/static/logo.png -------------------------------------------------------------------------------- /src/components/Loading/index.js: -------------------------------------------------------------------------------- 1 | import Loading from './Loading' 2 | 3 | export default Loading 4 | -------------------------------------------------------------------------------- /src/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MQuy/redux-boilerplate/HEAD/src/static/favicon.ico -------------------------------------------------------------------------------- /src/static/redux.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MQuy/redux-boilerplate/HEAD/src/static/redux.icns -------------------------------------------------------------------------------- /src/components/Forms/LoginForm/index.js: -------------------------------------------------------------------------------- 1 | import LoginForm from './LoginForm' 2 | export default LoginForm 3 | -------------------------------------------------------------------------------- /src/layouts/CoreLayout/index.js: -------------------------------------------------------------------------------- 1 | import CoreLayout from './CoreLayout' 2 | 3 | export default CoreLayout 4 | -------------------------------------------------------------------------------- /src/routes/Home/UserList/index.js: -------------------------------------------------------------------------------- 1 | import UserList from './UserList' 2 | 3 | export default UserList 4 | -------------------------------------------------------------------------------- /src/routes/Login/LoginForm/index.js: -------------------------------------------------------------------------------- 1 | import LoginForm from './LoginForm' 2 | 3 | export default LoginForm 4 | -------------------------------------------------------------------------------- /src/static/styles/core.scss: -------------------------------------------------------------------------------- 1 | :global { 2 | @import "base/normalize"; 3 | @import "module/panel"; 4 | } 5 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["env", "react", "stage-0"], 3 | "plugins": ["transform-runtime", "transform-decorators-legacy"] 4 | } 5 | -------------------------------------------------------------------------------- /tests/testHelper.js: -------------------------------------------------------------------------------- 1 | import spies from 'chai-spies' 2 | import chaiAsPromised from 'chai-as-promised' 3 | 4 | chai.use(spies) 5 | chai.use(chaiAsPromised) 6 | -------------------------------------------------------------------------------- /src/components/Footer/Footer.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export const Footer = () => ( 4 | 6 | ) 7 | 8 | export default Footer 9 | -------------------------------------------------------------------------------- /src/static/humans.txt: -------------------------------------------------------------------------------- 1 | # Check it out: http://humanstxt.org/ 2 | 3 | # TEAM 4 | 5 | -- -- 6 | 7 | # THANKS 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/routes/Home/Report/Report.scss: -------------------------------------------------------------------------------- 1 | .panelRight { 2 | margin: 0 0 0 20px; 3 | } 4 | 5 | .chart { 6 | padding: 28px 4px 28px 0; 7 | } 8 | 9 | .avatar { 10 | width: 60%; 11 | } 12 | -------------------------------------------------------------------------------- /cli/theme/font-awesome.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | styles: { 3 | mixins: true, 4 | core: true, 5 | icons: true, 6 | larger: true, 7 | path: true, 8 | animated: true, 9 | } 10 | }; 11 | -------------------------------------------------------------------------------- /src/static/styles/module/_panel.scss: -------------------------------------------------------------------------------- 1 | .panel { 2 | background-color: white; 3 | border: 1px solid #e6e6e6; 4 | 5 | .title { 6 | padding: 28px 28px 8px 28px; 7 | font-size: 16px; 8 | font-weight: bold; 9 | text-transform: uppercase; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Boilerplater 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/routes/Home/UserList/modules/reducers.js: -------------------------------------------------------------------------------- 1 | import { CHANGE_ACTIVE_USER } from './actions' 2 | 3 | export function activeUser(state = {}, action) { 4 | switch(action.type) { 5 | case CHANGE_ACTIVE_USER: 6 | return { ...action.user }; 7 | default: 8 | return state; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/routes/Home/UserList/modules/actions.js: -------------------------------------------------------------------------------- 1 | export const CHANGE_ACTIVE_USER = 'CHANGE_ACTIVE_USER'; 2 | 3 | export function activeUser(user) { 4 | return { 5 | type: CHANGE_ACTIVE_USER, 6 | user 7 | } 8 | } 9 | 10 | export function changeActiveUser(user) { 11 | return (dispatch) => dispatch(activeUser(user)); 12 | } 13 | -------------------------------------------------------------------------------- /src/components/Header/Header.scss: -------------------------------------------------------------------------------- 1 | .nav { 2 | height: 64px; 3 | font-size: 16px; 4 | border-bottom: 1px solid #e3e3e3; 5 | background-color: white; 6 | box-shadow: 0 0 6px -2px rgba(0, 0, 0, 0.1); 7 | } 8 | 9 | .logo { 10 | height: 40px; 11 | } 12 | 13 | .navRight { 14 | margin: 14px 0; 15 | } 16 | 17 | .icon { 18 | cursor: pointer; 19 | } 20 | -------------------------------------------------------------------------------- /tests/components/Footer/Footer.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { shallow } from 'enzyme' 3 | import { Footer } from '$root/components/Footer/Footer' 4 | 5 | describe("(Component) Footer", () => { 6 | let _wrapper; 7 | 8 | beforeEach(() => { 9 | _wrapper = shallow() 10 | }) 11 | 12 | it('Should render', () => { 13 | expect(_wrapper.is('footer')).to.be.ok 14 | }) 15 | }) 16 | -------------------------------------------------------------------------------- /cli/theme/font-awesome.config.prod.js: -------------------------------------------------------------------------------- 1 | const extractStyleLoader = require('./extract-style-loader'); 2 | const fontAwesomeConfig = require('./font-awesome.config.js'); 3 | const ExtractTextPlugin = require('extract-text-webpack-plugin'); 4 | fontAwesomeConfig.styleLoader = extractStyleLoader(ExtractTextPlugin.extract({ 5 | fallback: 'style-loader', 6 | use: 'css-loader!less-loader' 7 | })) 8 | module.exports = fontAwesomeConfig; 9 | -------------------------------------------------------------------------------- /src/routes/NotFound/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { browserHistory } from 'react-router' 3 | 4 | const goBack = (e) => { 5 | e.preventDefault() 6 | return browserHistory.goBack() 7 | } 8 | 9 | export const NotFound = () => ( 10 | 11 | Page not found! 12 | ← Back 13 | 14 | ) 15 | 16 | export default NotFound 17 | -------------------------------------------------------------------------------- /tests/components/Loading/Loading.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { shallow } from 'enzyme' 3 | import { Loading } from '$root/components/Loading/Loading' 4 | 5 | describe("(Component) Loading", () => { 6 | let _wrapper; 7 | 8 | beforeEach(() => { 9 | _wrapper = shallow() 10 | }) 11 | 12 | it('Should render loading rect', () => { 13 | expect(_wrapper.find('div')).to.have.length(6) 14 | }) 15 | }) 16 | -------------------------------------------------------------------------------- /src/components/Loading/Loading.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import style from './Loading.scss' 3 | 4 | export const Loading = () => ( 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | ) 13 | 14 | export default Loading 15 | -------------------------------------------------------------------------------- /src/components/Root/Root.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | import { connect } from 'react-redux' 4 | 5 | export class Root extends Component { 6 | static propTypes = { 7 | children: PropTypes.element.isRequired 8 | } 9 | render() { 10 | const { children } = this.props; 11 | 12 | return ( 13 | 14 | {children} 15 | 16 | ) 17 | } 18 | } 19 | 20 | export default Root 21 | -------------------------------------------------------------------------------- /tests/components/Root/Root.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { shallow } from 'enzyme' 3 | import { Root } from '$root/components/Root/Root' 4 | 5 | describe("(Component) Root", () => { 6 | let _wrapper; 7 | 8 | beforeEach(() => { 9 | let _props = { 10 | children: 11 | } 12 | _wrapper = shallow() 13 | }) 14 | 15 | it('Should render ', () => { 16 | expect(_wrapper.find('anchor')).to.have.length(1) 17 | }) 18 | }) 19 | -------------------------------------------------------------------------------- /src/store/reducers.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux' 2 | import { currentUser } from './reducers/currentUser' 3 | 4 | export const reducers = (asyncReducers) => { 5 | return combineReducers({ 6 | // Add sync reducers here 7 | currentUser, 8 | ...asyncReducers }) 9 | } 10 | 11 | export const injectReducer = (store, { key, reducer }) => { 12 | store.asyncReducers[key] = reducer; 13 | store.replaceReducer(reducers(store.asyncReducers)) 14 | } 15 | 16 | export default reducers 17 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb", 3 | "plugins": [ 4 | "react" 5 | ], 6 | "parserOptions": { 7 | "ecmaVersion": 7, 8 | "ecmaFeatures": { 9 | "jsx": true, 10 | "experimentalObjectRestSpread": true 11 | } 12 | }, 13 | "rules": { 14 | "semi": 0, 15 | "comma-dangle": ["error", "never"], 16 | "no-use-before-define": ["error", { "functions": false, "classes": true }], 17 | "no-else-return": 0 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/store/createStore.js: -------------------------------------------------------------------------------- 1 | import { applyMiddleware, createStore } from 'redux' 2 | import thunk from 'redux-thunk' 3 | 4 | import reducers from './reducers' 5 | import { get, post } from '$root/modules/fetch' 6 | 7 | export default (initialState = {}) => { 8 | const thunkWrapper = thunk.withExtraArgument({ get, post }) 9 | const middleware = applyMiddleware(thunkWrapper) 10 | 11 | const store = createStore(reducers(), initialState, middleware) 12 | 13 | store.asyncReducers = {} 14 | 15 | return store 16 | } 17 | -------------------------------------------------------------------------------- /src/modules/refetch.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { get } from './fetch' 3 | import forEach from 'lodash/forEach' 4 | 5 | const refetch = (config) => (Component) => class _ extends React.Component { 6 | componentWillMount() { 7 | forEach(config, (link, stateName) => { 8 | get(link) 9 | .then(json => this.setState({ [stateName]: json })) 10 | }) 11 | } 12 | render() { 13 | return ( 14 | 15 | ) 16 | } 17 | } 18 | 19 | export default refetch 20 | -------------------------------------------------------------------------------- /src/routes/Login/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import LoginForm from './LoginForm' 3 | import { authorized } from '$root/modules/currentUser' 4 | import style from './index.scss' 5 | 6 | function checkAuth(store) { 7 | return (nextState, replace) => { 8 | if (authorized(store)) { 9 | replace('/'); 10 | } 11 | } 12 | } 13 | 14 | const Main = () => ( 15 | 16 | 17 | 18 | ) 19 | 20 | export default (store) => ({ 21 | component: Main, 22 | onEnter: checkAuth(store) 23 | }) 24 | -------------------------------------------------------------------------------- /src/routes/Home/UserList/components/User.scss: -------------------------------------------------------------------------------- 1 | .row { 2 | padding: 26px 0; 3 | position: relative; 4 | 5 | &:after { 6 | content: ""; 7 | position: absolute; 8 | bottom: 0; 9 | left: 28px; 10 | right: 28px; 11 | border-bottom: 1px dashed #e9e9e9; 12 | } 13 | 14 | &:last-of-type:after { 15 | border: 0; 16 | } 17 | } 18 | 19 | .leftPanel { 20 | text-align: center; 21 | } 22 | 23 | .avatar { 24 | width: 60%; 25 | } 26 | 27 | .icon { 28 | margin: 0 6px 0 0; 29 | } 30 | 31 | .userName { 32 | font-weight: bold; 33 | } 34 | -------------------------------------------------------------------------------- /src/modules/currentUser.js: -------------------------------------------------------------------------------- 1 | import simpleStorage from 'simplestorage.js' 2 | 3 | export const SIGNOUT_SUCCESS = 'SIGNOUT_SUCCESS'; 4 | 5 | function signOutSuccess() { 6 | return { 7 | type: SIGNOUT_SUCCESS 8 | } 9 | } 10 | 11 | export function authorized(store) { 12 | const { currentUser } = store.getState(); 13 | 14 | return currentUser && currentUser.id; 15 | } 16 | 17 | export function signOut(dispatch) { 18 | const result = simpleStorage.deleteKey('currentUser'); 19 | 20 | dispatch(signOutSuccess()); 21 | return Promise.resolve(result); 22 | } 23 | -------------------------------------------------------------------------------- /src/components/Forms/LoginForm/LoginForm.scss: -------------------------------------------------------------------------------- 1 | .loginForm { 2 | width: 480px; 3 | margin: 5% auto; 4 | background-color: white; 5 | box-shadow: 0 0 4px -2px rgba(0, 0, 0, 0.1); 6 | border: 1px solid #e0e0e0; 7 | } 8 | 9 | .title { 10 | font-size: 18px; 11 | color: white; 12 | background-color: #563d7c; 13 | padding: 12px 24px; 14 | text-transform: uppercase; 15 | margin: 0 0 8px 0; 16 | } 17 | 18 | .body { 19 | padding: 16px 24px 8px 24px; 20 | } 21 | 22 | .btnControl { 23 | padding: 8px 0; 24 | 25 | button { 26 | text-transform: uppercase; 27 | font-size: 15px; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/layouts/CoreLayout/CoreLayout.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import Header from '$root/components/Header' 4 | import Footer from '$root/components/Footer' 5 | import classes from './CoreLayout.scss' 6 | import '$root/static/styles/core.scss' 7 | 8 | export const CoreLayout = ({ children }) => ( 9 | 10 | 11 | 12 | {children} 13 | 14 | 15 | 16 | ) 17 | 18 | CoreLayout.propTypes = { 19 | children: PropTypes.element.isRequired 20 | } 21 | 22 | export default CoreLayout 23 | -------------------------------------------------------------------------------- /tests/routes/Home/UserList/modules/reducers.spec.js: -------------------------------------------------------------------------------- 1 | import { CHANGE_ACTIVE_USER } from '$root/routes/Home/UserList/modules/actions' 2 | import { activeUser as reducer } from '$root/routes/Home/UserList/modules/reducers' 3 | 4 | const initialState = undefined 5 | const user = { id: 1 } 6 | 7 | describe("(Reducer) activeUser", () => { 8 | it('Should return initial state', () => { 9 | expect(reducer(initialState, {})).to.eql({}) 10 | }) 11 | 12 | it('Should return active user', () => { 13 | const action = { type: CHANGE_ACTIVE_USER, user } 14 | 15 | expect(reducer(initialState, action)).to.eql(user) 16 | }) 17 | }) 18 | -------------------------------------------------------------------------------- /src/store/reducers/currentUser.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux' 2 | import { browserHistory } from 'react-router' 3 | import { LOGIN_SUCCESS } from '$root/routes/Login/LoginForm/modules/actions' 4 | import { AUTHORIZED_SUCCESS, SIGNOUT_SUCCESS } from '$root/modules/currentUser' 5 | 6 | export const CURRENT_USER = 'currentUser'; 7 | 8 | export function currentUser(state = {}, action) { 9 | 10 | switch(action.type) { 11 | case LOGIN_SUCCESS: 12 | case AUTHORIZED_SUCCESS: 13 | return { ...action.user }; 14 | case SIGNOUT_SUCCESS: 15 | return {}; 16 | default: 17 | return state; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/routes/Login/LoginForm/modules/actions.js: -------------------------------------------------------------------------------- 1 | import simpleStorage from 'simplestorage.js' 2 | import { post } from '$root/modules/fetch' 3 | 4 | export const LOGIN_SUCCESS = 'LOGIN_SUCCESS' 5 | 6 | export function loginSuccess(json) { 7 | return { 8 | type: LOGIN_SUCCESS, 9 | user: json 10 | } 11 | } 12 | 13 | function fetchLogin(user, dispatch) { 14 | return post('/users/sign_in', { user }) 15 | .then(json => dispatch(loginSuccess(json))) 16 | .then(action => simpleStorage.set('currentUser', action.user)) 17 | } 18 | 19 | export function loginAction(user, dispatch) { 20 | return fetchLogin(user, dispatch); 21 | } 22 | -------------------------------------------------------------------------------- /src/static/styles/base/_normalize.scss: -------------------------------------------------------------------------------- 1 | * { 2 | vertical-align: middle; 3 | } 4 | 5 | html, body { 6 | height: 100%; 7 | min-height: 100%; 8 | } 9 | 10 | body { 11 | background-color: #f7f7f7; 12 | color: #333; 13 | } 14 | 15 | body, input, textarea, button { 16 | font-size: 14px; 17 | font-family: helvetica; 18 | } 19 | 20 | html, body, ul { 21 | margin: 0; 22 | padding: 0; 23 | } 24 | 25 | button, input, textarea { 26 | outline: none; 27 | } 28 | 29 | [draggable] { 30 | user-select: none; 31 | } 32 | 33 | textarea { 34 | resize: none; 35 | } 36 | 37 | a[href^="http"]:empty::before { 38 | content: attr(href); 39 | } 40 | -------------------------------------------------------------------------------- /src/routes/Home/UserList/components/User.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import style from './User.scss' 4 | 5 | export const User = ({ user, onClick }) => ( 6 | 7 | 8 | 9 | 10 | 11 | {user.fullName} - {user.email} 12 | 13 | 14 | ) 15 | 16 | User.propTypes = { 17 | user: PropTypes.object, 18 | onClick: PropTypes.func 19 | } 20 | 21 | export default User 22 | -------------------------------------------------------------------------------- /src/routes/Home/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import UserList from './UserList' 3 | import Report from './Report' 4 | import { authorized } from '$root/modules/currentUser' 5 | 6 | function checkAuth(store) { 7 | return (nextState, replace) => { 8 | if (!authorized(store)) { 9 | replace('/login'); 10 | } 11 | } 12 | } 13 | 14 | 15 | const Main = () => ( 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | ) 27 | 28 | export default (store) => ({ 29 | component: Main, 30 | onEnter: checkAuth(store) 31 | }) 32 | -------------------------------------------------------------------------------- /tests/routes/Home/Report/Report.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { shallow } from 'enzyme' 3 | import { Report } from '$root/routes/Home/Report/Report' 4 | 5 | const user = { id: 1, fullName: 'Redux' } 6 | 7 | describe("(Component) Report", () => { 8 | let _wrapper, _props; 9 | 10 | beforeEach(() => { 11 | _props = { user } 12 | _wrapper = shallow() 13 | }) 14 | 15 | it('Should render user name', () => { 16 | expect(_wrapper.html()).to.include(user.fullName) 17 | }) 18 | 19 | it('Should rerender name when change user', () => { 20 | let user1 = { id: 2, fullName: 'React' } 21 | 22 | _wrapper.setProps({ user: user1 }) 23 | expect(_wrapper.html()).to.include(user1.fullName) 24 | }) 25 | }) 26 | -------------------------------------------------------------------------------- /tests/routes/Home/UserList/components/User.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { shallow } from 'enzyme' 3 | import { User } from '$root//routes/Home/UserList/components/User' 4 | 5 | const user = { id: 1, email: 'abc@example.com', fullName: 'Redux' } 6 | 7 | describe("(Component) User", () => { 8 | let _wrapper, _props, _spy; 9 | 10 | beforeEach(() => { 11 | _props = { 12 | user, 13 | onClick: (_spy = chai.spy()) 14 | } 15 | _wrapper = shallow() 16 | }) 17 | 18 | it('Should contain username', () => { 19 | expect(_wrapper.text()).to.include(user.fullName) 20 | }) 21 | 22 | it('Should call when click', () => { 23 | _wrapper.simulate('click') 24 | expect(_spy).to.have.been.called 25 | }) 26 | }) 27 | -------------------------------------------------------------------------------- /src/routes/index.js: -------------------------------------------------------------------------------- 1 | import CoreLayout from '$root/layouts/CoreLayout/CoreLayout' 2 | import Home from './Home' 3 | import Login from './Login' 4 | 5 | export const createRoutes = (store) => { 6 | const routes = [{ 7 | path: '/login', 8 | ...Login(store) 9 | }, { 10 | path: '/', 11 | component: CoreLayout, 12 | indexRoute: Home(store) 13 | }, { 14 | path: '*', 15 | getComponent: (_, cb) => { 16 | import('./NotFound' /* webpackChunkName: "NotFound" */).then(loadRoute(cb)).catch(errorLoading) 17 | } 18 | }] 19 | 20 | return routes 21 | } 22 | 23 | function errorLoading(err) { 24 | console.error('Dynamic page loading failed', err); 25 | } 26 | 27 | function loadRoute(cb) { 28 | return (module) => cb(null, module.default); 29 | } 30 | 31 | export default createRoutes 32 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import { Router, browserHistory } from 'react-router' 4 | import createStore from './store/createStore' 5 | import { Provider } from 'react-redux' 6 | import Root from './components/Root' 7 | 8 | const MOUNT_ELEMENT = document.getElementById('root') 9 | 10 | const store = createStore(__INITIAL_STATE__) 11 | 12 | const render = () => { 13 | const routes = require('./routes/index').default(store) 14 | const App = ( 15 | 16 | 17 | 18 | ) 19 | ReactDOM.render(App, MOUNT_ELEMENT) 20 | } 21 | 22 | render() 23 | 24 | if('serviceWorker' in navigator) { 25 | navigator.serviceWorker.register('/service-worker.js'); 26 | } 27 | -------------------------------------------------------------------------------- /src/routes/Home/Report/Report.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | import { connect } from 'react-redux' 4 | import { get } from '$root/modules/fetch' 5 | import Loading from '$root/components/Loading' 6 | import style from './Report.scss' 7 | 8 | export class Report extends Component { 9 | static propTypes = { 10 | user: PropTypes.object 11 | } 12 | render() { 13 | const { user } = this.props; 14 | 15 | return ( 16 | 17 | Report {user.fullName ? ` - ${user.fullName}` : ''} 18 | 19 | 20 | ) 21 | } 22 | } 23 | 24 | function mapStateToProps(state) { 25 | return { 26 | user: state.activeUser 27 | } 28 | } 29 | 30 | export default connect(mapStateToProps)(Report) 31 | -------------------------------------------------------------------------------- /tests/routes/Home/UserList/modules/actions.spec.js: -------------------------------------------------------------------------------- 1 | import configureMockStore from 'redux-mock-store' 2 | import thunk from 'redux-thunk' 3 | import fetchMock from 'fetch-mock' 4 | import { activeUser, changeActiveUser, CHANGE_ACTIVE_USER } from '$root/routes/Home/UserList/modules/actions' 5 | 6 | const middlewares = [ thunk ] 7 | const mockStore = configureMockStore(middlewares) 8 | const user = { id: 1 } 9 | const expectedAction = { type: CHANGE_ACTIVE_USER, user }; 10 | const store = mockStore({ currentUser: {} }) 11 | 12 | describe("(Actions) User", () => { 13 | afterEach(() => { 14 | store.clearActions(); 15 | }) 16 | 17 | it('Should create action for active user', () => { 18 | expect(activeUser(user)).to.eql(expectedAction) 19 | }) 20 | 21 | it('Should dispatch active user', () => { 22 | store.dispatch(changeActiveUser(user)) 23 | expect(store.getActions()).to.eql([expectedAction]) 24 | }) 25 | }) 26 | -------------------------------------------------------------------------------- /cli/theme/extract-style-loader.js: -------------------------------------------------------------------------------- 1 | function encodeLoader(loader) { 2 | if (typeof loader === "string") { 3 | return loader; 4 | } 5 | 6 | if (typeof loader.options !== "undefined") { 7 | const query = Object 8 | .keys(loader.options) 9 | .map(function map(param) { 10 | return `${encodeURIComponent(param)}=${encodeURIComponent(loader.options[param])}`; 11 | }) 12 | .join("&"); 13 | return `${loader.loader}?${query}`; 14 | } 15 | return loader.loader; 16 | } 17 | 18 | module.exports = function buildExtractStylesLoader(loaders) { 19 | const extractTextLoader = encodeLoader(loaders[0]); 20 | const fallbackLoader = encodeLoader(loaders[1]); 21 | 22 | const restLoaders = loaders 23 | .slice(2) 24 | .map(function map(loader) { 25 | if (typeof loader === "string") { 26 | return loader; 27 | } 28 | return encodeLoader(loader); 29 | }); 30 | 31 | return [ 32 | extractTextLoader, 33 | fallbackLoader, 34 | ...restLoaders, 35 | ].join("!"); 36 | }; 37 | -------------------------------------------------------------------------------- /cli/server.js: -------------------------------------------------------------------------------- 1 | var express = require("express"); 2 | var app = express(); 3 | var cors = require('cors'); 4 | var path = require("path"); 5 | var users = [ 6 | {id: 1, email: 'admin@example.com', full_name: 'Admin', authentication_token: 'xxx', avatar_url: 'http://assets.pokemon.com/assets/cms2/img/pokedex/full/001.png'}, 7 | {id: 2, email: 'user@example.com', full_name: 'User', authentication_token: 'xxx', avatar_url: 'http://assets.pokemon.com/assets/cms2/img/pokedex/full/002.png'}, 8 | ]; 9 | 10 | app.use(express.static('dist')); 11 | app.use(cors()); 12 | app.set('port', (process.env.PORT || 3000)); 13 | 14 | app.post('/users/sign_in', function(req, res) { 15 | res.json(users[0]); 16 | }); 17 | 18 | app.get('/users/:id', function(req, res) { 19 | res.json(users[0]); 20 | }); 21 | 22 | app.get('/users', function(req, res) { 23 | res.json(users); 24 | }) 25 | 26 | app.listen(app.get('port'), function() { 27 | console.log("Node app is running on port:" + app.get('port')) 28 | }) 29 | -------------------------------------------------------------------------------- /tests/store/reducers/currentUser.spec.js: -------------------------------------------------------------------------------- 1 | import { currentUser as reducer } from '$root/store/reducers/currentUser' 2 | import { LOGIN_SUCCESS } from '$root/routes/Login/LoginForm/modules/actions' 3 | import { AUTHORIZED_SUCCESS, SIGNOUT_SUCCESS } from '$root/modules/currentUser' 4 | 5 | const user = { id: 1 } 6 | const initialState = undefined; 7 | 8 | describe("(Reducer) currentUser", () => { 9 | it('should return initial state', () => { 10 | expect(reducer(initialState, {})).to.eql({}) 11 | }) 12 | 13 | describe('return user', () => { 14 | it('after authorize successfully', () => { 15 | const action = { type: AUTHORIZED_SUCCESS, user } 16 | 17 | expect(reducer(initialState, action)).to.eql(user) 18 | }) 19 | 20 | it('after login successfully', () => { 21 | const action = { type: LOGIN_SUCCESS, user } 22 | 23 | expect(reducer(initialState, action)).to.eql(user) 24 | }) 25 | }) 26 | 27 | it('should clear user after logout', () => { 28 | const action = { type: SIGNOUT_SUCCESS } 29 | 30 | expect(reducer(initialState, action)).to.eql({}) 31 | }) 32 | }) 33 | -------------------------------------------------------------------------------- /src/routes/Login/LoginForm/LoginForm.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | import { connect } from 'react-redux' 4 | import LoginForm from '$root/components/Forms/LoginForm' 5 | import { loginAction } from './modules/actions' 6 | 7 | export class LoginFormContainer extends Component { 8 | static contextTypes = { 9 | router: PropTypes.object 10 | } 11 | state = { messages: [] } 12 | constructor(props) { 13 | super(props) 14 | 15 | this.formSubmit = this.formSubmit.bind(this) 16 | } 17 | formSubmit(email, password) { 18 | const { dispatch } = this.props; 19 | const { router } = this.context; 20 | const user = { email , password }; 21 | 22 | // NOTE: This then doesn't call in test 23 | return loginAction(user, dispatch) 24 | .then(() => { router.push('/') }) 25 | .catch(json => this.setState({messages: json})) 26 | } 27 | render() { 28 | const { messages } = this.state; 29 | 30 | return ( 31 | 32 | ) 33 | } 34 | } 35 | 36 | export default connect()(LoginFormContainer) 37 | -------------------------------------------------------------------------------- /tests/components/Header/Header.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { shallow } from 'enzyme' 3 | import { Header } from '$root/components/Header/Header' 4 | 5 | const currentUser = { id: 1, fullName: 'Redux' } 6 | 7 | describe("(Component) Header", () => { 8 | let _wrapper; 9 | 10 | beforeEach(() => { 11 | let _props = { 12 | currentUser: currentUser 13 | } 14 | _wrapper = shallow() 15 | }) 16 | 17 | 18 | it('Should render as a .', () => { 19 | expect(_wrapper.is('header')).to.be.ok 20 | }) 21 | 22 | it('Should render logo in ', () => { 23 | expect(_wrapper.find('li.nav-item img')).to.have.length(1) 24 | }) 25 | 26 | it('Should render with an that includes user name', () => { 27 | expect(_wrapper.text()).to.include(currentUser.fullName) 28 | }) 29 | 30 | it('Should render logout icon', () => { 31 | expect(_wrapper.find('i.fa-sign-out')).to.have.length(1) 32 | }) 33 | 34 | it('should rerender user name', () => { 35 | const user = { id: 2, fullName: 'React' } 36 | 37 | _wrapper.setProps({ currentUser: user }) 38 | expect(_wrapper.text()).to.include(user.fullName) 39 | }) 40 | }) 41 | -------------------------------------------------------------------------------- /tests/layouts/CoreLayout/CoreLayout.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Provider } from 'react-redux' 3 | import configureMockStore from 'redux-mock-store' 4 | import thunk from 'redux-thunk' 5 | import { render, shallow } from 'enzyme' 6 | import { CoreLayout } from '$root/layouts/CoreLayout/CoreLayout' 7 | import Footer from '$root/components/Footer' 8 | import Header from '$root/components/Header' 9 | 10 | const middlewares = [ thunk ] 11 | const mockStore = configureMockStore(middlewares) 12 | const store = mockStore({ currentUser: {} }) 13 | 14 | describe("(Component) CoreLayout", () => { 15 | let _wrapper, _props; 16 | 17 | beforeEach(() => { 18 | _props = { children: } 19 | _wrapper = shallow( 20 | 21 | , { context: { store } }) 22 | }) 23 | 24 | it('Should contain Header', () => { 25 | expect(_wrapper.containsMatchingElement()).to.be.ok 26 | }) 27 | 28 | it('Should contain Footer', () => { 29 | expect(_wrapper.containsMatchingElement()).to.be.ok 30 | }) 31 | 32 | it('Should render ', () => { 33 | expect(_wrapper.containsMatchingElement()).to.be.ok 34 | }) 35 | }) 36 | -------------------------------------------------------------------------------- /src/components/Loading/Loading.scss: -------------------------------------------------------------------------------- 1 | .spinner { 2 | margin: 0 auto; 3 | width: 50px; 4 | height: 40px; 5 | text-align: center; 6 | font-size: 10px; 7 | background-color: white; 8 | } 9 | 10 | .spinner > div { 11 | background-color: #00cdaf; 12 | height: 100%; 13 | width: 6px; 14 | display: inline-block; 15 | margin: 0 3px 0 0; 16 | 17 | -webkit-animation: sk-stretchdelay 1.2s infinite ease-in-out; 18 | animation: sk-stretchdelay 1.2s infinite ease-in-out; 19 | } 20 | 21 | .rect2.rect2 { 22 | -webkit-animation-delay: -1.1s; 23 | animation-delay: -1.1s; 24 | } 25 | 26 | .rect3.rect3 { 27 | -webkit-animation-delay: -1.0s; 28 | animation-delay: -1.0s; 29 | } 30 | 31 | .rect4.rect4 { 32 | -webkit-animation-delay: -0.9s; 33 | animation-delay: -0.9s; 34 | } 35 | 36 | .rect5.rect5 { 37 | -webkit-animation-delay: -0.8s; 38 | animation-delay: -0.8s; 39 | } 40 | 41 | @-webkit-keyframes sk-stretchdelay { 42 | 0%, 40%, 100% { -webkit-transform: scaleY(0.4); } 43 | 20% { -webkit-transform: scaleY(1.0); } 44 | } 45 | 46 | @keyframes sk-stretchdelay { 47 | 0%, 40%, 100% { 48 | transform: scaleY(0.4); 49 | -webkit-transform: scaleY(0.4); 50 | } 51 | 20% { 52 | transform: scaleY(1.0); 53 | -webkit-transform: scaleY(1.0); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /cli/client.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack'); 2 | var historyApiFallback = require('connect-history-api-fallback') 3 | var webpackDevMiddleware = require("webpack-dev-middleware"); 4 | var webpackHotMiddleware = require("webpack-hot-middleware"); 5 | var webpackConfig = require('./webpack.dev'); 6 | var express = require("express"); 7 | var path = require("path"); 8 | var Dashboard = require('webpack-dashboard'); 9 | var DashboardPlugin = require('webpack-dashboard/plugin'); 10 | 11 | var app = express(); 12 | var compiler = webpack(webpackConfig); 13 | var dashboard = new Dashboard(); 14 | 15 | compiler.apply(new DashboardPlugin(dashboard.setData)); 16 | 17 | app.set('port', (process.env.PORT || 3005)); 18 | 19 | app.use(historyApiFallback()); 20 | 21 | app.use(webpackDevMiddleware(compiler, { 22 | noInfo: true, 23 | historyApiFallback: true, 24 | quite: true, 25 | stats: { 26 | colors: true, 27 | hash: false, 28 | version: false, 29 | chunks: false, 30 | children: false, 31 | }, 32 | publicPath: webpackConfig.output.publicPath 33 | })); 34 | 35 | app.use(webpackHotMiddleware(compiler, { 36 | log: () => {} 37 | })); 38 | 39 | app.use(express.static(path.join(__dirname, '/src/static'))); 40 | 41 | app.listen(app.get('port'), function() { 42 | console.log("Node app is running on port:" + app.get('port')) 43 | }) 44 | -------------------------------------------------------------------------------- /tests/routes/Login/LoginForm/modules/actions.spec.js: -------------------------------------------------------------------------------- 1 | import configureMockStore from 'redux-mock-store' 2 | import thunk from 'redux-thunk' 3 | import fetchMock from 'fetch-mock' 4 | import { loginAction, loginSuccess, LOGIN_SUCCESS } from '$root/routes/Login/LoginForm/modules/actions' 5 | 6 | const middlewares = [ thunk ] 7 | const mockStore = configureMockStore(middlewares) 8 | const user = { id: 1 } 9 | const expectedAction = { type: LOGIN_SUCCESS, user }; 10 | const store = mockStore({ currentUser: {} }) 11 | 12 | describe('(Actions) LoginFormContainer', () => { 13 | afterEach(() => { 14 | store.clearActions(); 15 | fetchMock.restore(); 16 | }) 17 | 18 | it('should create action to add user', () => { 19 | expect(loginSuccess(user)).to.deep.equal(expectedAction); 20 | }) 21 | 22 | describe('fetchLogin', () => { 23 | it('login successfully', () => { 24 | fetchMock.mock('http://0.0.0.0:3000/users/sign_in', 'POST', { status: 200, body: user }) 25 | 26 | return loginAction(user, store.dispatch) 27 | .then(() => { 28 | expect(store.getActions()).to.deep.equal([expectedAction]) 29 | }) 30 | }) 31 | 32 | it('login fail', () => { 33 | fetchMock.mock('http://0.0.0.0:3000/users/sign_in', 'POST', { status: 403, body: {} }) 34 | 35 | return expect(loginAction(user, store.dispatch)).to.be.rejected; 36 | }) 37 | }) 38 | }) 39 | -------------------------------------------------------------------------------- /tests/components/Forms/LoginForm/LoginForm.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { shallow } from 'enzyme' 3 | import { LoginForm } from '$root/components/Forms/LoginForm/LoginForm' 4 | 5 | describe("(Component) LoginForm", () => { 6 | let _wrapper, _props, _spy; 7 | 8 | beforeEach(() => { 9 | _props = { 10 | onSubmit: (_spy = chai.spy()) 11 | } 12 | _wrapper = shallow() 13 | }) 14 | 15 | it('Should render as ', () => { 16 | expect(_wrapper.is('form')).to.be.ok 17 | }) 18 | 19 | it('Should update email state when filling', () => { 20 | let _email = _wrapper.find('input[name="email"]') 21 | 22 | _email.simulate('change', { target: { value: 'abc@example.com' }, preventDefault() {} }) 23 | expect(_wrapper.state('email')).to.equal('abc@example.com') 24 | }) 25 | 26 | it('Should update password state when filling', () => { 27 | let _password = _wrapper.find('input[name="password"]') 28 | 29 | _password.simulate('change', { target: { value: '0123456' }, preventDefault() {} }) 30 | }) 31 | 32 | it('Should update call onSubmit with email, password', () => { 33 | let [ email, password ] = ['abc@example.com', '0123456'] 34 | 35 | _wrapper.setState({ email, password }) 36 | _wrapper.find('form').simulate('submit', { preventDefault() {} }); 37 | expect(_spy).to.have.been.called.with(email, password) 38 | }) 39 | }) 40 | -------------------------------------------------------------------------------- /cli/webpack.dev.js: -------------------------------------------------------------------------------- 1 | var webpack = require("webpack"); 2 | var path = require("path"); 3 | var rootPath = path.join(__dirname, "../"); 4 | var webpackConfig = require("./webpack.config"); 5 | 6 | webpackConfig.devtool = "eval"; 7 | 8 | webpackConfig.entry = { 9 | app: [ 10 | "webpack-hot-middleware/client", 11 | "./main.js" 12 | ], 13 | vendor: [ 14 | "react", 15 | "react-dom", 16 | "prop-types", 17 | "react-redux", 18 | "react-router", 19 | "redux", 20 | "simplestorage.js", 21 | "inflection", 22 | "bootstrap-loader/extractStyles", 23 | "font-awesome-webpack!../cli/theme/font-awesome.config.js" 24 | ], 25 | polyfills: [ 26 | "babel-polyfill", 27 | "whatwg-fetch" 28 | ] 29 | }; 30 | 31 | webpackConfig.output = { 32 | filename: "[name].js", 33 | path: path.join(rootPath, "/web"), 34 | publicPath: "/" 35 | }; 36 | 37 | webpackConfig.module.rules.push( 38 | { 39 | test: /\.(js|jsx)$/, 40 | exclude: /node_modules/, 41 | loaders: ["react-hot-loader/webpack", "babel-loader"] 42 | } 43 | ); 44 | 45 | webpackConfig.plugins.push( 46 | new webpack.NoEmitOnErrorsPlugin(), 47 | new webpack.HotModuleReplacementPlugin(), 48 | new webpack.DefinePlugin({ 49 | __DEV__: false, 50 | __DEBUG__: false, 51 | __PROD__: true, 52 | __INITIAL_STATE__: {}, 53 | "process.env": { 54 | "NODE_ENV": JSON.stringify("development"), 55 | }, 56 | }) 57 | ); 58 | 59 | module.exports = webpackConfig; 60 | -------------------------------------------------------------------------------- /cli/webpack.test.js: -------------------------------------------------------------------------------- 1 | var ExtractTextPlugin = require("extract-text-webpack-plugin"); 2 | import webpackConfig from "./webpack.dev" 3 | 4 | const karmaConfig = { 5 | basePath: "../", 6 | frameworks: ["mocha", "chai", "sinon"], 7 | files: [ 8 | "node_modules/whatwg-fetch/fetch.js", 9 | "node_modules/babel-polyfill/dist/polyfill.js", 10 | "tests/testHelper.js", 11 | "tests/**/*.spec.js" 12 | ], 13 | preprocessors: { 14 | "tests/testHelper.js": ["webpack"], 15 | "tests/**/*.spec.js": ["webpack", "coverage"] 16 | }, 17 | browsers: ["PhantomJS"], 18 | webpack: { 19 | devtool: webpackConfig.devtool, 20 | resolve: webpackConfig.resolve, 21 | module: webpackConfig.module, 22 | plugins: [ 23 | new ExtractTextPlugin("[name].css", { allChunks: true }) 24 | ], 25 | node : { fs: "empty" }, 26 | externals: { 27 | "react/addons": true, 28 | "react/lib/ExecutionEnvironment": true, 29 | "react/lib/ReactContext": true 30 | } 31 | }, 32 | webpackMiddleware: { 33 | noInfo: true, 34 | stats: { 35 | colors: true 36 | } 37 | }, 38 | reporters: ["mocha", "coverage"], 39 | singleRun: false, 40 | plugins: [ 41 | require("karma-mocha"), 42 | require("karma-phantomjs-launcher"), 43 | require("karma-chrome-launcher"), 44 | require("karma-webpack"), 45 | require("karma-chai"), 46 | require("karma-coverage"), 47 | require("karma-mocha-reporter"), 48 | require("karma-sinon") 49 | ] 50 | } 51 | 52 | module.exports = (config) => config.set(karmaConfig) 53 | -------------------------------------------------------------------------------- /src/routes/Home/UserList/UserList.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | import { connect } from 'react-redux' 4 | import isEmpty from 'lodash/isEmpty' 5 | import refetch from '$root/modules/refetch' 6 | import Loading from '$root/components/Loading' 7 | import { injectReducer } from '$root/store/reducers' 8 | import User from './components/User' 9 | import { changeActiveUser, CHANGE_ACTIVE_USER } from './modules/actions' 10 | import { activeUser } from './modules/reducers' 11 | import style from './UserList.scss' 12 | 13 | export class UserList extends Component { 14 | static contextTypes = { 15 | store: PropTypes.object 16 | } 17 | static propTypes = { 18 | users: PropTypes.array 19 | } 20 | componentWillMount() { 21 | const { store } = this.context; 22 | 23 | injectReducer(store, { key: 'activeUser', reducer: activeUser }); 24 | } 25 | setActiveUser(user) { 26 | const { dispatch } = this.props; 27 | 28 | this.setState({ user }); 29 | dispatch(changeActiveUser(user)); 30 | } 31 | shouldComponentUpdate(nextProps, nextState) { 32 | const { users } = nextProps; 33 | 34 | return !isEmpty(users); 35 | } 36 | render() { 37 | const { setActiveUser, users } = this.props; 38 | 39 | return ( 40 | 41 | Users 42 | { isEmpty(users) ? 43 | : 44 | users.map(user => this.setActiveUser(user)}/>) 45 | } 46 | 47 | 48 | ) 49 | } 50 | } 51 | 52 | export default connect()( 53 | refetch({ users: '/users' })(UserList) 54 | ) 55 | -------------------------------------------------------------------------------- /src/components/Header/Header.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | import { connect } from 'react-redux' 4 | import { Link } from 'react-router' 5 | import { signOut } from '$root/modules/currentUser' 6 | import style from './Header.scss' 7 | 8 | export class Header extends Component { 9 | static contextTypes = { 10 | router: PropTypes.object 11 | } 12 | signOut() { 13 | const { dispatch } = this.props; 14 | const { router } = this.context; 15 | 16 | signOut(dispatch) 17 | .then(() => router.push('/login')); 18 | } 19 | render() { 20 | const { currentUser } = this.props; 21 | 22 | return ( 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | {currentUser.fullName} 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | ) 45 | } 46 | } 47 | 48 | function mapStateToProps(state) { 49 | return { 50 | currentUser: state.currentUser 51 | } 52 | } 53 | 54 | export default connect(mapStateToProps)(Header) 55 | -------------------------------------------------------------------------------- /src/components/Forms/LoginForm/LoginForm.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | import style from './LoginForm.scss' 4 | 5 | export class LoginForm extends Component { 6 | state = { email: '', password: '' } 7 | fieldChange = field => e => { 8 | e.preventDefault(); 9 | this.setState({ [field]: e.target.value }) 10 | } 11 | formSubmit(e) { 12 | e.preventDefault(); 13 | const { email, password } = this.state; 14 | const { onSubmit } = this.props; 15 | 16 | onSubmit(email, password); 17 | } 18 | render() { 19 | const { email, password } = this.state; 20 | const { messages } = this.props; 21 | 22 | return ( 23 | this.formSubmit(e)}> 24 | Boilerplater 25 | 26 | { !messages ? '' : 27 | 28 | {messages.map((m, i) => { return {m}})} 29 | 30 | } 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | Login 40 | 41 | 42 | 43 | ) 44 | } 45 | } 46 | 47 | LoginForm.propTypes = { 48 | onSubmit: PropTypes.func, 49 | messages: PropTypes.array 50 | } 51 | 52 | export default LoginForm 53 | -------------------------------------------------------------------------------- /tests/routes/Login/LoginForm/LoginForm.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import thunk from 'redux-thunk' 3 | import fetchMock from 'fetch-mock' 4 | import configureMockStore from 'redux-mock-store' 5 | import { render, shallow } from 'enzyme' 6 | import { LoginFormContainer } from '$root/routes/Login/LoginForm/LoginForm' 7 | import LoginForm from '$root/components/Forms/LoginForm' 8 | 9 | const middlewares = [ thunk ] 10 | const mockStore = configureMockStore(middlewares) 11 | const store = mockStore() 12 | const user = { id: 1 } 13 | 14 | describe("(Component) LoginFormContainer", () => { 15 | let _wrapper, _props, _spy; 16 | 17 | beforeEach(() => { 18 | _wrapper = shallow(, { 19 | conext: { store } 20 | }) 21 | }) 22 | 23 | it('Should render LoginForm', () => { 24 | expect(_wrapper.containsMatchingElement()).to.be.ok 25 | }) 26 | 27 | it('Should start with empty messages', () => { 28 | expect(_wrapper.state('messages')).to.eql([]) 29 | }) 30 | 31 | it('Should redirect to root after login successfully', () => { 32 | fetchMock.mock('http://0.0.0.0:3000/users/sign_in', 'POST', { status: 200, body: { id: 1 } }) 33 | }) 34 | 35 | it('Should set error for messages state', () => { 36 | fetchMock.mock('http://0.0.0.0:3000/users/sign_in', 'POST', { status: 403, body: ['Error'] }) 37 | _wrapper.instance().formSubmit().catch(() => { 38 | expect(_wrapper.state().messages).to.eql(['Error']) 39 | }) 40 | }) 41 | 42 | it('Should pass formSubmit to LoginForm', () => { 43 | const loginForm = _wrapper.find(LoginForm) 44 | const formSubmit = _wrapper.instance().formSubmit 45 | 46 | expect(loginForm.prop('onSubmit')).to.eql(formSubmit) 47 | }) 48 | 49 | it('Should pass messages to LoginForm', () => { 50 | const loginForm = _wrapper.find(LoginForm) 51 | 52 | expect(loginForm.prop('messages')).to.equal(_wrapper.state('messages')) 53 | }) 54 | }) 55 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Redux Boilerplate 2 | 3 | ### Getting Started 4 | 5 | ```bash 6 | $ git clone https://github.com/MQuy/redux-boilerplate 7 | $ cd redux-boilerplater 8 | $ npm install 9 | $ npm run client # launch client 10 | $ npm run server # launch fake server 11 | ``` 12 | 13 | ### Command 14 | 15 | | `
← Back
{m}