├── .gitignore ├── src ├── universal │ ├── styles │ │ └── global.less │ ├── redux │ │ ├── reducers │ │ │ └── index.js │ │ └── createStore.js │ ├── routes │ │ ├── static.js │ │ ├── async.js │ │ └── Routes.js │ ├── containers │ │ └── App │ │ │ └── AppContainer.js │ ├── components │ │ ├── App │ │ │ ├── App.js │ │ │ └── App.css │ │ └── Home │ │ │ ├── Home.css │ │ │ └── Home.js │ └── modules │ │ └── counter │ │ ├── ducks │ │ └── counter.js │ │ ├── containers │ │ └── Counter │ │ │ └── CounterContainer.js │ │ └── components │ │ └── Counter │ │ ├── Counter.js │ │ └── Counter.css ├── server │ ├── server.babel.js │ ├── hmr.js │ ├── server.js │ ├── ssr.js │ └── Html.js └── client │ ├── containers │ └── AppContainer.js │ └── client.js ├── postcss.config.js ├── webpack ├── client.babel.js ├── server.babel.js ├── webpack.config.server.js ├── webpack.config.development.js └── webpack.config.client.js ├── .babelrc ├── package.json └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | build 4 | npm-debug.log 5 | -------------------------------------------------------------------------------- /src/universal/styles/global.less: -------------------------------------------------------------------------------- 1 | .background { 2 | background-color: #DDDDDD; 3 | } 4 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [ 3 | require('autoprefixer') 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /webpack/client.babel.js: -------------------------------------------------------------------------------- 1 | require('babel-register'); 2 | module.exports = require('./webpack.config.client.js'); 3 | -------------------------------------------------------------------------------- /webpack/server.babel.js: -------------------------------------------------------------------------------- 1 | require('babel-register'); 2 | module.exports = require('./webpack.config.server.js'); 3 | -------------------------------------------------------------------------------- /src/universal/redux/reducers/index.js: -------------------------------------------------------------------------------- 1 | export {default as counter} from 'universal/modules/counter/ducks/counter.js'; 2 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["react", "es2015", "stage-0"], 3 | "plugins": [ 4 | ["transform-decorators-legacy"], 5 | ["add-module-exports"], 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /src/universal/routes/static.js: -------------------------------------------------------------------------------- 1 | export { default as Home } from 'universal/components/Home/Home.js'; 2 | export { default as Counter } from 'universal/modules/counter/containers/Counter/CounterContainer.js'; 3 | -------------------------------------------------------------------------------- /src/server/server.babel.js: -------------------------------------------------------------------------------- 1 | // All subsequent files required by node with the extensions .es6, .es, .jsx and .js will be transformed by Babel. 2 | require('babel-register'); 3 | 4 | // Server Driver Code, everything from here on can use all the super future ES6 features! 5 | module.exports = require('./server.js'); 6 | -------------------------------------------------------------------------------- /src/universal/containers/App/AppContainer.js: -------------------------------------------------------------------------------- 1 | import React, {Component, PropTypes} from 'react'; 2 | import App from 'universal/components/App/App'; 3 | 4 | class AppContainer extends Component { 5 | static propTypes = { 6 | children: PropTypes.element.isRequired 7 | }; 8 | 9 | render () { 10 | return ( 11 | 12 | ); 13 | } 14 | } 15 | 16 | export default AppContainer; 17 | -------------------------------------------------------------------------------- /src/universal/components/App/App.js: -------------------------------------------------------------------------------- 1 | import React, {Component, PropTypes} from 'react'; 2 | 3 | import styles from './App.css'; 4 | 5 | class App extends Component { 6 | static propTypes = { 7 | children: PropTypes.element.isRequired 8 | }; 9 | 10 | render () { 11 | return ( 12 |
13 | {this.props.children} 14 |
15 | ); 16 | } 17 | } 18 | 19 | export default App; 20 | -------------------------------------------------------------------------------- /src/universal/components/Home/Home.css: -------------------------------------------------------------------------------- 1 | 2 | .home { 3 | max-width: 750px; 4 | } 5 | 6 | .center { 7 | text-align: center; 8 | } 9 | 10 | .title { 11 | color: white; 12 | font-size: 3em; 13 | display: block; 14 | text-align: center; 15 | width: 100%; 16 | } 17 | 18 | .button { 19 | display: inline-block; 20 | margin-top: 50px; 21 | padding: 15px 40px; 22 | font-size: 1.5em; 23 | text-align: center; 24 | color: #E4E7EB; 25 | background-color: #F48F94; 26 | border: solid 7px #F27D83; 27 | border-radius: 100px; 28 | text-decoration: none; 29 | margin-top: 50px; 30 | 31 | } 32 | 33 | .button:hover { 34 | border-color: #F48F94; 35 | } 36 | -------------------------------------------------------------------------------- /src/server/hmr.js: -------------------------------------------------------------------------------- 1 | const HMR = (app) => { 2 | const webpack = require('webpack'); 3 | const devWebpackConfig = require('../../webpack/webpack.config.development.js'); 4 | const webpackDevMiddleware = require('webpack-dev-middleware'); 5 | const webpackHotMiddleware = require('webpack-hot-middleware'); 6 | 7 | const compiler = webpack(devWebpackConfig); 8 | 9 | app.use(webpackDevMiddleware(compiler, { 10 | noInfo: true, 11 | hot: true, 12 | publicPath: devWebpackConfig.output.publicPath 13 | })); 14 | 15 | app.use(webpackHotMiddleware(compiler, { 16 | log: console.log, 17 | reload: true 18 | })); 19 | 20 | return app; 21 | } 22 | 23 | export default HMR; 24 | -------------------------------------------------------------------------------- /src/client/containers/AppContainer.js: -------------------------------------------------------------------------------- 1 | import React, {Component, PropTypes} from 'react'; 2 | import { ConnectedRouter } from 'react-router-redux'; 3 | import {Route} from 'react-router'; 4 | 5 | // Redux 6 | import { Provider } from 'react-redux'; 7 | 8 | // Components 9 | import Routes from 'universal/routes/Routes.js'; 10 | 11 | class AppContainer extends Component { 12 | static propTypes = { 13 | history: PropTypes.object.isRequired 14 | } 15 | 16 | render () { 17 | const { 18 | history 19 | } = this.props; 20 | 21 | return ( 22 | 23 | { 24 | return () 25 | }}/> 26 | 27 | ) ; 28 | } 29 | } 30 | 31 | export default AppContainer; 32 | -------------------------------------------------------------------------------- /src/universal/modules/counter/ducks/counter.js: -------------------------------------------------------------------------------- 1 | import {fromJS, Map as iMap} from 'immutable'; 2 | import {push, replace} from 'react-router-redux'; 3 | 4 | export const COUNTER_INCREMENT = 'COUNTER_INCREMENT'; 5 | export const COUNTER_DECREMENT = 'COUNTER_DECREMENT'; 6 | 7 | const initialState = iMap({ 8 | count: 0 9 | }); 10 | 11 | export default function reducer(state = initialState, action = {}) { 12 | switch (action.type) { 13 | case COUNTER_INCREMENT: 14 | return state.merge({ 15 | count: state.get('count') + 1 16 | }); 17 | case COUNTER_DECREMENT: 18 | return state.merge({ 19 | count: state.get('count') - 1 20 | }); 21 | default: 22 | return state; 23 | } 24 | } 25 | 26 | 27 | export function incrementCount( ) { 28 | return { 29 | type: COUNTER_INCREMENT 30 | }; 31 | } 32 | 33 | export function decrementCount( ) { 34 | return { 35 | type: COUNTER_DECREMENT 36 | }; 37 | } 38 | -------------------------------------------------------------------------------- /src/universal/redux/createStore.js: -------------------------------------------------------------------------------- 1 | import { 2 | createStore, 3 | combineReducers, 4 | applyMiddleware 5 | } from 'redux'; 6 | 7 | import { 8 | ConnectedRouter, 9 | routerReducer, 10 | routerMiddleware 11 | } from 'react-router-redux'; 12 | 13 | import * as Reducers from './reducers/index.js'; 14 | 15 | export default (history) => { 16 | const middleware = routerMiddleware(history); 17 | 18 | const store = createStore(combineReducers({ 19 | ...Reducers, 20 | router: routerReducer 21 | }), applyMiddleware(middleware)); 22 | 23 | 24 | if (module.hot) { 25 | // Enable Webpack hot module replacement for reducers 26 | module.hot.accept('./reducers', () => { 27 | const nextReducers = require('./reducers/index.js'); 28 | const rootReducer = combineReducers({ 29 | ...nextReducers, 30 | router: routerReducer 31 | }); 32 | 33 | store.replaceReducer(rootReducer); 34 | }); 35 | } 36 | 37 | 38 | return store; 39 | } 40 | -------------------------------------------------------------------------------- /src/universal/components/App/App.css: -------------------------------------------------------------------------------- 1 | html { 2 | height: 100%; 3 | background: linear-gradient(90deg, #80A2CC, #F99F86); 4 | background: -webkit-linear-gradient(90deg, #80A2CC, #F99F86); 5 | } 6 | 7 | body { 8 | min-height: 100%; 9 | margin: 0; 10 | padding: 0; 11 | display: flex; 12 | position: relative; 13 | justify-content: center; 14 | align-items: center; 15 | font-family: 'Arvo', Arial, Helvetica, Sans-serif; 16 | background: linear-gradient(90deg, #80A2CC, #F99F86); 17 | background: -webkit-linear-gradient(90deg, #80A2CC, #F99F86); 18 | 19 | } 20 | 21 | h1,h2,h3,h4,h5,h6 { 22 | color: white; 23 | } 24 | 25 | h2 { 26 | font-size: 2em; 27 | line-height: 1.5em; 28 | } 29 | 30 | p, li, ul, ol { 31 | font-family: 'Raleway'; 32 | color: white; 33 | font-size: 20px; 34 | line-height: 1.25em; 35 | } 36 | 37 | .app { 38 | /*width: 100%; 39 | height: 100%;*/ 40 | margin-bottom: 150px; 41 | margin-top: 150px; 42 | 43 | /*padding-top: 100px;*/ 44 | } 45 | -------------------------------------------------------------------------------- /src/universal/routes/async.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | function asyncRoute(getComponent) { 4 | return class AsyncComponent extends React.Component { 5 | state = { 6 | Component: null 7 | }; 8 | 9 | componentDidMount() { 10 | if ( this.state.Component === null ) { 11 | getComponent().then((Component) => { 12 | this.setState({Component: Component}); 13 | }) 14 | } 15 | } 16 | 17 | render() { 18 | const { 19 | Component 20 | } = this.state; 21 | 22 | if ( Component ) { 23 | return (); 24 | } 25 | return (
loading...
); // or
with a loading spinner, etc.. 26 | } 27 | } 28 | } 29 | 30 | export const Home = asyncRoute(() => { 31 | return System.import('../components/Home/Home.js'); 32 | }); 33 | 34 | export const Counter = asyncRoute(() => { 35 | return System.import('../modules/counter/containers/Counter/CounterContainer.js'); 36 | }); 37 | -------------------------------------------------------------------------------- /src/client/client.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {render} from 'react-dom'; 3 | import {AppContainer} from 'react-hot-loader'; 4 | 5 | // Components 6 | import App from './containers/AppContainer.js'; 7 | 8 | // Redux 9 | import { Provider } from 'react-redux'; 10 | import createStore from '../universal/redux/createStore.js'; 11 | import createHistory from 'history/createBrowserHistory'; 12 | 13 | const history = createHistory(); 14 | const store = createStore(history); 15 | 16 | const rootEl = document.getElementById('root') 17 | const renderApp = (Component) => { 18 | render( 19 | 20 | 21 | 22 | 23 | , 24 | rootEl 25 | ); 26 | } 27 | 28 | renderApp(App); 29 | 30 | if (module.hot) { 31 | module.hot.accept('./containers/AppContainer.js', () => { 32 | const nextApp = require('./containers/AppContainer.js'); 33 | renderApp(nextApp); 34 | }); 35 | } 36 | -------------------------------------------------------------------------------- /src/universal/routes/Routes.js: -------------------------------------------------------------------------------- 1 | // Libraries 2 | import React, {Component, PropTypes} from 'react'; 3 | import {Route, Redirect} from 'react-router'; 4 | 5 | // Routes 6 | // For Development only 7 | import * as RouteMap from '../routes/static.js'; 8 | 9 | // This is used in production for code splitting via `wepback.config.server.js` 10 | // import * as RouteMap from 'universal/routes/async.js'; 11 | 12 | // Containers 13 | import AppContainer from 'universal/containers/App/AppContainer.js'; 14 | // import PrivateRouteContainer from 'universal/containers/PrivateRoute/PrivateRouteContainer.js'; 15 | 16 | class Routes extends Component { 17 | render () { 18 | const { 19 | location 20 | } = this.props; 21 | 22 | return ( 23 | 24 |
25 | 26 | 27 |
28 |
29 | ); 30 | } 31 | } 32 | 33 | export default Routes; 34 | -------------------------------------------------------------------------------- /src/universal/modules/counter/containers/Counter/CounterContainer.js: -------------------------------------------------------------------------------- 1 | // Libraries 2 | import React, {Component, PropTypes} from 'react'; 3 | import {connect} from 'react-redux'; 4 | 5 | // Components 6 | import Counter from 'universal/modules/counter/components/Counter/Counter.js'; 7 | 8 | // Actions 9 | import { 10 | incrementCount, 11 | decrementCount 12 | } from 'universal/modules/counter/ducks/counter.js'; 13 | 14 | 15 | @connect(mapStateToProps, mapDispatchToProps) 16 | class CounterContainer extends Component { 17 | static propTypes = { 18 | // State 19 | count: PropTypes.number.isRequired, 20 | 21 | // Dispatchers 22 | incrementCount: PropTypes.func.isRequired, 23 | decrementCount: PropTypes.func.isRequired 24 | } 25 | 26 | render () { 27 | return (); 28 | } 29 | } 30 | 31 | 32 | function mapStateToProps(state, props) { 33 | const count = state.counter.get('count'); 34 | return { 35 | count 36 | }; 37 | } 38 | 39 | 40 | function mapDispatchToProps(dispatch, props) { 41 | return { 42 | incrementCount: () => { 43 | dispatch(incrementCount()); 44 | }, 45 | decrementCount: () => { 46 | dispatch(decrementCount()); 47 | } 48 | }; 49 | } 50 | 51 | export default CounterContainer; 52 | -------------------------------------------------------------------------------- /src/universal/modules/counter/components/Counter/Counter.js: -------------------------------------------------------------------------------- 1 | import React, {Component, PropTypes} from 'react'; 2 | import styles from './Counter.css'; 3 | import classNames from 'classnames'; 4 | 5 | class Counter extends Component { 6 | 7 | static propTypes = { 8 | incrementCount: PropTypes.func.isRequired, 9 | decrementCount: PropTypes.func.isRequired, 10 | count: PropTypes.number.isRequired 11 | } 12 | 13 | handleLinkClick(event) { 14 | event.stopPropagation(); 15 | event.preventDefault(); 16 | } 17 | 18 | handleIncrementClick (incrementCount, event) { 19 | this.handleLinkClick(event); 20 | incrementCount(); 21 | } 22 | 23 | handleDecrementClick(decrementCount, event) { 24 | this.handleLinkClick(event); 25 | decrementCount(); 26 | } 27 | 28 | render () { 29 | const { 30 | count, 31 | incrementCount, 32 | decrementCount 33 | } = this.props; 34 | 35 | return ( 36 |
37 |
{count}
38 | + 39 | - 40 |
41 | ) 42 | } 43 | } 44 | 45 | export default Counter; 46 | -------------------------------------------------------------------------------- /src/server/server.js: -------------------------------------------------------------------------------- 1 | import http from 'http'; 2 | import express from 'express'; 3 | import colors from 'colors'; 4 | import path from 'path'; 5 | 6 | // Server Side Rendering 7 | import { 8 | renderPage, 9 | renderDevPage 10 | } from './ssr.js'; 11 | 12 | const PROD = process.env.NODE_ENV === 'production'; 13 | 14 | const app = express(); 15 | 16 | if (PROD) { 17 | app.use('/static', express.static('build')); 18 | app.get('*', renderPage); 19 | } else { 20 | const HMR = require('./hmr.js'); 21 | // Hot Module Reloading 22 | HMR(app); 23 | app.get('*', renderDevPage); 24 | } 25 | 26 | // catch 404 and forward to error handler 27 | app.use(function(req, res, next) { 28 | var err = new Error('Not Found'); 29 | err.status = 404; 30 | next(err); 31 | }); 32 | 33 | // development error handler 34 | if (!PROD) { 35 | app.use(function(err, req, res, next) { 36 | console.error('error : ', err) 37 | res.status(err.status || 500); 38 | }); 39 | } 40 | 41 | // production error handler 42 | app.use(function(err, req, res, next) { 43 | console.error('error : ', err.message) 44 | res.status(err.status || 500); 45 | }); 46 | 47 | const server = http.createServer(app); 48 | 49 | server.listen(8080, function() { 50 | const address = server.address(); 51 | console.log(`${'>>>'.cyan} ${'Listening on:'.rainbow} ${'localhost::'.trap.magenta}${`${address.port}`.green}`); 52 | }); 53 | -------------------------------------------------------------------------------- /src/universal/modules/counter/components/Counter/Counter.css: -------------------------------------------------------------------------------- 1 | .counterContainer { 2 | position: relative; 3 | } 4 | 5 | 6 | .counter { 7 | width: 200px; 8 | height: 200px; 9 | display: flex; 10 | justify-content: center; 11 | align-items: center; 12 | font-size: 4em; 13 | border: solid 10px white; 14 | border-radius: 200px; 15 | color: white; 16 | } 17 | 18 | .button { 19 | display: inline-block; 20 | display: flex; 21 | font-size: 3em; 22 | line-height: 50px; 23 | justify-content: center; 24 | vertical-align: center; 25 | color: white; 26 | background-color: #30B661; 27 | border: solid 1px #0F9D58; 28 | border-radius: 50px; 29 | width: 50px; 30 | height: 50px; 31 | -webkit-user-select: none; /* Chrome all / Safari all */ 32 | } 33 | 34 | .button:hover { 35 | cursor: pointer; 36 | } 37 | 38 | .positive { 39 | background-color: #B1DDCC; 40 | border: solid 7px #7EC6AB; 41 | position: absolute; 42 | right: -25px; 43 | top: 50%; 44 | margin-top: -25px; 45 | font-size: 2.5em 46 | } 47 | 48 | .positive:hover { 49 | /*background-color: #DEF0EA;*/ 50 | border-color: #B1DDCC; 51 | } 52 | 53 | .negative { 54 | background-color: #F48F94; 55 | border: solid 7px #F27D83; 56 | position: absolute; 57 | left: -25px; 58 | top: 50%; 59 | margin-top: -25px; 60 | line-height: 45px; 61 | } 62 | 63 | .negative:hover { 64 | border-color: #F48F94; 65 | } 66 | -------------------------------------------------------------------------------- /src/server/ssr.js: -------------------------------------------------------------------------------- 1 | // Node Modules 2 | import fs from 'fs'; 3 | import {basename, join} from 'path'; 4 | 5 | // Libraries 6 | import React from 'react'; 7 | import {renderToString} from 'react-dom/server'; 8 | 9 | // Redux 10 | // import {push} from 'react-router-redux'; 11 | import createStore from 'universal/redux/createStore.js'; 12 | import createHistory from 'history/createMemoryHistory' 13 | 14 | // Components 15 | import Html from './Html.js'; 16 | 17 | function renderApp(url, res, store, assets) { 18 | const context = {}; 19 | 20 | const html = renderToString( 21 | 27 | ); 28 | 29 | res.send(''+html); 30 | } 31 | 32 | export const renderPage = function (req, res) { 33 | const history = createHistory( ); 34 | const store = createStore(history); 35 | 36 | const assets = require('../../build/assets.json'); 37 | 38 | assets.manifest.text = fs.readFileSync( 39 | join(__dirname, '..', '..', 'build', basename(assets.manifest.js)), 40 | 'utf-8' 41 | ); 42 | 43 | renderApp(req.url, res, store, assets); 44 | }; 45 | 46 | export const renderDevPage = function (req, res) { 47 | const history = createHistory( ); 48 | const store = createStore(history); 49 | renderApp(req.url, res, store); 50 | }; 51 | 52 | export default renderPage; 53 | -------------------------------------------------------------------------------- /src/server/Html.js: -------------------------------------------------------------------------------- 1 | // Libraries 2 | import React, {Component, PropTypes} from 'react'; 3 | import {StaticRouter} from 'react-router'; 4 | import {renderToString} from 'react-dom/server'; 5 | 6 | // Redux 7 | import { Provider } from 'react-redux'; 8 | 9 | class Html extends Component { 10 | static propTypes = { 11 | url: PropTypes.string.isRequired, 12 | store: PropTypes.object.isRequired, 13 | title: PropTypes.string.isRequired, 14 | assets: PropTypes.object 15 | } 16 | 17 | render () { 18 | const PROD = process.env.NODE_ENV === 'production'; 19 | 20 | const { 21 | title, 22 | store, 23 | assets, 24 | url, 25 | context 26 | } = this.props; 27 | 28 | const { 29 | manifest, 30 | app, 31 | vendor 32 | } = assets || {}; 33 | 34 | let state = store.getState(); 35 | 36 | const initialState = `window.__INITIAL_STATE__ = ${JSON.stringify(state)}`; 37 | const Layout = PROD ? require( '../../build/prerender.js') : () => {}; 38 | 39 | const root = PROD && renderToString( 40 | 41 | 42 | 43 | 44 | 45 | ); 46 | 47 | return ( 48 | 49 | 50 | 51 | {title} 52 | 53 | {PROD && } 54 | 55 | 56 |