├── .babelrc ├── .dockerignore ├── .editorconfig ├── .eslintignore ├── .eslintrc.json ├── .gitattributes ├── .gitignore ├── .prettierrc ├── Dockerfile ├── README.md ├── circle.yml ├── package.json ├── postcss.config.js ├── server.js ├── src ├── bootstrap.js ├── components │ ├── README.md │ ├── app │ │ └── index.js │ └── button │ │ ├── index.js │ │ └── styles.css ├── index.html.js ├── index.js ├── modules │ ├── README.md │ ├── auth │ │ ├── actions.js │ │ └── reducers.js │ ├── reducers.js │ └── user │ │ ├── actions.js │ │ ├── reducers.js │ │ └── selectors.js ├── routes.js ├── server.js ├── services │ ├── auth.js │ └── user.js ├── store │ ├── configure-store.dev.js │ ├── configure-store.js │ └── configure-store.prod.js ├── styles │ └── settings │ │ ├── _grid.css │ │ └── settings.css ├── utils │ ├── README.md │ ├── request-auth.js │ ├── request.js │ ├── routes.js │ └── server.js └── views │ ├── README.md │ ├── github │ ├── index.js │ └── user.js │ ├── home │ ├── index.js │ └── styles.css │ ├── internal-server-error │ └── index.js │ ├── login │ └── index.js │ ├── not-found │ └── index.js │ └── restrict │ └── index.js ├── stylelint.config.js ├── tests └── config.test.js ├── webpack.config.js ├── webpack.config.production.js ├── webpack.config.server.js ├── webpack.config.vendor.js ├── webpack └── _resolve.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "es2015", 5 | { 6 | "modules": false 7 | } 8 | ], 9 | "stage-0", 10 | "react" 11 | ], 12 | "env": { 13 | "development": { 14 | "presets": [ 15 | "react-hmre", "es2015", "stage-0", "react" 16 | ] 17 | }, 18 | "test": { 19 | "presets": [ 20 | "es2015", 21 | "stage-0", 22 | "react" 23 | ] 24 | }, 25 | "production": { 26 | "plugins": ["transform-react-inline-elements"] 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .git 3 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.{json,js,yml}] 12 | indent_style = space 13 | indent_size = 2 14 | 15 | [.eslintrc] 16 | indent_style = space 17 | indent_size = 2 18 | 19 | [*.md] 20 | trim_trailing_whitespace = false 21 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | tests/__tests__ 2 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "cheesecakelabs" 3 | } 4 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | npm-debug.log 3 | node_modules 4 | .nyc_output 5 | dist 6 | dist-server 7 | tests/__tests__ 8 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "tabWidth": 2, 4 | "singleQuote": true, 5 | "trailingComma": "es5", 6 | "bracketSpacing": true, 7 | "jsxBracketSameLine": false, 8 | "parser": "babylon", 9 | "semi": false 10 | } 11 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:6 2 | 3 | RUN mkdir -p /usr/src/app 4 | WORKDIR /usr/src/app 5 | 6 | COPY package.json /usr/src/app/ 7 | COPY yarn.lock /usr/src/app/ 8 | RUN yarn 9 | 10 | COPY . /usr/src/app 11 | 12 | RUN yarn build 13 | EXPOSE 3000 14 | 15 | CMD [ "yarn", "start" ] 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # react-redux-boilerplate 4 | Boilerplate project for a webapp using React/Redux architecture. 5 | 6 | 7 | ## Getting started 8 | 9 | 1. Clone this repository and delete the .git folder 10 | 1. Remove/Adapt all example components if necessary: 11 | component/button 12 | modules/counter 13 | views/home 14 | routes.js 15 | 1. Run `yarn` 16 | 1. Run `yarn dev` 17 | 1. Your application is running on `http://localhost:ENV.PORT || 3000` 18 | 19 | 20 | ## Prettier 21 | 22 | ### Sublime config 23 | - Install it globally with `npm i -g prettier` 24 | - Install jsprettier package via sublime package control 25 | 26 | [Package](https://packagecontrol.io/packages/JsPrettier) 27 | 28 | ``` 29 | { 30 | "prettier_cli_path": "/usr/local/bin/prettier", 31 | "node_path": "/usr/local/bin/node", 32 | "auto_format_on_save": true, 33 | "allow_inline_formatting": false, 34 | "custom_file_extensions": [], 35 | "additional_cli_args": {}, 36 | "max_file_size_limit": -1, 37 | } 38 | 39 | ``` 40 | 41 | ### VSCode config 42 | [Package](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode) 43 | ``` 44 | { 45 | "prettier.eslintIntegration": true, 46 | "editor.formatOnSave": true, 47 | "typescript.check.npmIsInstalled": false, 48 | "extensions.ignoreRecommendations": true 49 | } 50 | 51 | ``` 52 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | machine: 2 | node: 3 | version: 6 4 | services: 5 | - docker 6 | 7 | dependencies: 8 | override: 9 | - yarn 10 | cache_directories: 11 | - ~/.cache/yarn 12 | 13 | test: 14 | pre: 15 | - yarn lint 16 | override: 17 | - JEST_JUNIT_OUTPUT="$CIRCLE_TEST_REPORTS/junit.xml" yarn jest -- -w 1 --testResultsProcessor jest-junit 18 | 19 | deployment: 20 | production: 21 | tag: /v[0-9]+(\.[0-9]+)*(-\w+)?/ 22 | commands: 23 | - eval $(aws ecr get-login --region $AWS_REGION | sed 's|https://||') 24 | - docker build -t $DOCKER_TAG --rm=false . 25 | - docker tag $DOCKER_TAG:latest $AWS_SERVER:$CIRCLE_TAG 26 | - docker push $AWS_SERVER:$CIRCLE_TAG 27 | - pip install ecs-deploy 28 | - ecs deploy $AWS_CLUSTER $AWS_SERVICE -t $CIRCLE_TAG --timeout $AWS_ECS_TIMEOUT 29 | 30 | development: 31 | branch: staging 32 | commands: 33 | - eval $(aws ecr get-login --region $AWS_REGION | sed 's|https://||') 34 | - docker build -t $DOCKER_TAG --rm=false . 35 | - docker tag $DOCKER_TAG:latest $AWS_SERVER:$AWS_TAG_DEV 36 | - docker push $AWS_SERVER:$AWS_TAG_DEV 37 | - pip install ecs-deploy 38 | - ecs deploy $AWS_CLUSTER $AWS_SERVICE_STAGING -t $AWS_TAG_DEV --timeout $AWS_ECS_TIMEOUT 39 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-redux-boilerplate", 3 | "version": "0.1.0", 4 | "description": "Boilerplate project for a webapp using React/Redux architecture.", 5 | "main": "lib/index.js", 6 | "license": "MIT", 7 | "repository": "CheesecakeLabs/react-redux-boilerplate", 8 | "scripts": { 9 | "start": "node dist-server/server.js", 10 | "test": "yarn lint && yarn generate-view-tests && yarn jest", 11 | "generate-view-tests": "rm -fr ./tests/__tests__ && generate-view-tests", 12 | "tdd": "yarn jest -- --watch", 13 | "jest": "NODE_ENV=test jest", 14 | "dev": "NODE_ENV=development PORT=5000 node server.js", 15 | "build": "yarn build:client && yarn build:server", 16 | "build:client": "yarn build:vendor && yarn build:production", 17 | "build:production": "NODE_ENV=production webpack --colors --config webpack.config.production.js --hide-modules", 18 | "build:server": "NODE_ENV=production webpack --colors --config webpack.config.server.js --hide-modules", 19 | "build:vendor": "NODE_ENV=production webpack --colors --config webpack.config.vendor.js --hide-modules", 20 | "lint": "yarn eslint && yarn stylelint", 21 | "eslint": "eslint src stories tests *.js", 22 | "stylelint": "stylelint 'src/**/*.css'", 23 | "precommit": "yarn lint", 24 | "prepush": "npm test" 25 | }, 26 | "jest": { 27 | "verbose": false, 28 | "transform": { 29 | ".*": "babel-jest" 30 | }, 31 | "moduleNameMapper": { 32 | "^.+\\.(css)$": "identity-obj-proxy", 33 | "^.+\\.(png|svg|txt)$": "/tests/empty-module.js", 34 | "^_modules/(.*)": "/src/modules/$1", 35 | "^_components/(.*)": "/src/components/$1", 36 | "^_services/(.*)": "/src/services/$1", 37 | "^_views/(.*)": "/src/views/$1", 38 | "^_utils/(.*)": "/src/utils/$1", 39 | "^_styles/(.*)": "/src/styles/$1" 40 | }, 41 | "moduleFileExtensions": [ 42 | "js" 43 | ], 44 | "setupFiles": [ 45 | "./tests/config.test.js" 46 | ], 47 | "testPathIgnorePatterns": [ 48 | "./tests/config.test.js", 49 | "[/\\\\](dist|dist-server|node_modules|.storybook)[/\\\\]" 50 | ], 51 | "transformIgnorePatterns": [ 52 | "[/\\\\]node_modules[/\\\\].+\\.(js|jsx)$" 53 | ] 54 | }, 55 | "devDependencies": { 56 | "autoprefixer": "^6.7.7", 57 | "babel-cli": "^6.22.2", 58 | "babel-eslint": "^7.2.3", 59 | "babel-jest": "^19.0.0", 60 | "babel-loader": "^6.2.10", 61 | "babel-plugin-transform-react-inline-elements": "^6.22.0", 62 | "babel-preset-es2015": "^6.22.0", 63 | "babel-preset-react": "^6.22.0", 64 | "babel-preset-react-hmre": "^1.1.1", 65 | "babel-preset-stage-0": "^6.5.0", 66 | "babel-register": "^6.9.0", 67 | "compression-webpack-plugin": "^0.3.2", 68 | "css-loader": "^0.26.2", 69 | "enzyme": "^2.8.2", 70 | "eslint": "^3.19.0", 71 | "eslint-config-cheesecakelabs": "^2.0.3", 72 | "extract-text-webpack-plugin": "^2.1.0", 73 | "husky": "^0.13.2", 74 | "identity-obj-proxy": "^3.0.0", 75 | "jest": "^20.0.4", 76 | "jest-junit": "^1.5.1", 77 | "postcss-css-variables": "^0.7.0", 78 | "postcss-import": "^9.1.0", 79 | "postcss-loader": "^1.3.3", 80 | "postcss-nested": "^1.0.0", 81 | "react-addons-test-utils": "^15.5.1", 82 | "react-test-renderer": "^15.5.4", 83 | "redux-logger": "^2.6.1", 84 | "style-loader": "^0.14.1", 85 | "stylelint": "^7.10.1", 86 | "stylelint-config-standard": "^16.0.0", 87 | "webpack": "^3.5.5", 88 | "webpack-dev-middleware": "^1.6.1", 89 | "webpack-hot-middleware": "^2.10.0", 90 | "webpack-manifest-plugin": "^1.1.0" 91 | }, 92 | "dependencies": { 93 | "@cheesecakelabs/boilerplate": "^0.5.0", 94 | "@cheesecakelabs/fetch": "^2.1.1", 95 | "express": "^4.15.4", 96 | "express-static-gzip": "^0.3.0", 97 | "immutable": "^3.8.1", 98 | "normalize.css": "^7.0.0", 99 | "normalizr": "^3.2.3", 100 | "prop-types": "^15.5.10", 101 | "react": "^15.6.1", 102 | "react-dom": "^15.6.1", 103 | "react-immutable-proptypes": "^2.1.0", 104 | "react-redux": "^5.0.4", 105 | "react-router": "^3.0.5", 106 | "react-router-redux": "^4.0.8", 107 | "redux": "^3.7.2", 108 | "redux-define": "^1.1.1", 109 | "redux-promise-middleware": "^4.3.0", 110 | "redux-thunk": "^2.2.0", 111 | "reselect": "^3.0.1", 112 | "suitcss-components-grid": "^3.0.3", 113 | "suitcss-utils-after": "^1.0.1", 114 | "suitcss-utils-before": "^1.0.1", 115 | "suitcss-utils-offset": "^1.0.0", 116 | "suitcss-utils-size": "^2.0.0" 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack') 2 | const autoprefixer = require('autoprefixer') 3 | const postCSSImport = require('postcss-import') 4 | const postCSSNested = require('postcss-nested') 5 | const postCssCssVariables = require('postcss-css-variables')() 6 | 7 | const postCSSAutoprefixer = autoprefixer({ browsers: ['IE 9', 'iOS 7'] }) 8 | 9 | const postCssImport = postCSSImport({ 10 | addDependencyTo: webpack, 11 | }) 12 | 13 | module.exports = { 14 | plugins: [postCssImport, postCSSAutoprefixer, postCSSNested, postCssCssVariables], 15 | } 16 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | const express = require('express') 2 | const webpack = require('webpack') 3 | const webpackDevMiddleware = require('webpack-dev-middleware') 4 | const webpackHotMiddleware = require('webpack-hot-middleware') 5 | 6 | const config = require('./webpack.config') 7 | const baseHTML = require('./src/index.html') 8 | 9 | const ip = '0.0.0.0' 10 | const port = process.env.PORT || 3000 11 | const app = express() 12 | const compiler = webpack(config) 13 | 14 | app.use( 15 | webpackDevMiddleware(compiler, { 16 | publicPath: config.output.publicPath, 17 | stats: 'errors-only', 18 | }) 19 | ) 20 | 21 | app.use(webpackHotMiddleware(compiler)) 22 | 23 | // index.html links to 2 20 | 21 | 22 | ` 23 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { render } from 'react-dom' 3 | import { Router, browserHistory } from 'react-router' 4 | import { syncHistoryWithStore } from 'react-router-redux' 5 | import { Provider } from 'react-redux' 6 | 7 | import './bootstrap' 8 | import configureStore from './store/configure-store' 9 | import routes from './routes' 10 | 11 | const store = configureStore() 12 | const history = syncHistoryWithStore(browserHistory, store) 13 | 14 | const Root = () => ( 15 | 16 | 17 | 18 | ) 19 | 20 | render(, document.getElementById('root')) 21 | -------------------------------------------------------------------------------- /src/modules/README.md: -------------------------------------------------------------------------------- 1 | # Modules 2 | 3 | This section contains logical modules, they implement Redux actions and 4 | reducers, as well as any extra helper functions that might be needed in the 5 | module context. A module is a block of code for a related functionality or 6 | instance. 7 | 8 | It should be implemented with this format: 9 | 10 | module-name/ 11 | -- actions.js 12 | -- reducers.js 13 | -- .js 14 | 15 | After that include your module reducers to the root reducer file. 16 | -------------------------------------------------------------------------------- /src/modules/auth/actions.js: -------------------------------------------------------------------------------- 1 | import { defineAction } from '@cheesecakelabs/boilerplate/utils' 2 | 3 | import * as authService from '_services/auth' 4 | 5 | export const AUTH_LOGIN = defineAction('AUTH_LOGIN') 6 | 7 | export const login = (username, password) => ({ 8 | type: AUTH_LOGIN, 9 | payload: authService.login(username, password), 10 | }) 11 | -------------------------------------------------------------------------------- /src/modules/auth/reducers.js: -------------------------------------------------------------------------------- 1 | import { Map, fromJS } from 'immutable' 2 | import { createReducer } from '@cheesecakelabs/boilerplate/utils' 3 | 4 | import { AUTH_LOGIN } from './actions' 5 | 6 | const INITIAL_STATE = new Map() 7 | 8 | export const auth = createReducer(INITIAL_STATE, { 9 | [AUTH_LOGIN.FULFILLED]: (state, { payload }) => fromJS(payload), 10 | }) 11 | -------------------------------------------------------------------------------- /src/modules/reducers.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux' 2 | import { routerReducer as routing } from 'react-router-redux' 3 | import { error, loading } from '@cheesecakelabs/boilerplate/reducers' 4 | 5 | import { user, org } from './user/reducers' 6 | import { auth } from './auth/reducers' 7 | 8 | const rootReducer = combineReducers({ 9 | routing, 10 | user, 11 | org, 12 | auth, 13 | error, 14 | loading, 15 | }) 16 | 17 | export default rootReducer 18 | -------------------------------------------------------------------------------- /src/modules/user/actions.js: -------------------------------------------------------------------------------- 1 | import { defineAction } from '@cheesecakelabs/boilerplate/utils' 2 | 3 | import { get, getOrgMembers } from '_services/user' 4 | 5 | export const GET_USER = defineAction('GET_USER') 6 | export const GET_MEMBERS = defineAction('GET_MEMBERS') 7 | 8 | export const getUser = user => ({ 9 | type: GET_USER, 10 | payload: get(user), 11 | }) 12 | 13 | export const getMembers = org => ({ 14 | type: GET_MEMBERS, 15 | payload: getOrgMembers(org), 16 | meta: { org }, 17 | }) 18 | -------------------------------------------------------------------------------- /src/modules/user/reducers.js: -------------------------------------------------------------------------------- 1 | import { Map, fromJS } from 'immutable' 2 | import { normalize, schema } from 'normalizr' 3 | import { createReducer } from '@cheesecakelabs/boilerplate/utils' 4 | 5 | import { GET_USER, GET_MEMBERS } from './actions' 6 | 7 | export const INITIAL_STATE = new Map() 8 | export const userSchema = new schema.Entity('user', {}, { idAttribute: 'login' }) 9 | 10 | export const user = createReducer(INITIAL_STATE, { 11 | [GET_USER.FULFILLED]: (state, { payload }) => 12 | state.mergeDeep(normalize(payload, userSchema).entities.user), 13 | [GET_MEMBERS.FULFILLED]: (state, { payload }) => 14 | state.mergeDeep(normalize(payload, [userSchema]).entities.user), 15 | }) 16 | 17 | const getLogin = u => u.login 18 | export const org = createReducer(INITIAL_STATE, { 19 | [GET_MEMBERS.FULFILLED]: (state, { payload, meta }) => 20 | state.set(meta.org, fromJS(payload.map(getLogin))), 21 | }) 22 | -------------------------------------------------------------------------------- /src/modules/user/selectors.js: -------------------------------------------------------------------------------- 1 | import { createSelector } from 'reselect' 2 | import { Map, List } from 'immutable' 3 | 4 | const getUsers = state => state.user 5 | const getOrg = (state, org) => state.org.get(org) 6 | 7 | export const getUsersFromOrg = createSelector( 8 | getUsers, 9 | getOrg, 10 | (users = new Map(), org = new List()) => org.map(login => users.get(login)) 11 | ) 12 | -------------------------------------------------------------------------------- /src/routes.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Route } from 'react-router' 3 | 4 | import { userLoginRequired } from '_utils/routes' 5 | import App from '_components/app' 6 | import Home from '_views/home' 7 | import NotFound from '_views/not-found' 8 | import Github from '_views/github' 9 | import User from '_views/github/user' 10 | import Login from '_views/login' 11 | import Restrict from '_views/restrict' 12 | 13 | const routes = store => ( 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | ) 24 | 25 | export default routes 26 | -------------------------------------------------------------------------------- /src/server.js: -------------------------------------------------------------------------------- 1 | import express from 'express' 2 | import React from 'react' 3 | import { renderToString } from 'react-dom/server' 4 | import { match, RouterContext } from 'react-router' 5 | import { Provider } from 'react-redux' 6 | import expressStaticGzip from 'express-static-gzip' 7 | import { serverStatus, getStatus } from '@cheesecakelabs/boilerplate/utils' 8 | 9 | import { assetsPaths } from '_utils/server' 10 | 11 | import './bootstrap' 12 | import baseHTML from './index.html' 13 | import routes from './routes' 14 | import configureStore from './store/configure-store.prod' 15 | 16 | const port = process.env.PORT || 3000 17 | const app = express() 18 | 19 | serverStatus() 20 | 21 | // Ideally, you'd have a proxy server (like nginx) serving /static files 22 | app.use('/static', expressStaticGzip('dist')) 23 | 24 | app.get('*', (req, res) => { 25 | const store = configureStore() 26 | match({ routes: routes(store), location: req.url }, (err, redirect, props) => { 27 | if (redirect && !err) { 28 | res.redirect(redirect.pathname + redirect.search) 29 | } else { 30 | try { 31 | const appHtml = renderToString( 32 | 33 | 34 | 35 | ) 36 | res.status(getStatus(err, props)).send(baseHTML(appHtml, assetsPaths)) 37 | } catch (e) { 38 | // We should dump this error to a logging service (like Sentry) 39 | console.warn('render error:\n', e, '\n\n') 40 | res.status(500).send(baseHTML('', {}, assetsPaths)) 41 | } 42 | } 43 | }) 44 | console.info( 45 | `[${new Date().toLocaleString()}]`, 46 | `"${req.method} ${req.url} HTTP/${req.httpVersion}"`, 47 | res.statusCode 48 | ) 49 | }) 50 | 51 | app.listen(port, err => { 52 | if (err) { 53 | console.error(err) 54 | return 55 | } 56 | 57 | console.info('[Production] App is running on port', port) 58 | }) 59 | -------------------------------------------------------------------------------- /src/services/auth.js: -------------------------------------------------------------------------------- 1 | import request from '_utils/request-auth' 2 | 3 | export const login = (username, password) => 4 | request.post( 5 | ['auth', 'login'], 6 | {}, 7 | { 8 | username, 9 | password, 10 | } 11 | ) 12 | -------------------------------------------------------------------------------- /src/services/user.js: -------------------------------------------------------------------------------- 1 | import request from '_utils/request' 2 | 3 | export const get = user => request.get(['users', user]) 4 | export const getOrgMembers = org => request.get(['orgs', org, 'members']) 5 | -------------------------------------------------------------------------------- /src/store/configure-store.dev.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware, compose } from 'redux' 2 | import thunk from 'redux-thunk' 3 | import { browserHistory } from 'react-router' 4 | import promise from 'redux-promise-middleware' 5 | import { routerMiddleware } from 'react-router-redux' 6 | import createLogger from 'redux-logger' 7 | import { errorMiddleware } from '@cheesecakelabs/boilerplate/middlewares' 8 | 9 | import rootReducer from '_modules/reducers' 10 | 11 | const logger = createLogger({ 12 | level: 'info', 13 | collapsed: true, 14 | }) 15 | 16 | const router = routerMiddleware(browserHistory) 17 | 18 | /** 19 | * Creates a preconfigured store. 20 | */ 21 | const configureStore = preloadedState => { 22 | const store = createStore( 23 | rootReducer, 24 | preloadedState, 25 | compose(applyMiddleware(thunk, errorMiddleware, promise(), router, logger)) 26 | ) 27 | 28 | if (module.hot) { 29 | // Enable Webpack hot module replacement for reducers 30 | module.hot.accept('../modules/reducers', () => { 31 | // eslint-disable-next-line global-require 32 | const nextRootReducer = require('../modules/reducers').default 33 | store.replaceReducer(nextRootReducer) 34 | }) 35 | } 36 | 37 | return store 38 | } 39 | 40 | export default configureStore 41 | -------------------------------------------------------------------------------- /src/store/configure-store.js: -------------------------------------------------------------------------------- 1 | /* eslint global-require: 0 */ 2 | if (process.env.NODE_ENV === 'production' || process.env.NODE_ENV === 'test') { 3 | module.exports = require('./configure-store.prod') 4 | } else { 5 | module.exports = require('./configure-store.dev') 6 | } 7 | -------------------------------------------------------------------------------- /src/store/configure-store.prod.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware } from 'redux' 2 | import thunk from 'redux-thunk' 3 | import { browserHistory } from 'react-router' 4 | import { routerMiddleware } from 'react-router-redux' 5 | import promise from 'redux-promise-middleware' 6 | import { errorMiddleware } from '@cheesecakelabs/boilerplate/middlewares' 7 | 8 | import rootReducer from '_modules/reducers' 9 | 10 | const router = routerMiddleware(browserHistory) 11 | 12 | const configureStore = preloadedState => 13 | createStore( 14 | rootReducer, 15 | preloadedState, 16 | applyMiddleware(thunk, errorMiddleware, promise(), router) 17 | ) 18 | 19 | export default configureStore 20 | -------------------------------------------------------------------------------- /src/styles/settings/_grid.css: -------------------------------------------------------------------------------- 1 | @import 'suitcss-components-grid'; 2 | @import 'suitcss-utils-size'; 3 | @import 'suitcss-utils-after'; 4 | @import 'suitcss-utils-before'; 5 | -------------------------------------------------------------------------------- /src/styles/settings/settings.css: -------------------------------------------------------------------------------- 1 | @import '_grid.css'; 2 | -------------------------------------------------------------------------------- /src/utils/README.md: -------------------------------------------------------------------------------- 1 | # Utils 2 | 3 | This section contains global function utilities. Keep one function per file, 4 | with a declarative name. 5 | -------------------------------------------------------------------------------- /src/utils/request-auth.js: -------------------------------------------------------------------------------- 1 | import Fetch from '@cheesecakelabs/fetch' 2 | 3 | export default new Fetch('http://cklapp.ckl.io/api/v1/') 4 | -------------------------------------------------------------------------------- /src/utils/request.js: -------------------------------------------------------------------------------- 1 | import Fetch from '@cheesecakelabs/fetch' 2 | 3 | export default new Fetch('https://api.github.com/', null, { removeTrailingSlash: true }) 4 | -------------------------------------------------------------------------------- /src/utils/routes.js: -------------------------------------------------------------------------------- 1 | export const isLoggedIn = auth => auth && !!auth.get('key') 2 | 3 | export const userLoginRequired = store => (nextState, replace) => { 4 | const auth = store.getState().auth 5 | if (!isLoggedIn(auth)) { 6 | replace({ 7 | pathname: '/login', 8 | state: { 9 | next: { 10 | pathname: nextState.location.pathname, 11 | query: nextState.location.query, 12 | }, 13 | }, 14 | }) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/utils/server.js: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import fs from 'fs' 3 | import crypto from 'crypto' 4 | 5 | import vendorStats from '../../dist/vendor.stats.json' // eslint-disable-line import/no-unresolved 6 | import clientStats from '../../dist/production.stats.json' // eslint-disable-line import/no-unresolved 7 | 8 | const checksum = (str, algorithm = 'sha384', encoding = 'base64') => 9 | crypto 10 | .createHash(algorithm) 11 | .update(str, 'utf8') 12 | .digest(encoding) 13 | 14 | const fileSum = (file, algorithm) => checksum(fs.readFileSync(file), algorithm) 15 | 16 | const calculateSRI = (file, algorithm) => 17 | `${algorithm}-${fileSum(path.join('.', 'dist', file), algorithm)}` 18 | 19 | const getPathData = asset => ({ path: asset, sri: calculateSRI(asset, 'sha384') }) 20 | 21 | export const assetsPaths = { 22 | vendor: getPathData(vendorStats['vendor.js']), 23 | production: getPathData(clientStats['production.js']), 24 | styles: getPathData(clientStats['production.css']), 25 | } 26 | -------------------------------------------------------------------------------- /src/views/README.md: -------------------------------------------------------------------------------- 1 | # Views 2 | 3 | This section contains application specific view code. Each folder represents a 4 | view (page or sub-page) and it can contain view-specific components. 5 | 6 | It should be implemented with this format: 7 | 8 | page-name/ 9 | -- index.js 10 | -- styles.css 11 | -- tests.js 12 | -- component-name/ 13 | -- -- * same format as generic components * 14 | -------------------------------------------------------------------------------- /src/views/github/index.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types' 2 | import React, { Component } from 'react' 3 | import { Link } from 'react-router' 4 | import { connect } from 'react-redux' 5 | import { List } from 'immutable' 6 | import ImmutablePropTypes from 'react-immutable-proptypes' 7 | 8 | import { getMembers } from '_modules/user/actions' 9 | import { getUsersFromOrg } from '_modules/user/selectors' 10 | 11 | const mapStateToProps = (state, { params }) => ({ 12 | members: getUsersFromOrg(state, params.org), 13 | }) 14 | 15 | const mapDispatchToProps = { getMembers } 16 | 17 | class Github extends Component { 18 | static propTypes = { 19 | members: ImmutablePropTypes.listOf( 20 | ImmutablePropTypes.contains({ 21 | avatar_url: PropTypes.string, 22 | login: PropTypes.string, 23 | name: PropTypes.string, 24 | }) 25 | ), 26 | getMembers: PropTypes.func.isRequired, 27 | params: PropTypes.shape({ 28 | org: PropTypes.string, 29 | }).isRequired, 30 | children: PropTypes.node, 31 | } 32 | 33 | static defaultProps = { 34 | members: new List(), 35 | children: null, 36 | } 37 | 38 | componentWillMount() { 39 | this.props.getMembers(this.props.params.org) 40 | } 41 | 42 | componentWillReceiveProps({ params: { org } }) { 43 | if (org !== this.props.params.org) { 44 | this.props.getMembers(org) 45 | } 46 | } 47 | 48 | renderMember = member => ( 49 | 50 | {`${member.get('name')} 51 | 52 | ) 53 | 54 | render() { 55 | const { members, children } = this.props 56 | return ( 57 |
58 |
    59 |
  • 60 | facebook 61 |
  • 62 |
  • 63 | cheesecakelabs 64 |
  • 65 |
66 |

{members.map(this.renderMember)}

67 | {children} 68 |
69 | ) 70 | } 71 | } 72 | 73 | export default connect(mapStateToProps, mapDispatchToProps)(Github) 74 | -------------------------------------------------------------------------------- /src/views/github/user.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types' 2 | import React, { Component } from 'react' 3 | import { connect } from 'react-redux' 4 | import { Map } from 'immutable' 5 | import ImmutablePropTypes from 'react-immutable-proptypes' 6 | 7 | import { getUser } from '_modules/user/actions' 8 | 9 | const mapStateToProps = ({ user }, { params }) => ({ 10 | user: user.get(params.user), 11 | }) 12 | 13 | const mapDispatchToProps = { getUser } 14 | 15 | class User extends Component { 16 | static propTypes = { 17 | user: ImmutablePropTypes.contains({ 18 | login: PropTypes.string, 19 | avatar_url: PropTypes.string, 20 | html_url: PropTypes.string, 21 | name: PropTypes.string, 22 | email: PropTypes.string, 23 | location: PropTypes.string, 24 | }), 25 | getUser: PropTypes.func.isRequired, 26 | params: PropTypes.shape({ 27 | user: PropTypes.string, 28 | }).isRequired, 29 | } 30 | 31 | static defaultProps = { 32 | user: new Map(), 33 | } 34 | 35 | componentWillMount() { 36 | this.props.getUser(this.props.params.user) 37 | } 38 | 39 | componentWillReceiveProps({ params: { user } }) { 40 | if (user !== this.props.params.user) { 41 | this.props.getUser(user) 42 | } 43 | } 44 | 45 | render() { 46 | const { user } = this.props 47 | return ( 48 |
49 |

50 | {user.get('name')} 51 |

52 | {`${user.get('name')} 53 |

@{user.get('login')}

54 |

{user.get('email')}

55 |

{user.get('location')}

56 |
57 | ) 58 | } 59 | } 60 | 61 | export default connect(mapStateToProps, mapDispatchToProps)(User) 62 | -------------------------------------------------------------------------------- /src/views/home/index.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react' 2 | 3 | import Button, { ButtonType, ButtonTheme, ButtonSize } from '_components//button' 4 | 5 | import styles from './styles.css' 6 | 7 | const noop = () => {} 8 | 9 | class Home extends PureComponent { 10 | state = { 11 | counter: 0, 12 | } 13 | 14 | incrementCounter = () => { 15 | this.setState({ 16 | counter: this.state.counter + 1, 17 | }) 18 | } 19 | 20 | decrementCounter = () => { 21 | this.setState({ 22 | counter: this.state.counter - 1, 23 | }) 24 | } 25 | 26 | render() { 27 | const { counter } = this.state 28 | return ( 29 |
30 |
31 |
32 | 39 |
40 |
{
{counter}
}
41 |
42 | 49 |
50 |
51 |
52 |
53 |
54 | 55 |
56 |
57 | 60 |
61 |
62 | 65 |
66 |
67 |
68 | ) 69 | } 70 | } 71 | 72 | export default Home 73 | -------------------------------------------------------------------------------- /src/views/home/styles.css: -------------------------------------------------------------------------------- 1 | @import '../../styles/settings/settings.css'; 2 | 3 | .counter { 4 | color: #f0f; 5 | } 6 | 7 | .grid { 8 | composes: Grid Grid--alignCenter; 9 | } 10 | -------------------------------------------------------------------------------- /src/views/internal-server-error/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const InternalServerError = () =>

500 - Internal Server Error

4 | 5 | export default InternalServerError 6 | -------------------------------------------------------------------------------- /src/views/login/index.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types' 2 | import React, { Component } from 'react' 3 | import { connect } from 'react-redux' 4 | import ImmutablePropTypes from 'react-immutable-proptypes' 5 | import { Map, List, fromJS } from 'immutable' 6 | import { browserHistory } from 'react-router' 7 | 8 | import { login, AUTH_LOGIN } from '_modules/auth/actions' 9 | 10 | const mapStateToProps = ({ loading, error, auth, routing }) => { 11 | const nextRoute = fromJS(routing).getIn(['locationBeforeTransitions', 'state', 'next']) 12 | return { 13 | isLoading: loading.get(AUTH_LOGIN.ACTION), 14 | errors: error.get(AUTH_LOGIN.ACTION), 15 | auth, 16 | nextRoute: nextRoute && nextRoute.toJS(), 17 | } 18 | } 19 | 20 | const mapDispatchToProps = { login } 21 | 22 | class User extends Component { 23 | static propTypes = { 24 | login: PropTypes.func, 25 | isLoading: PropTypes.bool, 26 | errors: ImmutablePropTypes.map, 27 | nextRoute: PropTypes.shape({ 28 | pathname: PropTypes.string, 29 | query: PropTypes.object, 30 | }), 31 | } 32 | 33 | static defaultProps = { 34 | login: () => {}, 35 | isLoading: false, 36 | errors: new Map(), 37 | nextRoute: undefined, 38 | } 39 | 40 | state = { 41 | username: '', 42 | password: '', 43 | } 44 | 45 | componentWillReceiveProps({ auth, nextRoute }) { 46 | if (auth.get('key')) { 47 | console.info('🙋 parabéns!') 48 | if (nextRoute) { 49 | browserHistory.replace(nextRoute) 50 | } 51 | } 52 | } 53 | 54 | onSubmit = e => { 55 | e.preventDefault() 56 | const { username, password } = this.state 57 | this.props.login(username, password) 58 | } 59 | 60 | getErrors = () => { 61 | const { errors, isLoading } = this.props 62 | if (errors.size > 0 && !isLoading) { 63 | return errors.get('non_field_errors', new List(['unknown error'])).first() 64 | } 65 | return null 66 | } 67 | 68 | handleInput = event => { 69 | const { name, value } = event.target 70 | this.setState({ 71 | [name]: value, 72 | }) 73 | } 74 | 75 | render() { 76 | const { username, password } = this.state 77 | const { isLoading } = this.props 78 | return ( 79 |
80 |

Login:

81 |
82 |

Username:

83 | 84 |
85 |
86 |

Password:

87 | 88 |
89 | 92 | {this.getErrors()} 93 |
94 | ) 95 | } 96 | } 97 | 98 | export default connect(mapStateToProps, mapDispatchToProps)(User) 99 | -------------------------------------------------------------------------------- /src/views/not-found/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const NotFoundPage = () =>

404 - Page Not Found

4 | 5 | export default NotFoundPage 6 | -------------------------------------------------------------------------------- /src/views/restrict/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const Restrict = () =>

You should only see this if you are logged in.

4 | 5 | export default Restrict 6 | -------------------------------------------------------------------------------- /stylelint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = require('@cheesecakelabs/boilerplate').stylelint 2 | -------------------------------------------------------------------------------- /tests/config.test.js: -------------------------------------------------------------------------------- 1 | import { shallow, render, mount } from 'enzyme' 2 | 3 | global.shallow = shallow 4 | global.render = render 5 | global.mount = mount 6 | 7 | // Fail tests on any console error 8 | console.error = message => { 9 | throw new Error(message) 10 | } 11 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | const webpack = require('webpack') 4 | const ExtractTextPlugin = require('extract-text-webpack-plugin') 5 | 6 | const resolve = require('./webpack/_resolve.js') 7 | 8 | module.exports = { 9 | devtool: 'cheap-module-eval-source-map', 10 | entry: ['webpack-hot-middleware/client', './src/index'], 11 | output: { 12 | path: path.join(__dirname, 'dist'), 13 | filename: 'bundle.js', 14 | publicPath: '/static/', 15 | }, 16 | plugins: [ 17 | new webpack.HotModuleReplacementPlugin(), 18 | new webpack.DefinePlugin({ 19 | 'process.env': { 20 | NODE_ENV: JSON.stringify('development'), 21 | DEVTOOLS_WINDOW: JSON.stringify(process.env.DEVTOOLS_WINDOW), 22 | }, 23 | }), 24 | new ExtractTextPlugin('styles.css'), 25 | ], 26 | resolve, 27 | module: { 28 | rules: [ 29 | { 30 | test: /\.js$/, 31 | loader: 'babel-loader', 32 | exclude: /node_modules/, 33 | }, 34 | { 35 | test: /\.css$/, 36 | include: /node_modules/, 37 | loader: ExtractTextPlugin.extract({ 38 | fallback: 'style-loader', 39 | use: [ 40 | { 41 | loader: 'css-loader', 42 | options: { 43 | modules: false, 44 | localIdentName: '[name]__[local]___[hash:base64:5]', 45 | }, 46 | }, 47 | ], 48 | }), 49 | }, 50 | { 51 | test: /\.css$/, 52 | exclude: /node_modules/, 53 | loader: ExtractTextPlugin.extract({ 54 | fallback: 'style-loader', 55 | use: [ 56 | { 57 | loader: 'css-loader', 58 | options: { 59 | modules: true, 60 | importLoaders: 2, 61 | localIdentName: '[name]__[local]___[hash:base64:5]', 62 | }, 63 | }, 64 | { 65 | loader: 'postcss-loader', 66 | }, 67 | ], 68 | }), 69 | }, 70 | ], 71 | }, 72 | } 73 | -------------------------------------------------------------------------------- /webpack.config.production.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | const webpack = require('webpack') 4 | const ExtractTextPlugin = require('extract-text-webpack-plugin') 5 | const CompressionPlugin = require('compression-webpack-plugin') 6 | const ManifestPlugin = require('webpack-manifest-plugin') 7 | 8 | const vendorManifest = require('./dist/vendor-manifest.json') // eslint-disable-line import/no-unresolved 9 | const resolve = require('./webpack/_resolve.js') 10 | 11 | module.exports = { 12 | devtool: 'source-map', 13 | entry: { production: './src/index' }, 14 | output: { 15 | path: path.join(__dirname, 'dist'), 16 | filename: '[name].[chunkhash].js', 17 | publicPath: '/static/', 18 | }, 19 | plugins: [ 20 | new webpack.optimize.ModuleConcatenationPlugin(), 21 | new webpack.DefinePlugin({ 22 | 'process.env': { 23 | NODE_ENV: JSON.stringify('production'), 24 | }, 25 | }), 26 | new webpack.optimize.UglifyJsPlugin({ 27 | sourceMap: true, 28 | compress: { 29 | screw_ie8: true, 30 | warnings: false, 31 | }, 32 | }), 33 | new ExtractTextPlugin('[name].[chunkhash].css'), 34 | new webpack.DllReferencePlugin({ 35 | manifest: vendorManifest, 36 | }), 37 | new CompressionPlugin({ 38 | test: /\.(js|css)$/, 39 | threshold: 10240, 40 | }), 41 | new ManifestPlugin({ 42 | fileName: 'production.stats.json', 43 | }), 44 | ], 45 | resolve, 46 | module: { 47 | rules: [ 48 | { 49 | test: /\.js$/, 50 | loader: 'babel-loader', 51 | exclude: /node_modules/, 52 | }, 53 | { 54 | test: /\.css$/, 55 | include: /node_modules/, 56 | loader: ExtractTextPlugin.extract({ 57 | fallback: 'style-loader', 58 | use: [ 59 | { 60 | loader: 'css-loader', 61 | options: { 62 | modules: false, 63 | minimize: true, 64 | }, 65 | }, 66 | ], 67 | }), 68 | }, 69 | { 70 | test: /\.css$/, 71 | exclude: /node_modules/, 72 | loader: ExtractTextPlugin.extract({ 73 | fallback: 'style-loader', 74 | use: [ 75 | { 76 | loader: 'css-loader', 77 | options: { 78 | modules: true, 79 | minimize: true, 80 | importLoaders: 2, 81 | localIdentName: '[name]__[local]___[hash:base64:5]', 82 | }, 83 | }, 84 | { 85 | loader: 'postcss-loader', 86 | }, 87 | ], 88 | }), 89 | }, 90 | ], 91 | }, 92 | } 93 | -------------------------------------------------------------------------------- /webpack.config.server.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | const webpack = require('webpack') 4 | const ExtractTextPlugin = require('extract-text-webpack-plugin') 5 | 6 | const resolve = require('./webpack/_resolve.js') 7 | 8 | module.exports = { 9 | entry: './src/server.js', 10 | target: 'node', 11 | output: { 12 | path: path.join(__dirname, 'dist-server'), 13 | filename: 'server.js', 14 | publicPath: '/static/', 15 | }, 16 | resolve, 17 | plugins: [ 18 | new webpack.DefinePlugin({ 19 | fetch: () => new Promise(() => {}), 20 | }), 21 | new ExtractTextPlugin('styles.css'), 22 | ], 23 | module: { 24 | rules: [ 25 | { 26 | test: /\.js$/, 27 | loader: 'babel-loader', 28 | exclude: /node_modules/, 29 | options: { 30 | presets: ['react'], 31 | }, 32 | }, 33 | { 34 | test: /\.css$/, 35 | include: /node_modules/, 36 | loader: ExtractTextPlugin.extract({ 37 | fallback: 'style-loader', 38 | use: [ 39 | { 40 | loader: 'css-loader', 41 | options: { 42 | modules: false, 43 | }, 44 | }, 45 | ], 46 | }), 47 | }, 48 | { 49 | test: /\.css$/, 50 | exclude: /node_modules/, 51 | loader: ExtractTextPlugin.extract({ 52 | fallback: 'style-loader', 53 | use: [ 54 | { 55 | loader: 'css-loader', 56 | options: { 57 | modules: true, 58 | importLoaders: 2, 59 | localIdentName: '[name]__[local]___[hash:base64:5]', 60 | }, 61 | }, 62 | { 63 | loader: 'postcss-loader', 64 | }, 65 | ], 66 | }), 67 | }, 68 | ], 69 | }, 70 | } 71 | -------------------------------------------------------------------------------- /webpack.config.vendor.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | const webpack = require('webpack') 4 | const CompressionPlugin = require('compression-webpack-plugin') 5 | const ManifestPlugin = require('webpack-manifest-plugin') 6 | 7 | module.exports = { 8 | entry: { 9 | vendor: [ 10 | '@cheesecakelabs/fetch', 11 | 'immutable', 12 | 'react', 13 | 'react-dom', 14 | 'react-immutable-proptypes', 15 | 'react-redux', 16 | 'react-router', 17 | 'react-router-redux', 18 | 'redux', 19 | 'redux-define', 20 | 'redux-promise-middleware', 21 | 'redux-thunk', 22 | ], 23 | }, 24 | 25 | output: { 26 | filename: '[name].[chunkhash].js', 27 | path: path.join(__dirname, 'dist'), 28 | library: '[name]_lib', 29 | }, 30 | 31 | plugins: [ 32 | new webpack.optimize.ModuleConcatenationPlugin(), 33 | new webpack.DefinePlugin({ 34 | 'process.env': { 35 | NODE_ENV: JSON.stringify('production'), 36 | }, 37 | }), 38 | new webpack.optimize.UglifyJsPlugin({ 39 | sourceMap: true, 40 | compress: { 41 | screw_ie8: true, 42 | warnings: false, 43 | }, 44 | }), 45 | new webpack.LoaderOptionsPlugin({ 46 | minimize: true, 47 | }), 48 | new webpack.DllPlugin({ 49 | path: path.join('dist', '[name]-manifest.json'), 50 | name: '[name]_lib', 51 | }), 52 | new CompressionPlugin({ 53 | test: /\.(js|css)$/, 54 | threshold: 10240, 55 | }), 56 | new ManifestPlugin({ 57 | fileName: 'vendor.stats.json', 58 | }), 59 | ], 60 | } 61 | -------------------------------------------------------------------------------- /webpack/_resolve.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | module.exports = { 4 | modules: [ 5 | path.join(__dirname, '..', 'src'), 6 | path.join(__dirname, '..', 'node_modules'), 7 | ], 8 | extensions: ['.js'], 9 | alias: { 10 | _modules: path.resolve(__dirname, '..', 'src/modules/'), 11 | _components: path.resolve(__dirname, '..', 'src/components/'), 12 | _services: path.resolve(__dirname, '..', 'src/services/'), 13 | _views: path.resolve(__dirname, '..', 'src/views/'), 14 | _utils: path.resolve(__dirname, '..', 'src/utils/'), 15 | _styles: path.resolve(__dirname, '..', 'src/styles/'), 16 | }, 17 | } 18 | --------------------------------------------------------------------------------