├── Procfile ├── .eslintignore ├── .gitignore ├── static ├── favicon.ico └── logo.svg ├── src ├── containers │ ├── RadiumContainer.js │ └── StargazersContainer.js ├── actions │ ├── actionTypes.js │ └── StargazersActions.js ├── reducers │ ├── index.js │ └── stargazers.js ├── components │ ├── User.js │ ├── About.js │ ├── Home.js │ └── Header.js ├── routes.js ├── store.js ├── client.js └── server.js ├── .editorconfig ├── .eslintrc ├── configs ├── webpack.server-watch.js ├── webpack.client.js ├── webpack.server.js └── webpack.client-watch.js ├── LICENSE.md ├── README.md └── package.json /Procfile: -------------------------------------------------------------------------------- 1 | web: npm run build && npm start 2 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | webpack.client-watch.js 2 | webpack.client.js 3 | babel.server.js 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | node_modules/ 3 | dist/ 4 | etc/ 5 | *.log 6 | *.map 7 | -------------------------------------------------------------------------------- /static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luandro/hapi-universal-redux/HEAD/static/favicon.ico -------------------------------------------------------------------------------- /src/containers/RadiumContainer.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Radium from 'radium'; 3 | 4 | const RadiumContainer = ({children}) => children 5 | export default Radium(RadiumContainer) 6 | -------------------------------------------------------------------------------- /src/actions/actionTypes.js: -------------------------------------------------------------------------------- 1 | export const STARGAZERS_FETCH = 'STARGAZERS_FETCH'; 2 | export const STARGAZERS_STOP_FETCH = 'STARGAZERS_STOP_FETCH'; 3 | export const STARGAZERS_REQUEST = 'STARGAZERS_REQUEST'; 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig: http://EditorConfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | end_of_line = crlf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | indent_style = tab 11 | tab_width = 4 12 | -------------------------------------------------------------------------------- /src/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import stargazers from './stargazers'; 3 | import { routerReducer } from 'react-router-redux' 4 | 5 | const rootReducer = combineReducers({ 6 | stargazers, 7 | routing: routerReducer 8 | }); 9 | 10 | export default rootReducer; 11 | -------------------------------------------------------------------------------- /src/components/User.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | const avatarSize = 32; 3 | const avatarUrl = (id) => `https://avatars.githubusercontent.com/u/${id}?v=3&s=${avatarSize}`; 4 | 5 | /** 6 | * Stateless React component 7 | */ 8 | export default ({user, styles}) => ( 9 | 10 | {user.login} 11 | 12 | ); 13 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "eslint-config-airbnb", 3 | "ecmaFeatures": { 4 | globalReturn: true 5 | }, 6 | "rules": { 7 | // Temporarily disabled due to a possible bug in babel-eslint (todomvc example) 8 | "block-scoped-var": 0, 9 | "no-console": 0, 10 | // Temporarily disabled for test/* until babel/babel-eslint#33 is resolved 11 | "padded-blocks": 0, 12 | "space-before-function-paren": [2, { "anonymous": "always", "named": "never" }] 13 | }, 14 | "globals": { 15 | "__CLIENT__": true, 16 | "__SERVER__": true 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/routes.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Router, Route } from 'react-router'; 3 | import StargazersContainer from './containers/StargazersContainer'; 4 | import Header from './components/Header'; 5 | import Home from './components/Home'; 6 | import About from './components/About'; 7 | 8 | /** 9 | * The React Routes for both the server and the client. 10 | */ 11 | module.exports = ( 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | ); 21 | -------------------------------------------------------------------------------- /configs/webpack.server-watch.js: -------------------------------------------------------------------------------- 1 | var webpack = require("webpack"); 2 | var config = require("./webpack.server.js"); 3 | var wds = { 4 | hostname: process.env.HOSTNAME || "localhost", 5 | port: 8080 6 | }; 7 | 8 | config.cache = true; 9 | config.debug = true; 10 | 11 | config.entry.unshift( 12 | "webpack/hot/poll?1000" 13 | ); 14 | 15 | config.output.publicPath = "http://" + wds.hostname + ":" + wds.port + "/dist"; 16 | 17 | config.plugins = [ 18 | new webpack.DefinePlugin({__CLIENT__: false, __SERVER__: true, __PRODUCTION__: false, __DEV__: true}), 19 | new webpack.HotModuleReplacementPlugin(), 20 | new webpack.NoErrorsPlugin() 21 | ]; 22 | 23 | module.exports = config; 24 | -------------------------------------------------------------------------------- /src/store.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware, compose } from 'redux'; 2 | import thunk from 'redux-thunk'; 3 | import rootReducer from './reducers'; 4 | 5 | export default function (initialState) { 6 | const finalCreateStore = compose( 7 | applyMiddleware(thunk), 8 | typeof window === 'object' && typeof window.devToolsExtension !== 'undefined' ? window.devToolsExtension() : f => f 9 | )(createStore); 10 | 11 | const store = finalCreateStore(rootReducer, initialState); 12 | 13 | if (module.hot) { 14 | // Enable Webpack hot module replacement for reducers 15 | module.hot.accept('./reducers', () => { 16 | const nextRootReducer = require('./reducers/index').default; 17 | store.replaceReducer(nextRootReducer); 18 | }); 19 | } 20 | 21 | return store; 22 | } 23 | -------------------------------------------------------------------------------- /src/reducers/stargazers.js: -------------------------------------------------------------------------------- 1 | import { 2 | STARGAZERS_FETCH, STARGAZERS_REQUEST, 3 | STARGAZERS_STOP_FETCH 4 | } from '../actions/actionTypes'; 5 | 6 | const initialState = { 7 | users: [], 8 | nextPage: 1, 9 | pagesToFetch: 30, 10 | isLoading: false 11 | }; 12 | 13 | export default function stargazers(state = initialState, action) { 14 | switch (action.type) { 15 | case STARGAZERS_FETCH: 16 | return Object.assign({}, state, { 17 | users: state.users.concat(action.fetchedStargazers), 18 | nextPage: state.nextPage + 1, 19 | pagesToFetch: state.pagesToFetch - 1 20 | }) 21 | case STARGAZERS_REQUEST: 22 | return Object.assign({}, state, { 23 | isLoading: true 24 | }) 25 | case STARGAZERS_STOP_FETCH: 26 | return Object.assign({}, state, { 27 | pagesToFetch: 0, 28 | isLoading: false 29 | }) 30 | default: 31 | return state; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/components/About.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default ({ styles }) => ( 4 |
5 |

Getting started

6 |

7 | Welcome to Hapi Universal Redux. 8 | Start editing the components to see the page hot reloading on the client. 9 | The goal of this project is to create a simple, 10 | yet production ready starterkit for React and Redux apps. 11 |

12 |

Redux DevTools

13 |

14 | To start managing your state create new constants, actions and reducers. 15 | Install Redux DevTools Extension for Chrome if you haven't already, 16 | and use it to debug you're app's state. 17 |

18 |
19 | ) 20 | -------------------------------------------------------------------------------- /configs/webpack.client.js: -------------------------------------------------------------------------------- 1 | var webpack = require("webpack"); 2 | var path = require("path"); 3 | 4 | module.exports = { 5 | target: "web", 6 | cache: false, 7 | context: __dirname, 8 | debug: false, 9 | devtool: false, 10 | entry: ["../src/client"], 11 | output: { 12 | path: path.join(__dirname, "../static/dist"), 13 | filename: "client.js", 14 | chunkFilename: "[name].[id].js" 15 | }, 16 | plugins: [ 17 | new webpack.DefinePlugin({__CLIENT__: true, __SERVER__: false, __PRODUCTION__: true, __DEV__: false}), 18 | new webpack.DefinePlugin({"process.env": {NODE_ENV: '"production"'}}), 19 | new webpack.optimize.DedupePlugin(), 20 | new webpack.optimize.OccurenceOrderPlugin(), 21 | new webpack.optimize.UglifyJsPlugin({compress: {warnings: false}}) 22 | ], 23 | module: { 24 | loaders: [ 25 | {test: /\.json$/, loaders: ["json"]} 26 | ], 27 | postLoaders: [ 28 | {test: /\.js$/, loaders: ["babel?presets[]=es2015&presets[]=stage-0&presets[]=react"], exclude: /node_modules/} 29 | ], 30 | noParse: /\.min\.js/ 31 | }, 32 | resolve: { 33 | modulesDirectories: [ 34 | "src", 35 | "node_modules", 36 | "web_modules" 37 | ], 38 | extensions: ["", ".json", ".js"] 39 | }, 40 | node: { 41 | __dirname: true, 42 | fs: 'empty' 43 | } 44 | }; 45 | -------------------------------------------------------------------------------- /src/client.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import { Router, browserHistory } from 'react-router' 4 | import configureStore from "./store.js"; 5 | import { Provider } from 'react-redux'; 6 | import routes from "./routes"; 7 | import RadiumContainer from "./containers/RadiumContainer"; 8 | import { syncHistoryWithStore } from 'react-router-redux' 9 | 10 | const store = configureStore(window.__INITIAL_STATE__); 11 | delete window.__INITIAL_STATE__; 12 | const history = syncHistoryWithStore(browserHistory, store) 13 | 14 | /** 15 | * Fire-up React Router. 16 | */ 17 | const reactRoot = window.document.getElementById("react-root"); 18 | 19 | ReactDOM.render( 20 | 21 | 22 | 23 | 24 | , 25 | reactRoot 26 | ) 27 | 28 | 29 | /** 30 | * Detect whether the server-side render has been discarded due to an invalid checksum. 31 | */ 32 | if(process.env.NODE_ENV === "production"){ 33 | if (!reactRoot.firstChild || !reactRoot.firstChild.attributes || 34 | !reactRoot.firstChild.attributes["data-react-checksum"]) { 35 | console.error("Server-side React render was discarded. Make sure that your initial render does not contain any client-side code."); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/actions/StargazersActions.js: -------------------------------------------------------------------------------- 1 | import fetch from 'isomorphic-fetch'; 2 | import { 3 | STARGAZERS_FETCH, STARGAZERS_REQUEST, 4 | STARGAZERS_STOP_FETCH, 5 | } from './actionTypes'; 6 | 7 | let githubApi = "https://api.github.com"; 8 | if (__CLIENT__) { 9 | const { protocol, hostname, port } = window.location; 10 | githubApi = `${protocol}//${hostname}:${port}/api/github`; 11 | } 12 | 13 | function receiveUsers(fetchedStargazers) { 14 | return { 15 | type: STARGAZERS_FETCH, 16 | fetchedStargazers 17 | }; 18 | } 19 | 20 | export function requestUsers() { 21 | return { 22 | type: STARGAZERS_REQUEST 23 | } 24 | } 25 | function stopFetching() { 26 | return { 27 | type: STARGAZERS_STOP_FETCH 28 | } 29 | } 30 | 31 | export function fetchUsers(nextPage, pagesToFetch) { 32 | return function (dispatch) { 33 | return fetch(githubApi + "/repos/Luandro/hapi-universal-redux/stargazers" +`?per_page=10&page=${nextPage}`) 34 | .then((response) => response.json()) 35 | .then((body) => { 36 | if (!body || !body.length) { 37 | dispatch(stopFetching()); 38 | return 39 | } 40 | const fetchedStargazers = body.map(({ id, login }) => ({ id, login })); 41 | dispatch(receiveUsers(fetchedStargazers)); 42 | }) 43 | .catch((error) => { 44 | console.error(error); 45 | }) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /configs/webpack.server.js: -------------------------------------------------------------------------------- 1 | var webpack = require("webpack"); 2 | var nodeExternals = require("webpack-node-externals"); 3 | var path = require("path"); 4 | var fs = require("fs"); 5 | 6 | module.exports = { 7 | target: "node", 8 | cache: false, 9 | context: __dirname, 10 | debug: false, 11 | devtool: "source-map", 12 | entry: ["../src/server"], 13 | output: { 14 | path: path.join(__dirname, "../dist"), 15 | filename: "server.js" 16 | }, 17 | plugins: [ 18 | new webpack.DefinePlugin({__CLIENT__: false, __SERVER__: true, __PRODUCTION__: true, __DEV__: false}), 19 | new webpack.DefinePlugin({"process.env": {NODE_ENV: '"production"'}}) 20 | ], 21 | module: { 22 | loaders: [ 23 | {test: /\.json$/, loaders: ["json"]}, 24 | {test: /\.(ico|gif|png|jpg|jpeg|svg|webp)$/, loaders: ["file?context=static&name=/[path][name].[ext]"], exclude: /node_modules/}, 25 | {test: /\.js$/, loaders: ["babel?presets[]=es2015&presets[]=stage-0&presets[]=react"], exclude: /node_modules/} 26 | ], 27 | postLoaders: [ 28 | ], 29 | noParse: /\.min\.js/ 30 | }, 31 | externals: [nodeExternals({ 32 | whitelist: ["webpack/hot/poll?1000"] 33 | })], 34 | resolve: { 35 | modulesDirectories: [ 36 | "src", 37 | "node_modules", 38 | "static" 39 | ], 40 | extensions: ["", ".json", ".js"] 41 | }, 42 | node: { 43 | __dirname: true, 44 | fs: "empty" 45 | } 46 | }; 47 | -------------------------------------------------------------------------------- /configs/webpack.client-watch.js: -------------------------------------------------------------------------------- 1 | var webpack = require("webpack"); 2 | var config = require("./webpack.client.js"); 3 | var wds = { 4 | hostname: process.env.HOSTNAME || "localhost", 5 | port: 8080 6 | }; 7 | 8 | config.cache = true; 9 | config.debug = true; 10 | config.devtool = "cheap-module-eval-source-map"; 11 | 12 | config.entry.unshift( 13 | "webpack-dev-server/client?http://" + wds.hostname + ":" + wds.port, 14 | "webpack/hot/only-dev-server" 15 | ); 16 | 17 | config.devServer = { 18 | publicPath: "http://" + wds.hostname + ":" + wds.port + "/dist", 19 | hot: true, 20 | inline: false, 21 | lazy: false, 22 | quiet: true, 23 | noInfo: true, 24 | headers: {"Access-Control-Allow-Origin": "*"}, 25 | stats: {colors: true}, 26 | host: wds.hostname 27 | }; 28 | 29 | config.output.publicPath = config.devServer.publicPath; 30 | config.output.hotUpdateMainFilename = "update/[hash]/update.json"; 31 | config.output.hotUpdateChunkFilename = "update/[hash]/[id].update.js"; 32 | 33 | config.plugins = [ 34 | new webpack.DefinePlugin({__CLIENT__: true, __SERVER__: false, __PRODUCTION__: false, __DEV__: true}), 35 | new webpack.HotModuleReplacementPlugin(), 36 | new webpack.NoErrorsPlugin() 37 | ]; 38 | 39 | config.module.postLoaders = [ 40 | {test: /\.js$/, loaders: ["babel?cacheDirectory&presets[]=es2015&presets[]=stage-0&presets[]=react&presets[]=react-hmre"], exclude: /node_modules/} 41 | ]; 42 | 43 | module.exports = config; 44 | -------------------------------------------------------------------------------- /src/components/Home.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import User from './User'; 4 | 5 | const Home = ({ styles, stargazers }) => ( 6 |
7 |

Features

8 | 19 |

20 | In short – an excellent choice. 21 | Ready to start{'?'} 22 |

23 |

24 | Community 25 |

26 | {stargazers.map((user, key) => { 27 | return 28 | })} 29 |
30 | ) 31 | 32 | /** 33 | * Connect to Redux store. 34 | */ 35 | export default connect( 36 | state => ({ 37 | stargazers: state.stargazers.users, 38 | }) 39 | )(Home) 40 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # BSD 3-Clause License 2 | 3 | Copyright © 2015, Rick Wong 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright 10 | notice, this list of conditions and the following disclaimer. 11 | 2. Redistributions in binary form must reproduce the above copyright 12 | notice, this list of conditions and the following disclaimer in the 13 | documentation and/or other materials provided with the distribution. 14 | 3. Neither the name of the copyright holder nor the 15 | names of its contributors may be used to endorse or promote products 16 | derived from this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 19 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY 22 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 23 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 24 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 25 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 27 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /src/containers/StargazersContainer.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import fetch from 'isomorphic-fetch'; 3 | import { connect } from 'react-redux'; 4 | import { fetchUsers, requestUsers } from '../actions/StargazersActions'; 5 | import Radium, { Style } from 'radium'; 6 | 7 | 8 | class StargazersContainer extends Component { 9 | 10 | componentWillMount = () => { 11 | /** 12 | * Start loading. 13 | */ 14 | this.props.dispatch(requestUsers()); 15 | /** 16 | * Start recursive loading. 17 | */ 18 | this.recursiveFetch(); 19 | }; 20 | 21 | componentDidUpdate = () => { 22 | const {stargazers, dispatch} = this.props; 23 | /** 24 | * Recursive fetch everytime component updates. 25 | */ 26 | if(stargazers.nextPage > 1 && stargazers.pagesToFetch > 0 && stargazers.isLoading === true){ 27 | this.recursiveFetch(); 28 | } 29 | }; 30 | 31 | /** 32 | * Function that dispatches the fetch action. 33 | */ 34 | recursiveFetch = () => { 35 | const {stargazers, dispatch} = this.props; 36 | dispatch(fetchUsers(stargazers.nextPage, stargazers.pagesToFetch)); 37 | }; 38 | 39 | /** 40 | * Render child routes and Radium's Style component, for css-like global styles. 41 | */ 42 | render() { 43 | return ( 44 |
45 |