├── .editorconfig ├── .gitignore ├── .travis.yml ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── bin ├── clean.js └── server.js ├── dist └── .gitignore ├── docs └── examle-hrm.gif ├── package.json ├── src ├── client │ ├── Root.js │ └── index.js ├── common │ ├── actions │ │ ├── Auth.js │ │ └── Github.js │ ├── components │ │ ├── Error.js │ │ ├── Github.js │ │ ├── Layout │ │ │ ├── Footer.js │ │ │ ├── Header.js │ │ │ └── index.js │ │ ├── NotFound.js │ │ └── Page │ │ │ ├── About.js │ │ │ └── Home │ │ │ ├── assets │ │ │ ├── fonts │ │ │ │ └── Orbitron-Regular.ttf │ │ │ ├── images │ │ │ │ ├── astronaut.gif │ │ │ │ ├── background.gif │ │ │ │ └── ground.png │ │ │ └── styles.styl │ │ │ └── index.js │ ├── constants │ │ ├── actions │ │ │ ├── Auth.js │ │ │ └── Github.js │ │ └── application.js │ ├── containers │ │ ├── Github │ │ │ └── index.js │ │ ├── Layout │ │ │ └── index.js │ │ └── User │ │ │ ├── Login.js │ │ │ └── Profile.js │ ├── helpers │ │ └── routes.js │ ├── reducers │ │ ├── auth.js │ │ ├── github.js │ │ └── index.js │ ├── routes.js │ └── store.js └── server │ ├── containers │ └── Html │ │ └── index.js │ └── index.js ├── static └── assets │ └── .gitignore ├── tests ├── e2e │ └── Homepage.js ├── entry.js ├── karma.config.js └── unit │ └── components │ ├── Layout │ └── index.js │ ├── NotFound.js │ └── Page │ ├── About.js │ └── Home.js └── webpack ├── client ├── config.js └── watch.js ├── common.config.js ├── isomorphic.config.js ├── server ├── config.js └── watch.js └── tests.config.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig: http://EditorConfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | indent_style = space 11 | indent_size = 2 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .idea/ 3 | node_modules/ 4 | webpack-assets.json 5 | webpack-stats.json 6 | *.log 7 | *.map 8 | .screenshots/ 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - 8 5 | 6 | before_script: 7 | - npm install 8 | 9 | script: 10 | - npm run test 11 | - npm run lint 12 | - npm run deploy & sleep 120 13 | - npm run test:e2e 14 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | If you have any idea to contribute this project, please follow these simple steps: 4 | 5 | 1. Open a new issue so we can discuss more about your idea 6 | 7 | 2. Fork this repo and implement your solution: 8 | 9 | * Commit messages should be expressive 10 | 11 | * Don't forget to run test before pull request 12 | 13 | 3. Create a pull request, you should use something like ```fixed [number]``` 14 | where number is the issues number from step 1 so it can be automatically closed. 15 | 16 | 4. Thank you for your contribution 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Ufocoder 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Redux universal boilerplate 2 | =========================== 3 | 4 | Boilerplate for react universal application building on flux architecture based on redux implementation. 5 | 6 | [![Build Status](https://travis-ci.org/ufocoder/redux-universal-boilerplate.svg?branch=master)](https://travis-ci.org/ufocoder/redux-universal-boilerplate) 7 | [![Dependencies](https://david-dm.org/ufocoder/redux-universal-boilerplate.svg)](https://david-dm.org/ufocoder/redux-universal-boilerplate) 8 | [![devDependencies Status](https://david-dm.org/ufocoder/redux-universal-boilerplate/dev-status.svg)](https://david-dm.org/ufocoder/redux-universal-boilerplate?type=dev) 9 | [![MIT License](https://img.shields.io/npm/l/check-dependencies.svg?style=flat-square)](http://opensource.org/licenses/MIT) 10 | 11 | Boilerplate based on: 12 | 13 | * [ExpressJS](http://expressjs.com) 14 | * [React](https://github.com/reactjs/) 15 | * [React-router](https://github.com/reactjs/react-router) 16 | * [React-helmet](https://github.com/nfl/react-helmet) 17 | * [React-redux](https://github.com/reactjs/react-redux) 18 | * [Redux](https://github.com/reactjs/redux) 19 | * [Redial](https://github.com/markdalgleish/redial) 20 | * [BabelJS](https://babeljs.io) 21 | * [Webpack 2](https://webpack.github.io/) 22 | * and etc. 23 | 24 | # Features 25 | 26 | * es6/es7 syntax 27 | * Testing environment 28 | * Server and client side rendering 29 | * Routing on client and server sides 30 | * Hot module replacement 31 | * Html layout as `react` component 32 | * Not Found page with 404 HTTP status 33 | * Stubs of media asset modules for server bundle 34 | * CSS preprocessors support: [SASS](http://sass-lang.com/), [Stylus](http://stylus-lang.com/) 35 | 36 | # Installation 37 | 38 | ```bash 39 | git clone https://github.com/ufocoder/redux-universal-boilerplate.git 40 | cd redux-universal-boilerplate 41 | 42 | npm install 43 | ``` 44 | 45 | # Production 46 | 47 | To build and start project for production run in console: 48 | 49 | ```bash 50 | npm run deploy 51 | ``` 52 | 53 | # Development 54 | 55 | There're two ways to work with boilerplate 56 | 57 | 1) Build once and then run bundles: 58 | 59 | ```bash 60 | npm run build 61 | npm run start 62 | ``` 63 | 64 | 2) Developing in `watch` mode: 65 | 66 | ```bash 67 | npm run watch 68 | ``` 69 | 70 | # Watch mode 71 | 72 | When you run in console: 73 | 74 | ```bash 75 | npm run watch 76 | ``` 77 | 78 | Two web servers will be run: 79 | 80 | * web-server for backend started by `server` entry point on 8000 default port 81 | * webpack-dev-server with `client` bundle working on 8080 default port 82 | 83 | For working with hot reloading mode, open your in browser http://localhost:8080/, all `non-assets` http request to will be proxy to backend endpoint 84 | 85 | ![Example of hot reload mode](docs/examle-hrm.gif) 86 | 87 | # Testing environment 88 | 89 | * [Karma](https://karma-runner.github.io/) 90 | * [Karma-chrome-launcher](https://github.com/karma-runner/karma-chrome-launcher) 91 | * [Mocha](https://mochajs.org/) 92 | * [Chai](http://chaijs.com/) 93 | * [Sinon](http://sinonjs.org/) 94 | * [Enzyme](https://github.com/airbnb/enzyme) 95 | * [Puppeteer](https://github.com/GoogleChrome/puppeteer) 96 | * and etc. 97 | 98 | There is a watch mode for testing: 99 | 100 | ```bash 101 | npm run test:watch 102 | ``` 103 | 104 | # Attention 105 | Don't forget that there's universal (isomorphic) boilerplate so you need to consider this fact when you will develop your UI application. 106 | 107 | Remember that you should use browser objects (like window, document and etc.) in ReactJS [componentDidMount](https://facebook.github.io/react/docs/component-specs.html#mounting-componentdidmount) Method 108 | 109 | # Contributing 110 | 111 | I would be thankful for your [issues](https://github.com/ufocoder/redux-universal-boilerplate/issues) and [pull requests](https://github.com/ufocoder/redux-universal-boilerplate/pulls) 112 | 113 | # License 114 | 115 | MIT license. Copyright © 2016, Ufocoder. All rights reserved. 116 | -------------------------------------------------------------------------------- /bin/clean.js: -------------------------------------------------------------------------------- 1 | let clientBundlePath = require('../webpack/client/config').output.path 2 | let serverBundlePath = require('../webpack/server/config').output.path 3 | let path = require('path') 4 | let del = require('del') 5 | 6 | del.sync([ 7 | path.resolve(path.join(clientBundlePath, '**')), 8 | path.resolve(path.join(serverBundlePath, '**')), 9 | '!' + path.resolve(path.join(clientBundlePath)), 10 | '!' + path.resolve(path.join(serverBundlePath)), 11 | '!' + path.resolve(path.join(clientBundlePath, '.gitignore')), 12 | '!' + path.resolve(path.join(serverBundlePath, '.gitignore')) 13 | ]) 14 | -------------------------------------------------------------------------------- /bin/server.js: -------------------------------------------------------------------------------- 1 | let WebpackIsomorphicTools = require('webpack-isomorphic-tools') 2 | let config = require('../webpack/isomorphic.config') 3 | 4 | let projectBasePath = require('path').resolve(__dirname) 5 | let devMode = process.env.NODE_ENV !== 'production' 6 | 7 | let webpackIsomorphicTools = new WebpackIsomorphicTools(config) 8 | .server(projectBasePath, () => { 9 | require('../dist/server') 10 | }) 11 | 12 | global.__CLIENT__ = false 13 | global.__SERVER__ = true 14 | global.__DEV__ = devMode 15 | global.webpackIsomorphicTools = webpackIsomorphicTools 16 | -------------------------------------------------------------------------------- /dist/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /docs/examle-hrm.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ufocoder/redux-universal-boilerplate/ac036727deb2250ca175aa8f725e5f34aac0437b/docs/examle-hrm.gif -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redux-universal-boilerplate", 3 | "description": "Boilerplate of an isomorphic (universal) react application", 4 | "author": "Sergey Ivanov ", 5 | "license": "MIT", 6 | "version": "0.1.0", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/ufocoder/redux-universal-boilerplate" 10 | }, 11 | "homepage": "https://github.com/ufocoder/redux-universal-boilerplate", 12 | "keywords": [ 13 | "react", 14 | "react-router", 15 | "react-redux", 16 | "redux", 17 | "universal", 18 | "isomorphic", 19 | "webpack", 20 | "express", 21 | "starter-kit", 22 | "boilerplate", 23 | "babel" 24 | ], 25 | "main": "bin/server.js", 26 | "scripts": { 27 | "clean": "node ./bin/clean.js", 28 | "start": "nodemon ./bin/server.js", 29 | "prebuild": "npm-run-all clean lint test", 30 | "build": "npm-run-all --parallel build:*", 31 | "build:server": "webpack --config webpack/server/config.js", 32 | "build:client": "webpack --config webpack/client/config.js", 33 | "deploy": "cross-env NODE_ENV=production npm-run-all build start", 34 | "prewatch": "npm run clean", 35 | "watch": "npm-run-all --parallel watch:*", 36 | "watch:wait": "just-wait --pattern \"dist/*.js\" && npm run start", 37 | "watch:server": "webpack --config webpack/server/watch.js", 38 | "watch:client": "webpack-dev-server --config webpack/client/watch.js", 39 | "test": "karma start ./tests/karma.config.js --single-run", 40 | "test:watch": "karma start ./tests/karma.config.js no-single-run", 41 | "test:e2e": "mkdir -p .screenshots && mocha --timeout=20000 ./tests/e2e", 42 | "lint": "standard ./src/**/*.js ./bin/**/*.js ./webpack/**/*.js ./tests/**/*.js", 43 | "prettier": "prettier ./src/**/*.js ./bin/**/*.js ./webpack/**/*.js ./tests/**/*.js" 44 | }, 45 | "dependencies": { 46 | "axios": "^0.16.1", 47 | "babel": "^6.23.0", 48 | "babel-core": "^6.23.1", 49 | "babel-loader": "^7.0.0", 50 | "babel-plugin-transform-decorators-legacy": "^1.3.4", 51 | "babel-polyfill": "^6.23.0", 52 | "babel-preset-es2015": "^6.22.0", 53 | "babel-preset-react": "^6.23.0", 54 | "babel-preset-react-hmre": "^1.1.1", 55 | "babel-preset-stage-0": "^6.22.0", 56 | "body-parser": "^1.17.1", 57 | "css-loader": "^0.28.1", 58 | "express": "^4.15.2", 59 | "extract-text-webpack-plugin": "^3.0.1", 60 | "file-loader": "^1.1.5", 61 | "json-loader": "^0.5.4", 62 | "locale": "^0.1.0", 63 | "lodash": "^4.17.4", 64 | "node-noop": "^1.0.0", 65 | "node-sass": "^4.5.0", 66 | "prop-types": "^15.5.8", 67 | "react": "^16.0.0", 68 | "react-dom": "^16.0.0", 69 | "react-helmet": "^5.0.3", 70 | "react-redux": "^5.0.3", 71 | "react-router": "^3.0.2", 72 | "react-router-redux": "^4.0.8", 73 | "redial": "^0.5.0", 74 | "redux": "^3.6.0", 75 | "redux-thunk": "^2.2.0", 76 | "sass-loader": "^6.0.3", 77 | "semantic-ui-css": "^2.2.9", 78 | "serialize-javascript": "^1.3.0", 79 | "style-loader": "^0.19.0", 80 | "stylus": "^0.54.5", 81 | "stylus-loader": "^3.0.1", 82 | "url-loader": "^0.6.2", 83 | "webpack-isomorphic-tools": "^3.0.1" 84 | }, 85 | "devDependencies": { 86 | "babel-eslint": "^8.0.1", 87 | "chai": "^4.1.2", 88 | "cross-env": "^5.0.5", 89 | "del": "^3.0.0", 90 | "enzyme": "^3.1.0", 91 | "enzyme-adapter-react-16": "^1.0.1", 92 | "es6-map": "^0.1.5", 93 | "es6-set": "^0.1.5", 94 | "just-wait": "^1.0.11", 95 | "karma": "^1.5.0", 96 | "karma-chai": "^0.1.0", 97 | "karma-chrome-launcher": "^2.2.0", 98 | "karma-coverage": "^1.1.1", 99 | "karma-mocha": "^1.3.0", 100 | "karma-sinon": "^1.0.4", 101 | "karma-sourcemap-loader": "^0.3.7", 102 | "karma-webpack": "^2.0.2", 103 | "mocha": "^4.0.1", 104 | "nodemon": "^1.11.0", 105 | "npm-run-all": "^4.0.2", 106 | "prettier": "^1.7.4", 107 | "puppeteer": "^0.11.0", 108 | "react-hot-loader": "^3.0.0-beta.6", 109 | "react-test-renderer": "^16.0.0", 110 | "sinon": "^4.0.1", 111 | "standard": "^10.0.3", 112 | "webpack": "^3.6.0", 113 | "webpack-dev-server": "^2.4.1", 114 | "webpack-node-externals": "^1.4.3" 115 | }, 116 | "engines": { 117 | "node": ">=6.9.0" 118 | }, 119 | "standard": { 120 | "env": [ 121 | "mocha" 122 | ] 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/client/Root.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { AppContainer } from 'react-hot-loader' 3 | import { trigger } from 'redial' 4 | import { Router, browserHistory, match } from 'react-router' 5 | import { syncHistoryWithStore } from 'react-router-redux' 6 | import { Provider } from 'react-redux' 7 | 8 | import configureStore from 'src/common/store' 9 | import routesContainer from 'src/common/routes' 10 | 11 | const initialState = window.__INITIAL_STATE__ 12 | const store = configureStore(browserHistory, initialState) 13 | const history = syncHistoryWithStore(browserHistory, store) 14 | const routes = routesContainer(store) 15 | const { dispatch } = store 16 | 17 | history.listen((location) => { 18 | match({ routes, location, history }, (error, redirectLocation, renderProps) => { 19 | if (error) { 20 | console.error(error) 21 | } 22 | if (renderProps) { 23 | const { components } = renderProps 24 | const locals = { 25 | path: renderProps.location.pathname, 26 | query: renderProps.location.query, 27 | params: renderProps.params, 28 | store, 29 | dispatch 30 | } 31 | 32 | if (window.__INITIAL_STATE__) { 33 | delete window.__INITIAL_STATE__ 34 | } else { 35 | trigger('fetch', components, locals) 36 | } 37 | 38 | trigger('defer', components, locals) 39 | } 40 | }) 41 | }) 42 | 43 | export default class Root extends Component { 44 | render () { 45 | return ( 46 | 47 | 48 | 49 | {routes} 50 | 51 | 52 | 53 | ) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/client/index.js: -------------------------------------------------------------------------------- 1 | /* global window, __PRODUCTION__ */ 2 | /* eslint no-console: [2, { allow: ["error"] }] */ 3 | 4 | import React from 'react' 5 | import ReactDOM from 'react-dom' 6 | import {CONTAINER_ID} from 'src/common/constants/application' 7 | import Root from './Root' 8 | 9 | const reactRoot = window.document.getElementById(CONTAINER_ID) 10 | const render = () => { 11 | ReactDOM.hydrate(, reactRoot) 12 | } 13 | 14 | render() 15 | 16 | if (!__PRODUCTION__ && module.hot) { 17 | module.hot.accept(render) 18 | } 19 | 20 | if (__PRODUCTION__) { 21 | if (!reactRoot.firstChild || 22 | !reactRoot.firstChild.attributes || 23 | !reactRoot.firstChild.attributes['data-react-checksum']) { 24 | console.error( 25 | 'Server-side React render was discarded. Make sure that ' + 26 | 'your initial render does not contain any client-side code.' 27 | ) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/common/actions/Auth.js: -------------------------------------------------------------------------------- 1 | import { 2 | LOGIN_SUCCESS, 3 | LOGIN_FAILURE, 4 | LOGOUT_SUCCESS, 5 | LOGOUT_FAILURE 6 | } from 'src/common/constants/actions/Auth' 7 | 8 | import { 9 | TEST_USERNAME, 10 | TEST_PASSWORD 11 | } from 'src/common/constants/application' 12 | 13 | const fakeUser = { 14 | username: 'demo', 15 | token: 'ojn2jr3wrefj' 16 | } 17 | 18 | /** 19 | * @param {string} username Username value for `login` request 20 | * @param {string} password Password value for `login` request 21 | * 22 | * @return {function} Login async action 23 | */ 24 | export function login (username, password) { 25 | return async (dispatch) => { 26 | try { 27 | const validCredentials = username !== TEST_USERNAME && password !== TEST_PASSWORD 28 | 29 | if (validCredentials) { 30 | throw new Error('Bad credentials') 31 | } 32 | 33 | dispatch({ 34 | type: LOGIN_SUCCESS, 35 | result: fakeUser 36 | }) 37 | } catch (error) { 38 | dispatch({ 39 | type: LOGIN_FAILURE, 40 | error: error.message 41 | }) 42 | } 43 | } 44 | } 45 | 46 | /** 47 | * @return {function} Logout async action 48 | */ 49 | export function logout () { 50 | return async (dispatch) => { 51 | try { 52 | dispatch({ 53 | type: LOGOUT_SUCCESS 54 | }) 55 | } catch (error) { 56 | dispatch({ 57 | type: LOGOUT_FAILURE, 58 | error: error.message 59 | }) 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/common/actions/Github.js: -------------------------------------------------------------------------------- 1 | import { 2 | RESET_TRENDS, 3 | FETCH_TRENDS_LOADING, 4 | FETCH_TRENDS_SUCCESS, 5 | FETCH_TRENDS_FAILURE 6 | } from 'src/common/constants/actions/Github' 7 | 8 | import axios from 'axios' 9 | 10 | const URL = 'https://api.github.com/search/repositories' + 11 | '?q=react' + 12 | '&created:>2016-01-01' + 13 | '&sort=stars' 14 | 15 | /** 16 | * @return {function} Fetch trends async action 17 | */ 18 | export function fetchTrends () { 19 | return async (dispatch) => { 20 | try { 21 | dispatch({ 22 | type: FETCH_TRENDS_LOADING 23 | }) 24 | 25 | const response = await axios.get(URL) 26 | 27 | dispatch({ 28 | type: FETCH_TRENDS_SUCCESS, 29 | trends: response.data.items 30 | }) 31 | } catch (error) { 32 | dispatch({ 33 | type: FETCH_TRENDS_FAILURE, 34 | error: error.message || 'Unknown error occured' 35 | }) 36 | } 37 | } 38 | } 39 | 40 | /** 41 | * @return {function} Reset trends action 42 | */ 43 | export function resetTrends () { 44 | return { 45 | type: RESET_TRENDS 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/common/components/Error.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | const Error = (props) => ( 5 |
6 | {props.title && ( 7 |
8 | {props.title } 9 |
10 | )} 11 | {props.message && ( 12 |

{props.message}

13 | )} 14 |
15 | ) 16 | 17 | Error.propTypes = { 18 | title: PropTypes.string, 19 | message: PropTypes.string 20 | } 21 | -------------------------------------------------------------------------------- /src/common/components/Github.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import Helmet from 'react-helmet' 4 | import Error from './Error' 5 | 6 | const Github = (props) => { 7 | const {error, loading, trends} = props 8 | 9 | if (error) { 10 | return ( 11 | 12 | ) 13 | } 14 | 15 | if (loading) { 16 | return ( 17 |
loading..
18 | ) 19 | } 20 | 21 | return ( 22 |
23 | 24 | 25 |

React Github trends

26 | 27 | {trends.map(function (trend, key) { 28 | return ( 29 |
30 | 31 |
32 | {trend.full_name} 33 |
{trend.stargazers_count}
34 |
35 |
36 | ) 37 | })} 38 |
39 | ) 40 | } 41 | 42 | Github.propTypes = { 43 | trends: PropTypes.array, 44 | loading: PropTypes.bool, 45 | error: PropTypes.string 46 | } 47 | 48 | export default Github 49 | -------------------------------------------------------------------------------- /src/common/components/Layout/Footer.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const Header = () => ( 4 |
5 |
6 | © {(new Date()).getFullYear()}, All rights reserved 7 |
8 |
9 | ) 10 | 11 | export default Header 12 | -------------------------------------------------------------------------------- /src/common/components/Layout/Header.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import {IndexLink, Link} from 'react-router' 4 | 5 | const Header = (props) => { 6 | const links = [ 7 | { 8 | to: '/trends', 9 | title: 'Github trends' 10 | }, 11 | { 12 | to: '/about', 13 | title: 'About' 14 | }, 15 | { 16 | to: '/404', 17 | title: 'Non-exists page' 18 | } 19 | ] 20 | 21 | if (props.loggedIn) { 22 | links.push({ 23 | to: '/profile', 24 | title: 'Profile' 25 | }) 26 | links.push({ 27 | to: '/logout', 28 | title: 'Logout' 29 | }) 30 | } else { 31 | links.push({ 32 | to: '/login', 33 | title: 'Login' 34 | }) 35 | } 36 | 37 | return ( 38 |
39 |

40 | Redux universal boilerplate 41 |

42 |
43 | 47 | Homepage 48 | 49 | { links.map(function (link, i) { 50 | return ( 51 | 56 | {link.title} 57 | 58 | ) 59 | }) } 60 |
61 |
62 | ) 63 | } 64 | 65 | Header.propTypes = { 66 | loggedIn: PropTypes.bool 67 | } 68 | 69 | export default Header 70 | -------------------------------------------------------------------------------- /src/common/components/Layout/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | import Header from './Header' 5 | import Footer from './Footer' 6 | 7 | import 'semantic-ui-css/semantic.css' 8 | 9 | const Layout = (props) => ( 10 |
11 |
12 |
13 |
14 |
15 | {props.children} 16 |
17 |
18 |
19 |
20 |
21 | ) 22 | 23 | Layout.propTypes = { 24 | children: PropTypes.object, 25 | loggedIn: PropTypes.bool 26 | } 27 | 28 | export default Layout 29 | -------------------------------------------------------------------------------- /src/common/components/NotFound.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import {Link} from 'react-router' 3 | import Helmet from 'react-helmet' 4 | 5 | const NotFound = () => ( 6 |
7 | 8 | 9 |

404

10 |

Page not found

11 |
12 | This page will return 404 HTTP status 13 | if you directly open it in your browser 14 |
15 |

Go home

16 |
17 | ) 18 | 19 | export default NotFound 20 | -------------------------------------------------------------------------------- /src/common/components/Page/About.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Helmet from 'react-helmet' 3 | 4 | const About = () => ( 5 |
6 | 7 |

8 | Boilerplate for react universal application building on 9 | flux architecture based on redux implementation. 10 |

11 |
12 | ) 13 | 14 | export default About 15 | -------------------------------------------------------------------------------- /src/common/components/Page/Home/assets/fonts/Orbitron-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ufocoder/redux-universal-boilerplate/ac036727deb2250ca175aa8f725e5f34aac0437b/src/common/components/Page/Home/assets/fonts/Orbitron-Regular.ttf -------------------------------------------------------------------------------- /src/common/components/Page/Home/assets/images/astronaut.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ufocoder/redux-universal-boilerplate/ac036727deb2250ca175aa8f725e5f34aac0437b/src/common/components/Page/Home/assets/images/astronaut.gif -------------------------------------------------------------------------------- /src/common/components/Page/Home/assets/images/background.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ufocoder/redux-universal-boilerplate/ac036727deb2250ca175aa8f725e5f34aac0437b/src/common/components/Page/Home/assets/images/background.gif -------------------------------------------------------------------------------- /src/common/components/Page/Home/assets/images/ground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ufocoder/redux-universal-boilerplate/ac036727deb2250ca175aa8f725e5f34aac0437b/src/common/components/Page/Home/assets/images/ground.png -------------------------------------------------------------------------------- /src/common/components/Page/Home/assets/styles.styl: -------------------------------------------------------------------------------- 1 | @font-face 2 | font-family Orbitron 3 | src url('./fonts/Orbitron-Regular.ttf') 4 | 5 | .homepage 6 | text-align center 7 | margin -1em 8 | &__ground 9 | background url('./images/ground.png') repeat 10 | height 15px 11 | &__space 12 | background url('./images/background.gif') repeat 13 | height 250px 14 | position relative 15 | &__title 16 | position relative 17 | top 90px 18 | font-size 40px 19 | font-family 'Orbitron' 20 | &__astronaut 21 | position absolute 22 | bottom 0px 23 | height 48px 24 | width 100% 25 | background-image url('./images/astronaut.gif') 26 | background-position center center 27 | background-repeat no-repeat 28 | -------------------------------------------------------------------------------- /src/common/components/Page/Home/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Helmet from 'react-helmet' 3 | 4 | import './assets/styles.styl' 5 | 6 | const Home = () => ( 7 |
8 | 9 |
10 |
11 | Universal boilerplate 12 |
13 |
14 |
15 |
16 |
17 | ) 18 | 19 | export default Home 20 | -------------------------------------------------------------------------------- /src/common/constants/actions/Auth.js: -------------------------------------------------------------------------------- 1 | export const LOGIN_FAILURE = 'LOGIN_FAILURE' 2 | export const LOGIN_SUCCESS = 'LOGIN_SUCCESS' 3 | 4 | export const LOGOUT_SUCCESS = 'LOGOUT_SUCCESS' 5 | export const LOGOUT_FAILURE = 'LOGOUT_FAILURE' 6 | -------------------------------------------------------------------------------- /src/common/constants/actions/Github.js: -------------------------------------------------------------------------------- 1 | export const RESET_TRENDS = 'RESET_TRENDS' 2 | 3 | export const FETCH_TRENDS_LOADING = 'FETCH_TRENDS_LOADING' 4 | export const FETCH_TRENDS_SUCCESS = 'FETCH_TRENDS_SUCCESS' 5 | export const FETCH_TRENDS_FAILURE = 'FETCH_TRENDS_FAILURE' 6 | -------------------------------------------------------------------------------- /src/common/constants/application.js: -------------------------------------------------------------------------------- 1 | export const CONTAINER_ID = 'application' 2 | export const BASE_URL = '/' 3 | 4 | export const TEST_USERNAME = 'demo' 5 | export const TEST_PASSWORD = 'demo' 6 | -------------------------------------------------------------------------------- /src/common/containers/Github/index.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import {provideHooks} from 'redial'; 4 | import {connect} from 'react-redux'; 5 | import {fetchTrends, resetTrends} from 'src/common/actions/Github'; 6 | import Github from 'src/common/components/Github'; 7 | 8 | @provideHooks({ 9 | fetch: ({dispatch}) => { 10 | dispatch(resetTrends()); 11 | return dispatch(fetchTrends()); 12 | }, 13 | }) 14 | @connect( 15 | (state) => ({ 16 | trends: state.github.trends, 17 | loading: state.github.loading, 18 | error: state.github.error, 19 | }) 20 | ) 21 | export default class GithubContainer extends Component { 22 | static propTypes = { 23 | trends: PropTypes.array, 24 | loading: PropTypes.bool, 25 | error: PropTypes.string, 26 | } 27 | 28 | render() { 29 | return ( 30 | 31 | ); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/common/containers/Layout/index.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import {connect} from 'react-redux'; 4 | import Layout from 'src/common/components/Layout'; 5 | 6 | @connect( 7 | (state) => ({ 8 | loggedIn: state.auth.loggedIn, 9 | }) 10 | ) 11 | export default class LayoutContainer extends Component { 12 | static propTypes = { 13 | children: PropTypes.object, 14 | loggedIn: PropTypes.bool, 15 | } 16 | 17 | render() { 18 | return ( 19 | 20 | ); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/common/containers/User/Login.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import Helmet from 'react-helmet'; 4 | import {connect} from 'react-redux'; 5 | import {push} from 'react-router-redux'; 6 | import {login} from 'src/common/actions/Auth'; 7 | import Error from 'src/common/components/Error'; 8 | import { 9 | TEST_USERNAME, 10 | TEST_PASSWORD, 11 | } from 'src/common/constants/application'; 12 | 13 | @connect( 14 | (state) => ({ 15 | user: state.auth.user, 16 | error: state.auth.error, 17 | loggedIn: state.auth.loggedIn, 18 | }), 19 | (dispatch) => ({ 20 | handleSubmit: (username, password) => { 21 | dispatch(login(username, password)); 22 | }, 23 | handleRedirect: () => { 24 | dispatch(push('/profile')); 25 | }, 26 | }) 27 | ) 28 | export default class Login extends Component { 29 | static propTypes = { 30 | user: PropTypes.object, 31 | error: PropTypes.string, 32 | loggedIn: PropTypes.bool, 33 | 34 | handleSubmit: PropTypes.func, 35 | handleRedirect: PropTypes.func, 36 | } 37 | 38 | handleSubmit = (event) => { 39 | const username = this.username.value; 40 | const password = this.password.value; 41 | 42 | event.preventDefault(); 43 | 44 | this.props.handleSubmit(username, password); 45 | } 46 | 47 | componentWillReceiveProps(nextProps) { 48 | if (nextProps.loggedIn) { 49 | this.props.handleRedirect(); 50 | } 51 | } 52 | 53 | render() { 54 | const userLayout = ( 55 |
You already logged in
56 | ); 57 | 58 | const guestLayout = ( 59 |
60 | 61 | 62 |

Login form

63 | 64 |
65 | For test use this credentials: {TEST_USERNAME}/{TEST_PASSWORD} 66 |
67 | 68 | { this.props.error ? 69 | ( 70 | 71 | ) : null 72 | } 73 |
74 |
75 | 76 | { 77 | this.username = ref; 78 | }} placeholder="Enter a username" /> 79 |
80 |
81 | 82 | { 83 | this.password = ref; 84 | }} placeholder="Last Name" /> 85 |
86 | 90 |
91 |
92 | ); 93 | 94 | return this.props.loggedIn ? userLayout : guestLayout; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/common/containers/User/Profile.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import Helmet from 'react-helmet'; 4 | import {connect} from 'react-redux'; 5 | 6 | @connect( 7 | (state) => ({ 8 | user: state.auth.user, 9 | }) 10 | ) 11 | export default class Profile extends Component { 12 | static propTypes = { 13 | user: PropTypes.object, 14 | }; 15 | 16 | render() { 17 | return ( 18 |
19 | 20 | 21 | Welcome, {this.props.user.username}! 22 |
23 | ); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/common/helpers/routes.js: -------------------------------------------------------------------------------- 1 | import {logout} from 'src/common/actions/Auth' 2 | 3 | /** 4 | * @param {object} store Redux store 5 | * @return {function} onEnter callback helper for check authenticated user 6 | */ 7 | export function authRequired (store) { 8 | return (nextState, replace) => { 9 | const state = store.getState() 10 | if (!state.auth.loggedIn) { 11 | replace('/login') 12 | } 13 | } 14 | } 15 | 16 | /** 17 | * @param {object} store Redux store 18 | * @return {function} onEnter callback helper for check not authenticated user 19 | */ 20 | export function authNoRequired (store) { 21 | return (nextState, replace) => { 22 | const state = store.getState() 23 | if (state.auth.loggedIn) { 24 | replace('/profile') 25 | } 26 | } 27 | } 28 | 29 | /** 30 | * @param {object} store Redux store 31 | * @return {function} onEnter callback helper for user logout 32 | */ 33 | export function authLogout (store) { 34 | return (nextState, replace) => { 35 | store.dispatch(logout()) 36 | replace('/login') 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/common/reducers/auth.js: -------------------------------------------------------------------------------- 1 | import { 2 | LOGIN_SUCCESS, 3 | LOGIN_FAILURE, 4 | LOGOUT_SUCCESS, 5 | LOGOUT_FAILURE 6 | } from 'src/common/constants/actions/Auth' 7 | 8 | const initialState = { 9 | user: null, 10 | loggedIn: false 11 | } 12 | 13 | export default (state = initialState, action) => { 14 | switch (action.type) { 15 | case LOGIN_SUCCESS: 16 | return { 17 | ...state, 18 | user: action.result, 19 | loggedIn: true 20 | } 21 | case LOGIN_FAILURE: 22 | return { 23 | ...state, 24 | user: null, 25 | error: action.error 26 | } 27 | case LOGOUT_SUCCESS: 28 | return { 29 | ...state, 30 | loggedIn: false, 31 | user: null 32 | } 33 | case LOGOUT_FAILURE: 34 | return { 35 | ...state, 36 | error: action.error 37 | } 38 | default: 39 | return state 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/common/reducers/github.js: -------------------------------------------------------------------------------- 1 | import { 2 | RESET_TRENDS, 3 | FETCH_TRENDS_LOADING, 4 | FETCH_TRENDS_SUCCESS, 5 | FETCH_TRENDS_FAILURE 6 | } from 'src/common/constants/actions/Github' 7 | 8 | export const initialState = { 9 | trends: [], 10 | loading: false, 11 | error: null 12 | } 13 | 14 | export default (state = initialState, action) => { 15 | switch (action.type) { 16 | case FETCH_TRENDS_LOADING: 17 | return { 18 | ...state, 19 | loading: true 20 | } 21 | case FETCH_TRENDS_SUCCESS: 22 | return { 23 | ...state, 24 | loading: false, 25 | trends: action.trends 26 | } 27 | case FETCH_TRENDS_FAILURE: 28 | return { 29 | ...state, 30 | loading: false, 31 | error: action.error 32 | } 33 | case RESET_TRENDS: 34 | return initialState 35 | default: 36 | return state 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/common/reducers/index.js: -------------------------------------------------------------------------------- 1 | import {combineReducers} from 'redux' 2 | import {routerReducer as routing} from 'react-router-redux' 3 | import auth from './auth' 4 | import github from './github' 5 | 6 | export const rootReducer = combineReducers({ 7 | routing, 8 | auth, 9 | github 10 | }) 11 | 12 | export default rootReducer 13 | -------------------------------------------------------------------------------- /src/common/routes.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import {Route, IndexRoute} from 'react-router' 3 | import Layout from './containers/Layout' 4 | import NotFound from './components/NotFound' 5 | import About from './components/Page/About' 6 | import Home from './components/Page/Home' 7 | import Login from './containers/User/Login' 8 | import Profile from './containers/User/Profile' 9 | import Github from './containers/Github' 10 | import { 11 | authRequired, 12 | authNoRequired, 13 | authLogout 14 | } from './helpers/routes' 15 | 16 | const routes = (store) => { 17 | return ( 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | ) 35 | } 36 | 37 | export default routes 38 | -------------------------------------------------------------------------------- /src/common/store.js: -------------------------------------------------------------------------------- 1 | /* global window, __DEV__, __CLIENT__ */ 2 | 3 | import {createStore, applyMiddleware, compose} from 'redux' 4 | import thunk from 'redux-thunk' 5 | import {routerMiddleware} from 'react-router-redux' 6 | import rootReducer from './reducers' 7 | 8 | /** 9 | * Return store 10 | * 11 | * @param {object} history History 12 | * @param {object} initialState Initial state for store 13 | * @return {object} Returns store with state 14 | */ 15 | export default function (history, initialState = {}) { 16 | let finalCreateStore 17 | 18 | if (__DEV__ && __CLIENT__) { 19 | finalCreateStore = compose( 20 | applyMiddleware(thunk), 21 | applyMiddleware(routerMiddleware(history)), 22 | typeof window === 'object' && 23 | typeof window.devToolsExtension !== 'undefined' ? window.devToolsExtension() : (f) => f 24 | )(createStore) 25 | } else { 26 | finalCreateStore = compose( 27 | applyMiddleware(thunk), 28 | applyMiddleware(routerMiddleware(history)) 29 | )(createStore) 30 | } 31 | 32 | const store = finalCreateStore(rootReducer, initialState) 33 | 34 | if (__DEV__ && module.hot) { 35 | module.hot.accept('./reducers', () => { 36 | const nextRootReducer = require('./reducers').default 37 | store.replaceReducer(nextRootReducer) 38 | }) 39 | } 40 | 41 | return store 42 | } 43 | -------------------------------------------------------------------------------- /src/server/containers/Html/index.js: -------------------------------------------------------------------------------- 1 | /* eslint react/no-danger: 0 */ 2 | 3 | import React from 'react'; 4 | import PropTypes from 'prop-types'; 5 | import Helmet from 'react-helmet'; 6 | import serialize from 'serialize-javascript'; 7 | import {CONTAINER_ID} from 'src/common/constants/application'; 8 | 9 | export default class Html extends React.Component { 10 | static propTypes = { 11 | assets: PropTypes.object.isRequired, 12 | content: PropTypes.string.isRequired, 13 | store: PropTypes.object.isRequired, 14 | } 15 | 16 | render() { 17 | const {assets, store, content} = this.props; 18 | 19 | const helmet = Helmet.rewind(); 20 | const attrs = helmet.htmlAttributes.toComponent(); 21 | 22 | return ( 23 | 24 | 25 | 26 | {helmet.title.toComponent()} 27 | {helmet.meta.toComponent()} 28 | {helmet.link.toComponent()} 29 | 30 | {Object.keys(assets.styles).map((style, i) => 31 | 32 | )} 33 | 34 | 35 | 36 |
37 |