├── .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 |
56 |
57 | {PROD ? : }
58 | {PROD && }
59 | {PROD &&
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 |