├── .babelrc ├── .bootstraprc ├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .gitignore ├── .travis.yml ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── bin └── server.js ├── package.json ├── server.babel.js ├── src ├── actions │ └── index.js ├── client.js ├── components │ ├── Explore │ │ └── Explore.js │ ├── List │ │ ├── List.js │ │ └── List.scss │ ├── Repo │ │ └── Repo.js │ ├── User │ │ ├── User.js │ │ └── __tests__ │ │ │ └── User-test.js │ └── index.js ├── config.js ├── containers │ ├── App │ │ ├── App.js │ │ └── App.scss │ ├── DevTools │ │ └── DevTools.js │ ├── NotFound │ │ ├── NotFound.js │ │ └── NotFound.scss │ ├── RepoPage │ │ ├── RepoPage.js │ │ └── RepoPage.scss │ ├── Root │ │ ├── Root.dev.js │ │ ├── Root.js │ │ └── Root.prod.js │ ├── UserPage │ │ └── UserPage.js │ └── index.js ├── helpers │ └── Html.js ├── reducers │ ├── index.js │ ├── paginate.js │ └── selectors.js ├── routes.js ├── sagas │ ├── index.js │ └── waitAll.js ├── server.js ├── services │ ├── api │ │ └── index.js │ └── index.js ├── store │ ├── configureStore.dev.js │ ├── configureStore.js │ └── configureStore.prod.js └── theme │ ├── bootstrap.overrides.scss │ ├── font-awesome.config.js │ ├── font-awesome.config.less │ ├── font-awesome.config.prod.js │ ├── fonts.scss │ ├── style.scss │ └── variables.scss ├── static └── favicon.ico ├── styles.md ├── tests.webpack.js └── webpack ├── dev.config.js ├── prod.config.js ├── webpack-dev-server.js └── webpack-isomorphic-tools.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["react", "es2015", "stage-0"], 3 | 4 | "plugins": [ 5 | "transform-runtime", 6 | "add-module-exports", 7 | "transform-decorators-legacy", 8 | "transform-react-display-name" 9 | ], 10 | 11 | "env": { 12 | "development": { 13 | "plugins": [ 14 | "typecheck", 15 | ["react-transform", { 16 | "transforms": [{ 17 | "transform": "react-transform-catch-errors", 18 | "imports": ["react", "redbox-react"] 19 | } 20 | ] 21 | }] 22 | ] 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /.bootstraprc: -------------------------------------------------------------------------------- 1 | --- 2 | # Output debugging info 3 | # loglevel: debug 4 | 5 | # Major version of Bootstrap: 3 or 4 6 | bootstrapVersion: 3 7 | 8 | # If Bootstrap version 3 is used - turn on/off custom icon font path 9 | useCustomIconFontPath: false 10 | 11 | # Webpack loaders, order matters 12 | styleLoaders: 13 | - style 14 | - css 15 | - sass 16 | 17 | # Extract styles to stand-alone css file 18 | # Different settings for different environments can be used, 19 | # It depends on value of NODE_ENV environment variable 20 | # This param can also be set in webpack config: 21 | # entry: 'bootstrap-loader/extractStyles' 22 | # extractStyles: false 23 | env: 24 | development: 25 | extractStyles: false 26 | production: 27 | extractStyles: true 28 | 29 | # Customize Bootstrap variables that get imported before the original Bootstrap variables. 30 | # Thus original Bootstrap variables can depend on values from here. 31 | preBootstrapCustomizations: ./src/theme/variables.scss 32 | 33 | # This gets loaded after bootstrap/variables is loaded 34 | # So you can refer to bootstrap variables 35 | bootstrapCustomizations: ./src/theme/bootstrap.overrides.scss 36 | 37 | # With CSS Modules we load all application styles directly in React components 38 | # appStyles: ./app/styles/app.scss 39 | 40 | ### Bootstrap styles 41 | styles: 42 | 43 | # Mixins 44 | mixins: true 45 | 46 | # Reset and dependencies 47 | normalize: true 48 | print: true 49 | glyphicons: true 50 | 51 | # Core CSS 52 | scaffolding: true 53 | type: true 54 | code: true 55 | grid: true 56 | tables: true 57 | forms: true 58 | buttons: true 59 | 60 | # Components 61 | component-animations: true 62 | dropdowns: true 63 | button-groups: true 64 | input-groups: true 65 | navs: true 66 | navbar: true 67 | breadcrumbs: true 68 | pagination: true 69 | pager: true 70 | labels: true 71 | badges: true 72 | jumbotron: true 73 | thumbnails: true 74 | alerts: true 75 | progress-bars: true 76 | media: true 77 | list-group: true 78 | panels: true 79 | wells: true 80 | responsive-embed: true 81 | close: true 82 | 83 | # Components w/ JavaScript 84 | modals: true 85 | tooltip: true 86 | popovers: true 87 | carousel: true 88 | 89 | # Utility classes 90 | utilities: true 91 | responsive-utilities: true 92 | 93 | ### Bootstrap scripts 94 | scripts: false -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | indent_style = space 3 | end_of_line = lf 4 | indent_size = 2 5 | charset = utf-8 6 | trim_trailing_whitespace = true 7 | 8 | [*.md] 9 | max_line_length = 0 10 | trim_trailing_whitespace = false 11 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | webpack/* 2 | karma.conf.js 3 | tests.webpack.js 4 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { "extends": ["eslint-config-airbnb"], 2 | "env": { 3 | "browser": true, 4 | "node": true, 5 | "mocha": true 6 | }, 7 | "parser": "babel-eslint", 8 | "parserOptions": { 9 | "sourceType": "module" 10 | }, 11 | "rules": { 12 | "react/no-multi-comp": 0, 13 | "react/prefer-stateless-function": 0, 14 | "import/default": 0, 15 | "import/no-duplicates": 0, 16 | "import/named": 0, 17 | "import/namespace": 0, 18 | "import/no-unresolved": 0, 19 | "import/no-named-as-default": 2, 20 | "comma-dangle": 0, // not sure why airbnb turned this on. gross! 21 | "indent": [2, 2, {"SwitchCase": 1}], 22 | "no-console": 0, 23 | "no-alert": 0, 24 | "strict": 0, 25 | "arrow-body-style": [2, "as-needed"], 26 | "no-confusing-arrow": [2, {"allowParens": true}] 27 | }, 28 | "plugins": [ 29 | "react", "import" 30 | ], 31 | "settings": { 32 | "import/parser": "babel-eslint", 33 | "import/resolve": { 34 | "moduleDirectory": ["node_modules", "src"] 35 | } 36 | }, 37 | "globals": { 38 | "__DEVELOPMENT__": true, 39 | "__CLIENT__": true, 40 | "__SERVER__": true, 41 | "__DISABLE_SSR__": true, 42 | "__DEVTOOLS__": true, 43 | "socket": true, 44 | "webpackIsomorphicTools": true 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | node_modules/ 3 | dist/ 4 | *.iml 5 | webpack-assets.json 6 | webpack-stats.json 7 | npm-debug.log -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - "4" 5 | - "5" 6 | - "6" 7 | - "stable" 8 | 9 | sudo: false 10 | 11 | before_script: 12 | - export DISPLAY=:99.0 13 | - sh -e /etc/init.d/xvfb start 14 | 15 | script: 16 | - npm run lint 17 | - npm test 18 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Some basic conventions for contributing to this project. 4 | 5 | ### General 6 | 7 | Please make sure that there aren't existing pull requests attempting to address the issue mentioned. Likewise, please check for issues related to update, as someone else may be working on the issue in a branch or fork. 8 | 9 | * Non-trivial changes should be discussed in an issue first 10 | * Develop in a topic branch, not master 11 | * Squash your commits 12 | 13 | ### Linting 14 | 15 | Please check your code using `npm run lint` before submitting your pull requests, as the CI build will fail if `eslint` fails. 16 | 17 | ### Commit Message Format 18 | 19 | Each commit message should include a **type**, a **scope** and a **subject**: 20 | 21 | ``` 22 | (): 23 | ``` 24 | 25 | Lines should not exceed 100 characters. This allows the message to be easier to read on github as well as in various git tools and produces a nice, neat commit log ie: 26 | 27 | ``` 28 | #459 refactor(utils): create url mapper utility function 29 | #463 chore(webpack): update to isomorphic tools v2 30 | #494 fix(babel): correct dependencies and polyfills 31 | #510 feat(app): add react-bootstrap responsive navbar 32 | ``` 33 | 34 | #### Type 35 | 36 | Must be one of the following: 37 | 38 | * **feat**: A new feature 39 | * **fix**: A bug fix 40 | * **docs**: Documentation only changes 41 | * **style**: Changes that do not affect the meaning of the code (white-space, formatting, missing 42 | semi-colons, etc) 43 | * **refactor**: A code change that neither fixes a bug or adds a feature 44 | * **test**: Adding missing tests 45 | * **chore**: Changes to the build process or auxiliary tools and libraries such as documentation 46 | generation 47 | 48 | #### Scope 49 | 50 | The scope could be anything specifying place of the commit change. For example `webpack`, 51 | `helpers`, `api` etc... 52 | 53 | #### Subject 54 | 55 | The subject contains succinct description of the change: 56 | 57 | * use the imperative, present tense: "change" not "changed" nor "changes" 58 | * don't capitalize first letter 59 | * no dot (.) at the end 60 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Kawi Xiao 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # React Universal Saga 3 | 4 | [![React Universal Saga Version](https://img.shields.io/npm/v/react-universal-saga.svg?maxAge=2592000)](https://img.shields.io/npm/dm/react-universal-saga.svg?maxAge=2592000) 5 | [![Downloads](https://img.shields.io/npm/dm/react-universal-saga.svg?maxAge=2592000)](https://img.shields.io/npm/dm/react-universal-saga.svg?maxAge=2592000) 6 | [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://raw.githubusercontent.com/xkawi/react-universal-saga/master/LICENSE) 7 | 8 | 9 | [![Build Status](https://travis-ci.org/xkawi/react-universal-saga.svg?branch=master)](https://travis-ci.org/xkawi/react-universal-saga) 10 | [![Dependency Status](https://david-dm.org/xkawi/react-universal-saga.svg)](https://david-dm.org/xkawi/react-universal-saga) 11 | [![devDependency Status](https://david-dm.org/xkawi/react-universal-saga/dev-status.svg)](https://david-dm.org/xkawi/react-universal-saga#info=devDependencies) 12 | 13 | --- 14 | 15 | ## Getting Started 16 | 17 | Universal React App Starter featuring Redux Saga. Heavily modified version of [React Redux Universal Hot Example](https://github.com/erikras/react-redux-universal-hot-example) combined with Redux Saga [real-world](https://github.com/yelouafi/redux-saga/tree/master/examples/real-world) example. 18 | 19 | `$ git clone https://github.com/xkawi/react-universal-saga` 20 | 21 | `$ cd react-universal-saga && npm install` 22 | 23 | `$ npm run dev` (run in development mode) 24 | 25 | Live Demo: [react-universal-saga.herokuapp.com](https://react-universal-saga.herokuapp.com) 26 | 27 | UPDATE: Checkout [react-universal-saga-modular](https://github.com/xkawi/react-universal-saga-modular) for a different structure of react-universal-saga that's more modular, scalable, and maintainable. :smile: 28 | 29 | ## Features 30 | 31 | * [Universal](https://medium.com/@mjackson/universal-javascript-4761051b7ae9) rendering, with Client and Server Side Data Fetching 32 | * [React](https://github.com/facebook/react) - latest version `^15.4.2` 33 | * [Redux](https://github.com/rackt/redux)'s futuristic [Flux](https://facebook.github.io/react/blog/2014/05/06/flux.html) implementation 34 | * [Redux Saga](https://github.com/yelouafi/redux-saga) to handle all of your Side Effects logic in a central place 35 | * [React Router](https://github.com/ReactTraining/react-router/tree/v2.8.1) 36 | * [Express](http://expressjs.com) 37 | * [Babel](http://babeljs.io) for ES6 and ES7 magic 38 | * [Webpack](http://webpack.github.io) for bundling 39 | * [Webpack Dev Middleware](http://webpack.github.io/docs/webpack-dev-middleware.html) 40 | * [Webpack Hot Middleware](https://github.com/glenjamin/webpack-hot-middleware) 41 | * [Redux Dev Tools](https://github.com/gaearon/redux-devtools) for next generation DX (developer experience). Watch [Dan Abramov's talk](https://www.youtube.com/watch?v=xsSnOQynTHs). 42 | * [ESLint](http://eslint.org) to maintain a consistent code style 43 | * [redux-form](https://github.com/erikras/redux-form) to manage form state in Redux 44 | * [PostCSS](https://github.com/postcss/postcss-loader), [style-loader](https://github.com/webpack/style-loader), [sass-loader](https://github.com/jtangelder/sass-loader) and [less-loader](https://github.com/webpack/less-loader) to allow import of stylesheets in plain css, sass and less, 45 | * [bootstrap-loader](https://github.com/shakacode/bootstrap-loader) and [font-awesome-webpack](https://github.com/gowravshekar/font-awesome-webpack) to customize Bootstrap and FontAwesome 46 | * [react-helmet](https://github.com/nfl/react-helmet) to manage title and meta tag information on both server and client 47 | * [webpack-isomorphic-tools](https://github.com/halt-hammerzeit/webpack-isomorphic-tools) to allow require() work for statics both on client and server 48 | * [Jest](https://facebook.github.io/jest/) to allow writing unit tests for the project. 49 | * Refer to `package.json` for more details 50 | 51 | ## New to React? 52 | 53 | Core Concepts to learn if you are new to ReactJS and this repo: 54 | 55 | - Dumb Components (Component) 56 | - Smart Component (Container) 57 | - Actions - define available action types 58 | - Reducers - given previous state and an action, create a new state 59 | - Sagas - manage or orchestrate event/action dispatching flow (called Side Effects) 60 | 61 | ### Crash Course on React, Redux, and Redux Saga 62 | 63 | 1. [Hands-on] [Get started with ReactJS](https://facebook.github.io/react/docs/tutorial.html) 64 | 65 | 2. [Reading] [Thinking in React way](https://facebook.github.io/react/docs/thinking-in-react.html) 66 | 67 | 3. [Reading] [Flux Pattern](https://code-cartoons.com/a-cartoon-guide-to-flux-6157355ab207#3c82) 68 | [(video)](https://www.youtube.com/watch?v=nYkdrAPrdcw&feature=youtu.be) 69 | 70 | 4. [Hand-on] [Flux](https://facebook.github.io/flux/docs/todo-list.html) 71 | 72 | 5. [Reading] [Why someone created a tool called Redux](https://code-cartoons.com/hot-reloading-and-time-travel-debugging-what-are-they-3c8ed2812f35) 73 | 74 | 6. [Reading] [More about Redux](https://code-cartoons.com/a-cartoon-intro-to-redux-3afb775501a6) 75 | 76 | 7. [Hands-on] [Getting started with Redux](https://egghead.io/series/getting-started-with-redux) 77 | 78 | 8. [Hands-on] [More comprehensive hands-on with Redux and React](http://teropa.info/blog/2015/09/10/full-stack-redux-tutorial.html) 79 | 80 | 9. [Reading] [Redux best practices](https://medium.com/lexical-labs-engineering/redux-best-practices-64d59775802e#.v34syy9ia) 81 | 82 | 10. [Reading + Hands-on] [Redux Saga](http://yelouafi.github.io/redux-saga/) 83 | 84 | 11. Everything - [Awesome Redux](https://github.com/xgrommx/awesome-redux) 85 | 86 | ## Development Workflow 87 | 88 | Here is a suggested development workflow that works for me: 89 | 90 | 1. Define the routes, which helps to reason about the needed containers and components. 91 | 92 | 2. Implement dump components + props (state and actions). 93 | * Each component should define `.propTypes` for prop's validation, which will throw a warning if the container that uses it do not provides all the props that the component need. 94 | 95 | 3. Often, a container = a route page; a container is also where you import necessary components, actions and states (pass down to component) to form a single page. 96 | * if a container wants to "pre-populate" data during server rendering process, assign `Container.preload` with sagas that fetches necessary data from the API server. (refer to `containers/App.js` for an example) 97 | 98 | 4. Implement necessary Actions as you define needed containers and components (PS: this implementation is independent from previous steps). It often helps maintainability and readability if action methods are categorized into 2: 99 | * Actions that manipulate store directly through reducers (e.g. `actions/index.js > createRequestTypes` method), often to update the state and trigger re-render the components. 100 | * "LOAD" or "TRIGGER" actions for starting a saga routine/daemon, often to make network call. 101 | 102 | 5. Implement Reducers. As you implemented the actions, you will have better idea on how to modify the states for different actions. 103 | 104 | ### Using Redux DevTools 105 | 106 | [Redux Devtools](https://github.com/gaearon/redux-devtools) are enabled by default in development. 107 | 108 | - CTRL+H Toggle DevTools Dock 109 | - CTRL+Q Move DevTools Dock Position 110 | - see [redux-devtools-dock-monitor](https://github.com/gaearon/redux-devtools-dock-monitor) for more detailed information. 111 | 112 | If you have the 113 | [Redux DevTools chrome extension](https://chrome.google.com/webstore/detail/redux-devtools/lmhkpmbekcpmknklioeibfkpmmfibljd) installed it will automatically be used on the client-side instead. 114 | 115 | If you want to disable the dev tools during development, set `__DEVTOOLS__` to `false` in `/webpack/dev.config.js`. 116 | DevTools are not enabled during production. 117 | 118 | ## Deployment 119 | 120 | `$ npm run build` 121 | 122 | `$ npm run start` 123 | 124 | ## How Everything Works? 125 | 126 | What initially gets run is `bin/server.js`, which does little more than enable ES6 and ES7 awesomeness in the 127 | server-side node code. It then initiates `server.js`. In `server.js` we perform data fetching using Redux Saga ([details](https://github.com/yelouafi/redux-saga/issues/13#ref-commit-3e8321c)). Aside from serving the favicon and static content from `/static`, the only thing `server.js` does is initiate delegate rendering to `react-router`. At the bottom of `server.js`, we listen to port `3000` and initiate the API server. 128 | 129 | ### Routing and HTML return 130 | 131 | The primary section of `server.js` generates an HTML page with the contents returned by `react-router`. Then we perform [server-side data fetching](#server-side-data-fetching), wait for the data to be loaded, and render the page with the now-fully-loaded `redux` state. 132 | 133 | The last interesting bit of the main routing section of `server.js` is that we swap in the hashed script and css from the `webpack-assets.json` that the Webpack Dev Server – or the Webpack build process on production – has spit out on its last run. You won't have to deal with `webpack-assets.json` manually because [webpack-isomorphic-tools](https://github.com/halt-hammerzeit/webpack-isomorphic-tools) take care of that. 134 | 135 | We also spit out the `redux` state into a global `window.__data` variable in the webpage to be loaded by the client-side `redux` code. 136 | 137 | ### Server-side Data Fetching 138 | 139 | The [redux-saga](https://github.com/yelouafi/redux-saga) provides a mechanism for server-side data fetching from the actual backend API servers/services, when it reaches client-side (React) there is no need to do additional network call. You have to define the Sagas that a container need (refers to `containers/UserPage.js > UserPage.preload` for example) for server-side to fetch. PS: You have the flexibility to implement additional logic (e.g. handle authentication) when fetching data at server-side rendering stage, as it differs from client-side. 140 | 141 | ### Client Side 142 | 143 | The client side entry point is reasonably named `client.js`. All it does is load the routes, initiate `react-router`, rehydrate the redux state from the `window.__data` passed in from the server, and render the page over top of the server-rendered DOM. This makes React enable all its event listeners without having to re-render the DOM. 144 | 145 | ### Redux Middleware 146 | 147 | Currently, we only use Saga Middleware and Logger Middleware (for development). If you need to use or add custom middlewares, you can do so by modifying `store/configureStore.dev.js` (for dev env) or `store/configureStore.prod.js` (for prod env). 148 | 149 | ### Handling Authentication 150 | 151 | If you want to implement authentication/authorization feature, follow this issue posted on redux-saga repo - [Question: Authentication flow](https://github.com/yelouafi/redux-saga/issues/14) - it is my main source of reference. 152 | 153 | ### Unit Tests 154 | 155 | The project uses [Jest](https://facebook.github.io/jest) to run your unit tests and the [Test Utilities](http://facebook.github.io/react/docs/test-utils.html) from Facebook api. 156 | 157 | An example is available at `components > User`. 158 | 159 | To run the tests in the project, just simply run `npm test`. 160 | 161 | ## Styling 162 | 163 | ### Local Styles 164 | 165 | This project uses [local styles](https://medium.com/seek-ui-engineering/the-end-of-global-css-90d2a4a06284) using [css-loader](https://github.com/webpack/css-loader). The way it works is that you import your stylesheet at the top of the `render()` function in your React Component, and then you use the classnames returned from that import. Like so: 166 | 167 | `render() { const styles = require('./App.scss'); }` 168 | 169 | Then you set the `className` of your element to match one of the CSS classes in your SCSS file, and you're good to go! 170 | 171 | `
...
` 172 | 173 | ### Global Style Variables 174 | 175 | `react-universal-saga` support global style variables by defining the variable in `theme/style.scss`. Once defined, you can use in any scss file so long it is imported (refer to `RepoPage.scss` for example). 176 | 177 | ## Notable Alternatives 178 | 179 | * [Next.js](https://github.com/zeit/next.js) 180 | * [create-react-app](https://github.com/facebookincubator/create-react-app) 181 | * [react-universally](https://github.com/ctrlplusb/react-universally) 182 | * [react-boilerplate](https://github.com/mxstbr/react-boilerplate) 183 | * [react-server](https://github.com/redfin/react-server) 184 | * [retax](https://github.com/retaxJS/retax) 185 | 186 | Any contribution is welcome. 187 | 188 | Cheers, 189 | 190 | Kawi 191 | -------------------------------------------------------------------------------- /bin/server.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | require('../server.babel'); // babel registration (runtime transpilation for node) 3 | var path = require('path'); 4 | var rootDir = path.resolve(__dirname, '..'); 5 | /** 6 | * Define isomorphic constants. 7 | */ 8 | global.__CLIENT__ = false; 9 | global.__SERVER__ = true; 10 | global.__DISABLE_SSR__ = false; // <----- DISABLES SERVER SIDE RENDERING FOR ERROR DEBUGGING 11 | global.__DEVELOPMENT__ = process.env.NODE_ENV !== 'production'; 12 | 13 | // https://github.com/halt-hammerzeit/webpack-isomorphic-tools 14 | var WebpackIsomorphicTools = require('webpack-isomorphic-tools'); 15 | global.webpackIsomorphicTools = new WebpackIsomorphicTools(require('../webpack/webpack-isomorphic-tools')) 16 | .server(rootDir, function() { 17 | require('../src/server'); 18 | }); 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-universal-saga", 3 | "description": "Universal React App Starter ft. Redux Saga", 4 | "author": "Kawi Xiao (http://github.com/xkawi)", 5 | "version": "1.1.1", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/xkawi/react-universal-saga" 9 | }, 10 | "license": "MIT", 11 | "bugs": { 12 | "url": "https://github.com/xkawi/react-universal-saga/issues" 13 | }, 14 | "keywords": [ 15 | "react", 16 | "isomorphic", 17 | "universal", 18 | "webpack", 19 | "express", 20 | "hot reloading", 21 | "react-hot-reloader", 22 | "redux", 23 | "starter", 24 | "boilerplate", 25 | "babel", 26 | "saga", 27 | "redux saga" 28 | ], 29 | "main": "bin/server.js", 30 | "scripts": { 31 | "start": "better-npm-run start-prod", 32 | "build": "better-npm-run build", 33 | "postinstall": "npm run build", 34 | "lint": "eslint -c .eslintrc src", 35 | "start-dev": "better-npm-run start-dev", 36 | "watch-client": "better-npm-run watch-client", 37 | "dev": "concurrently --kill-others \"npm run watch-client\" \"npm run start-dev\"", 38 | "test": "jest" 39 | }, 40 | "jest": { 41 | "globals": { 42 | "__SERVER__": false 43 | }, 44 | "moduleFileExtensions": [ 45 | "", 46 | "json", 47 | "js", 48 | "jsx" 49 | ], 50 | "moduleDirectories": [ 51 | "src", 52 | "node_modules" 53 | ] 54 | }, 55 | "betterScripts": { 56 | "start-prod": { 57 | "command": "node ./bin/server.js", 58 | "env": { 59 | "NODE_PATH": "./src", 60 | "NODE_ENV": "production", 61 | "PORT": 8080 62 | } 63 | }, 64 | "start-dev": { 65 | "command": "node ./bin/server.js", 66 | "env": { 67 | "NODE_PATH": "./src", 68 | "NODE_ENV": "development", 69 | "PORT": 3000 70 | } 71 | }, 72 | "watch-client": { 73 | "command": "node webpack/webpack-dev-server.js", 74 | "env": { 75 | "UV_THREADPOOL_SIZE": 100, 76 | "NODE_PATH": "./src", 77 | "PORT": 3000 78 | } 79 | }, 80 | "build": { 81 | "command": "webpack --verbose --colors --display-error-details --config webpack/prod.config.js", 82 | "env": { 83 | "NODE_ENV": "production" 84 | } 85 | } 86 | }, 87 | "dependencies": { 88 | "compression": "^1.6.0", 89 | "express": "^4.13.3", 90 | "express-http-proxy": "^0.11.0", 91 | "file-loader": "^0.11.1", 92 | "hoist-non-react-statics": "^1.2.0", 93 | "humps": "^2.0.0", 94 | "invariant": "^2.2.1", 95 | "isomorphic-fetch": "^2.2.1", 96 | "less": "^2.5.3", 97 | "less-loader": "^2.2.1", 98 | "lodash": "^4.13.1", 99 | "normalizr": "^3.2.2", 100 | "react": "^15.4.2", 101 | "react-dom": "^15.4.2", 102 | "react-ga": "^2.1.0", 103 | "react-helmet": "^5.0.2", 104 | "react-redux": "^5.0.3", 105 | "react-router": "^2.5.2", 106 | "redux": "^3.5.2", 107 | "redux-logger": "^3.0.1", 108 | "redux-saga": "^0.15.3", 109 | "scroll-behavior": "^0.8.2", 110 | "serialize-javascript": "^1.1.2", 111 | "serve-favicon": "^2.3.0", 112 | "url-loader": "^0.5.7", 113 | "warning": "^3.0.0", 114 | "webpack-isomorphic-tools": "^3.0.2" 115 | }, 116 | "devDependencies": { 117 | "autoprefixer": "^6.3.7", 118 | "babel-core": "^6.10.4", 119 | "babel-eslint": "^7.0.0", 120 | "babel-loader": "^6.2.4", 121 | "babel-plugin-add-module-exports": "^0.2.1", 122 | "babel-plugin-react-transform": "^2.0.2", 123 | "babel-plugin-transform-decorators-legacy": "^1.3.4", 124 | "babel-plugin-transform-react-display-name": "^6.8.0", 125 | "babel-plugin-transform-runtime": "^6.9.0", 126 | "babel-plugin-typecheck": "^3.9.0", 127 | "babel-preset-es2015": "^6.9.0", 128 | "babel-preset-react": "^6.11.1", 129 | "babel-preset-stage-0": "^6.5.0", 130 | "babel-register": "^6.9.0", 131 | "babel-runtime": "^6.11.6", 132 | "better-npm-run": "0.0.11", 133 | "bootstrap-loader": "^1.0.10", 134 | "bootstrap-sass": "^3.3.5", 135 | "clean-webpack-plugin": "^0.1.6", 136 | "concurrently": "^3.1.0", 137 | "eslint": "2.13.1", 138 | "eslint-config-airbnb": "9.0.1", 139 | "css-loader": "^0.25.0", 140 | "eslint-loader": "^1.0.0", 141 | "eslint-plugin-import": "^1.9.2", 142 | "eslint-plugin-jsx-a11y": "^1.5.3", 143 | "eslint-plugin-react": "^5.2.2", 144 | "extract-text-webpack-plugin": "^1.0.1", 145 | "font-awesome": "^4.4.0", 146 | "font-awesome-webpack": "0.0.4", 147 | "jest": "^16.0.1", 148 | "json-loader": "^0.5.4", 149 | "node-sass": "^3.4.2", 150 | "postcss-loader": "^0.9.1", 151 | "precss": "^1.4.0", 152 | "react-a11y": "^0.3.3", 153 | "react-addons-test-utils": "^15.2.0", 154 | "react-transform-catch-errors": "^1.0.2", 155 | "react-transform-hmr": "^1.0.4", 156 | "redbox-react": "^1.2.10", 157 | "redux-devtools": "^3.3.1", 158 | "redux-devtools-dock-monitor": "^1.1.1", 159 | "redux-devtools-log-monitor": "^1.0.11", 160 | "resolve-url-loader": "^1.6.0", 161 | "sass-loader": "^4.0.0", 162 | "strip-loader": "^0.1.2", 163 | "style-loader": "^0.13.1", 164 | "timekeeper": "0.1.1", 165 | "webpack": "^1.13.1", 166 | "webpack-dev-middleware": "^1.6.1", 167 | "webpack-hot-middleware": "^2.12.0" 168 | }, 169 | "engines": { 170 | "node": "7.x.x" 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /server.babel.js: -------------------------------------------------------------------------------- 1 | // enable runtime transpilation to use ES6/7 in node 2 | 3 | var fs = require('fs'); 4 | 5 | var babelrc = fs.readFileSync('./.babelrc'); 6 | var config; 7 | 8 | try { 9 | config = JSON.parse(babelrc); 10 | } catch (err) { 11 | console.error('==> ERROR: Error parsing your .babelrc.'); 12 | console.error(err); 13 | } 14 | 15 | require('babel-register')(config); 16 | -------------------------------------------------------------------------------- /src/actions/index.js: -------------------------------------------------------------------------------- 1 | 2 | const REQUEST = 'REQUEST'; 3 | const SUCCESS = 'SUCCESS'; 4 | const FAILURE = 'FAILURE'; 5 | 6 | function createRequestTypes(base) { 7 | const res = {}; 8 | //eslint-disable-next-line 9 | [REQUEST, SUCCESS, FAILURE].forEach(type => res[type] = `${base}_${type}`); 10 | return res; 11 | } 12 | 13 | export const USER = createRequestTypes('USER'); 14 | export const REPO = createRequestTypes('REPO'); 15 | export const STARRED = createRequestTypes('STARRED'); 16 | export const STARGAZERS = createRequestTypes('STARGAZERS'); 17 | 18 | export const UPDATE_ROUTER_STATE = 'UPDATE_ROUTER_STATE'; 19 | export const NAVIGATE = 'NAVIGATE'; 20 | export const LOAD_USER_PAGE = 'LOAD_USER_PAGE'; 21 | export const LOAD_REPO_PAGE = 'LOAD_REPO_PAGE'; 22 | export const LOAD_MORE_STARRED = 'LOAD_MORE_STARRED'; 23 | export const LOAD_MORE_STARGAZERS = 'LOAD_MORE_STARGAZERS'; 24 | export const RESET_ERROR_MESSAGE = 'RESET_ERROR_MESSAGE'; 25 | 26 | function action(type, payload = {}) { 27 | return { type, ...payload }; 28 | } 29 | 30 | export const user = { 31 | request: (login) => action(USER.REQUEST, { login }), 32 | success: (login, response) => action(USER.SUCCESS, { login, response }), 33 | failure: (login, error) => action(USER.FAILURE, { login, error }), 34 | }; 35 | 36 | export const repo = { 37 | request: (fullName) => action(REPO.REQUEST, { fullName }), 38 | success: (fullName, response) => action(REPO.SUCCESS, { fullName, response }), 39 | failure: (fullName, error) => action(REPO.FAILURE, { fullName, error }), 40 | }; 41 | 42 | export const starred = { 43 | request: (login) => action(STARRED.REQUEST, { login }), 44 | success: (login, response) => action(STARRED.SUCCESS, { login, response }), 45 | failure: (login, error) => action(STARRED.FAILURE, { login, error }), 46 | }; 47 | 48 | export const stargazers = { 49 | request: (fullName) => action(STARGAZERS.REQUEST, { fullName }), 50 | success: (fullName, response) => action(STARGAZERS.SUCCESS, { fullName, response }), 51 | failure: (fullName, error) => action(STARGAZERS.FAILURE, { fullName, error }), 52 | }; 53 | 54 | export const updateRouterState = (state) => action(UPDATE_ROUTER_STATE, { state }); 55 | export const navigate = (pathname) => action(NAVIGATE, { pathname }); 56 | export const loadUserPage = (login, requiredFields = []) => { 57 | const payload = { login, requiredFields }; 58 | return action(LOAD_USER_PAGE, payload); 59 | }; 60 | export const loadRepoPage = (fullName, requiredFields = []) => { 61 | const payload = { fullName, requiredFields }; 62 | return action(LOAD_REPO_PAGE, payload); 63 | }; 64 | export const loadMoreStarred = (login) => action(LOAD_MORE_STARRED, { login }); 65 | export const loadMoreStargazers = (fullName) => action(LOAD_MORE_STARGAZERS, { fullName }); 66 | 67 | export const resetErrorMessage = () => action(RESET_ERROR_MESSAGE); 68 | -------------------------------------------------------------------------------- /src/client.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from 'react-dom'; 3 | import GoogleAnalytics from 'react-ga'; 4 | 5 | import { Root } from 'containers'; 6 | import rootSaga from './sagas'; 7 | import getRoutes from './routes'; 8 | import { history } from './services'; 9 | import configureStore from './store/configureStore'; 10 | import config from './config'; 11 | 12 | const dest = document.getElementById('content'); 13 | const store = configureStore(window.__data); // eslint-disable-line 14 | 15 | GoogleAnalytics.initialize(config.app.googleAnalytics.appId); 16 | 17 | store.runSaga(rootSaga); 18 | 19 | render( 20 | , 25 | dest 26 | ); 27 | 28 | if (process.env.NODE_ENV !== 'production') { 29 | window.React = React; // enable debugger 30 | } 31 | -------------------------------------------------------------------------------- /src/components/Explore/Explore.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | 3 | const GITHUB_REPO = 'https://github.com/xkawi/react-universal-saga'; 4 | 5 | export default class Explore extends Component { 6 | constructor(props) { 7 | super(props); 8 | this.handleKeyUp = this.handleKeyUp.bind(this); 9 | this.handleGoClick = this.handleGoClick.bind(this); 10 | } 11 | 12 | componentWillReceiveProps(nextProps) { 13 | if (nextProps.value !== this.props.value) { 14 | this.setInputValue(nextProps.value); 15 | } 16 | } 17 | 18 | getInputValue() { 19 | return this.refs.input.value; 20 | } 21 | 22 | setInputValue(val) { 23 | // Generally mutating DOM is a bad idea in React components, 24 | // but doing this for a single uncontrolled field is less fuss 25 | // than making it controlled and maintaining a state for it. 26 | this.refs.input.value = val; 27 | } 28 | 29 | handleKeyUp(e) { 30 | if (e.keyCode === 13) { 31 | this.handleGoClick(); 32 | } 33 | } 34 | 35 | handleGoClick() { 36 | this.props.onChange(this.getInputValue()); 37 | } 38 | 39 | render() { 40 | return ( 41 |
42 |

Type a username or repo full name and hit 'Go':

43 | 44 | 45 |

Code on Github.

46 |

Move the DevTools with Ctrl+Q or hide them with Ctrl+H.

47 |
48 | ); 49 | } 50 | } 51 | 52 | Explore.propTypes = { 53 | value: PropTypes.string.isRequired, 54 | onChange: PropTypes.func.isRequired 55 | }; 56 | -------------------------------------------------------------------------------- /src/components/List/List.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | 3 | export default class List extends Component { 4 | renderLoadMore() { 5 | const { isFetching, onLoadMoreClick } = this.props; 6 | return ( 7 | 10 | ); 11 | } 12 | 13 | render() { 14 | const { 15 | isFetching, nextPageUrl, pageCount, 16 | items, renderItem, loadingLabel 17 | } = this.props; 18 | 19 | const isEmpty = items.length === 0; 20 | if (isEmpty && isFetching) { 21 | return (

{loadingLabel}

); 22 | } 23 | 24 | const isLastPage = !nextPageUrl; 25 | if (isEmpty && isLastPage) { 26 | return (

Nothing here!

); 27 | } 28 | 29 | return ( 30 |
31 | {items.map(renderItem)} 32 | {pageCount > 0 && !isLastPage && this.renderLoadMore()} 33 |
34 | ); 35 | } 36 | } 37 | 38 | List.propTypes = { 39 | loadingLabel: PropTypes.string.isRequired, 40 | pageCount: PropTypes.number, 41 | renderItem: PropTypes.func.isRequired, 42 | items: PropTypes.array.isRequired, 43 | isFetching: PropTypes.bool.isRequired, 44 | onLoadMoreClick: PropTypes.func.isRequired, 45 | nextPageUrl: PropTypes.string 46 | }; 47 | 48 | List.defaultProps = { 49 | isFetching: true, 50 | loadingLabel: 'Loading...' 51 | }; 52 | -------------------------------------------------------------------------------- /src/components/List/List.scss: -------------------------------------------------------------------------------- 1 | @import '../../theme/style'; 2 | 3 | .loading-container { 4 | color: grey; 5 | margin-top: 40px; 6 | margin-bottom: 40px; 7 | width: 100%; 8 | text-align: center; 9 | letter-spacing: 1px; 10 | .content { 11 | margin: 0 auto; 12 | .spinner { 13 | color: grey; 14 | margin-right: 20px; 15 | } 16 | .error { 17 | color: red; 18 | margin-right: 20px; 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/components/Repo/Repo.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import { Link } from 'react-router'; 3 | 4 | export default class Repo extends Component { 5 | render() { 6 | const { repo, owner } = this.props; 7 | const { login } = owner; 8 | const { name, description } = repo; 9 | 10 | return ( 11 |
12 |

13 | 14 | {name} 15 | 16 | {' by '} 17 | 18 | {login} 19 | 20 |

21 | {description && 22 |

{description}

23 | } 24 |
25 | ); 26 | } 27 | } 28 | 29 | Repo.propTypes = { 30 | repo: PropTypes.shape({ 31 | name: PropTypes.string.isRequired, 32 | description: PropTypes.string 33 | }).isRequired, 34 | owner: PropTypes.shape({ 35 | login: PropTypes.string.isRequired 36 | }).isRequired 37 | }; 38 | -------------------------------------------------------------------------------- /src/components/User/User.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import { Link } from 'react-router'; 3 | 4 | export default class User extends Component { 5 | render() { 6 | const { login, avatarUrl, name } = this.props.user; 7 | 8 | return ( 9 |
10 | 11 | {name} 12 |

13 | {login} {name && ({name})} 14 |

15 | 16 |
17 | ); 18 | } 19 | } 20 | 21 | User.propTypes = { 22 | user: PropTypes.shape({ 23 | login: PropTypes.string.isRequired, 24 | avatarUrl: PropTypes.string.isRequired, 25 | name: PropTypes.string 26 | }).isRequired 27 | }; 28 | -------------------------------------------------------------------------------- /src/components/User/__tests__/User-test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | describe('User', () => { 3 | it('assert equal a string', () => { 4 | const aString = "a"; 5 | expect(aString).toEqual("a"); 6 | }); 7 | }); -------------------------------------------------------------------------------- /src/components/index.js: -------------------------------------------------------------------------------- 1 | export List from './List/List'; 2 | export Explore from './Explore/Explore'; 3 | export User from './User/User'; 4 | export Repo from './Repo/Repo'; 5 | -------------------------------------------------------------------------------- /src/config.js: -------------------------------------------------------------------------------- 1 | const environment = { 2 | development: { 3 | isProduction: false 4 | }, 5 | production: { 6 | isProduction: true 7 | } 8 | }[process.env.NODE_ENV || 'development']; 9 | 10 | module.exports = Object.assign({ 11 | host: process.env.HOST || 'localhost', 12 | port: process.env.PORT || '3000', 13 | apiBaseUrl: process.env.API_URL || 'https://api.github.com', 14 | app: { 15 | googleAnalytics: { 16 | appId: process.env.GOOGLE_ANALYTIC_ID || 'UA-XXXXXXXX-X' 17 | }, 18 | title: 'React Universal Saga', 19 | description: 'Universal React Starter Kit ft. Redux Saga', 20 | head: { 21 | titleTemplate: 'React Universal Saga', 22 | meta: [ 23 | { name: 'description', content: 'Universal React Starter Kit ft. Redux Saga' }, 24 | { charset: 'utf-8' }, 25 | { property: 'og:site_name', content: 'React Universal Saga' }, 26 | { property: 'og:image', content: 'https://facebook.github.io/react/img/logo_og.png' }, 27 | { property: 'og:locale', content: 'en_US' }, 28 | { property: 'og:title', content: 'React Universal Saga' }, 29 | { property: 'og:description', content: 'Universal React Starter Kit ft. Redux Saga' }, 30 | { property: 'og:card', content: 'summary' }, 31 | { property: 'og:site', content: '@xkawi' }, 32 | { property: 'og:creator', content: '@xkawi' }, 33 | { property: 'og:image:width', content: '200' }, 34 | { property: 'og:image:height', content: '200' } 35 | ] 36 | } 37 | }, 38 | 39 | }, environment); 40 | -------------------------------------------------------------------------------- /src/containers/App/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import Helmet from 'react-helmet'; 4 | import { Explore } from 'components'; 5 | 6 | import { 7 | navigate, 8 | updateRouterState, 9 | resetErrorMessage 10 | } from '../../actions'; 11 | import styles from './App.scss'; // eslint-disable-line 12 | 13 | class App extends Component { 14 | constructor(props) { 15 | super(props); 16 | this.handleChange = this.handleChange.bind(this); 17 | } 18 | 19 | componentWillMount() { 20 | this.props.updateRouterState({ 21 | pathname: this.props.location.pathname, 22 | params: this.props.params 23 | }); 24 | } 25 | 26 | componentWillReceiveProps(nextProps) { 27 | if (nextProps.errorMessage) { 28 | // handle error here 29 | } 30 | if (this.props.location.pathname !== nextProps.location.pathname) { 31 | this.props.updateRouterState({ 32 | pathname: nextProps.location.pathname, 33 | params: nextProps.params 34 | }); 35 | } 36 | } 37 | 38 | handleDismissClick(e) { 39 | this.props.resetErrorMessage(); 40 | e.preventDefault(); 41 | } 42 | 43 | handleChange(nextValue) { 44 | this.props.navigate(`/${nextValue}`); 45 | } 46 | 47 | render() { 48 | const { children, inputValue } = this.props; 49 | return ( 50 |
51 | 55 | 56 |
57 | {children} 58 |
59 |
60 | ); 61 | } 62 | } 63 | 64 | App.propTypes = { 65 | errorMessage: PropTypes.string, 66 | inputValue: PropTypes.string.isRequired, 67 | navigate: PropTypes.func.isRequired, 68 | updateRouterState: PropTypes.func.isRequired, 69 | resetErrorMessage: PropTypes.func.isRequired, 70 | children: PropTypes.node, 71 | location: PropTypes.shape({ 72 | pathname: PropTypes.string 73 | }), 74 | params: PropTypes.object 75 | }; 76 | 77 | // function preload() { 78 | // return [ 79 | // [sagaName] 80 | // ]; 81 | // } 82 | // App.preload = preload; 83 | 84 | function mapStateToProps(state) { 85 | return { 86 | errorMessage: state.errorMessage, 87 | inputValue: state.router.pathname.substring(1) 88 | }; 89 | } 90 | 91 | export default connect(mapStateToProps, { 92 | navigate, 93 | updateRouterState, 94 | resetErrorMessage 95 | })(App); 96 | -------------------------------------------------------------------------------- /src/containers/App/App.scss: -------------------------------------------------------------------------------- 1 | // eslint-disable 2 | @import '~bootstrap-sass/assets/stylesheets/bootstrap/mixins'; 3 | 4 | .app { 5 | display: flex; 6 | min-height: 100vh; 7 | flex-direction: column; 8 | margin: 10px 10px; 9 | .brand { 10 | position: absolute; 11 | $size: 40px; 12 | top: 5px; 13 | left: 5px; 14 | display: inline-block; 15 | background: #1daede no-repeat center center; 16 | width: $size; 17 | height: $size; 18 | background-size: 80%; 19 | margin: 0 10px 0 0; 20 | border-radius: $size / 2; 21 | } 22 | nav :global(.fa) { 23 | font-size: 2em; 24 | line-height: 20px; 25 | } 26 | .content { 27 | flex: 1; 28 | width: 100%; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/containers/DevTools/DevTools.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { createDevTools } from 'redux-devtools'; 3 | import LogMonitor from 'redux-devtools-log-monitor'; 4 | import DockMonitor from 'redux-devtools-dock-monitor'; 5 | 6 | export default createDevTools( 7 | 8 | 9 | 10 | ); 11 | -------------------------------------------------------------------------------- /src/containers/NotFound/NotFound.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default function NotFound() { 4 | return ( 5 |
6 |

Doh! 404!

7 |

These are not the droids you are looking for!

8 |
9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /src/containers/NotFound/NotFound.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | color: grey; 3 | margin-top: 40px; 4 | margin-bottom: 40px; 5 | width: 100%; 6 | letter-spacing: 1px; 7 | display: flex; 8 | flex-direction: row; 9 | justify-content: center; 10 | align-items: center; 11 | .text { 12 | margin-left: 20px; 13 | display: flex; 14 | flex-direction: column; 15 | } 16 | .logo { 17 | color: grey; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/containers/RepoPage/RepoPage.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { loadRepoPage, loadMoreStargazers } from '../../actions'; 4 | import { Repo, User, List } from 'components'; 5 | import styles from './RepoPage.scss'; // eslint-disable-line 6 | 7 | class RepoPage extends Component { 8 | constructor(props) { 9 | super(props); 10 | this.renderUser = this.renderUser.bind(this); 11 | this.handleLoadMoreClick = this.handleLoadMoreClick.bind(this); 12 | } 13 | 14 | componentWillMount() { 15 | this.props.loadRepoPage(this.props.fullName); 16 | } 17 | 18 | componentWillReceiveProps(nextProps) { 19 | if (nextProps.fullName !== this.props.fullName) { 20 | this.props.loadRepoPage(nextProps.fullName); 21 | } 22 | } 23 | 24 | handleLoadMoreClick() { 25 | this.props.loadMoreStargazers(this.props.fullName); 26 | } 27 | 28 | renderUser(user) { 29 | return (); 30 | } 31 | 32 | render() { 33 | const { repo, owner, name } = this.props; 34 | if (!repo || !owner) { 35 | return (

Loading {name} details...

); 36 | } 37 | 38 | const { stargazers, stargazersPagination } = this.props; 39 | return ( 40 |
41 | 42 |
43 | 50 |
51 | ); 52 | } 53 | } 54 | 55 | RepoPage.propTypes = { 56 | repo: PropTypes.object, 57 | fullName: PropTypes.string.isRequired, 58 | name: PropTypes.string.isRequired, 59 | owner: PropTypes.object, 60 | stargazers: PropTypes.array.isRequired, 61 | stargazersPagination: PropTypes.object, 62 | loadRepoPage: PropTypes.func.isRequired, 63 | loadMoreStargazers: PropTypes.func.isRequired 64 | }; 65 | 66 | function mapStateToProps(state) { 67 | const { login, name } = state.router.params; 68 | const { 69 | pagination: { stargazersByRepo }, 70 | entities: { users, repos } 71 | } = state; 72 | 73 | const fullName = `${login}/${name}`; 74 | const stargazersPagination = stargazersByRepo[fullName] || { ids: [] }; 75 | const stargazers = stargazersPagination.ids.map(id => users[id]); 76 | 77 | return { 78 | fullName, 79 | name, 80 | stargazers, 81 | stargazersPagination, 82 | repo: repos[fullName], 83 | owner: users[login] 84 | }; 85 | } 86 | 87 | export default connect(mapStateToProps, { 88 | loadRepoPage, 89 | loadMoreStargazers 90 | })(RepoPage); 91 | -------------------------------------------------------------------------------- /src/containers/RepoPage/RepoPage.scss: -------------------------------------------------------------------------------- 1 | // eslint-disable 2 | @import '../../theme/style'; 3 | 4 | .container { 5 | margin-bottom: 30px; 6 | background-color: $gray-light; 7 | .list { 8 | margin-top: 15px; 9 | } 10 | } -------------------------------------------------------------------------------- /src/containers/Root/Root.dev.js: -------------------------------------------------------------------------------- 1 | import has from 'lodash/has'; 2 | import React, { Component, PropTypes } from 'react'; 3 | import { Provider } from 'react-redux'; 4 | import { Router, RouterContext } from 'react-router'; 5 | import DevTools from '../DevTools/DevTools'; 6 | import GoogleAnalytics from 'react-ga'; 7 | 8 | export default class Root extends Component { 9 | constructor(props) { 10 | super(props); 11 | this.onUpdate = this.onUpdate.bind(this); 12 | } 13 | 14 | onUpdate() { 15 | const { store, type } = this.props; 16 | if (type !== 'server') { 17 | const state = store.getState(); 18 | if (has(state, 'router.pathname')) { 19 | GoogleAnalytics.pageview(state.router.pathname); 20 | } 21 | } 22 | } 23 | render() { 24 | const { store, history, routes, type, renderProps } = this.props; 25 | return ( 26 | 27 |
28 | {type === 'server' 29 | ? 30 | : } 31 | 32 |
33 |
34 | ); 35 | } 36 | } 37 | 38 | Root.propTypes = { 39 | store: PropTypes.object.isRequired, 40 | history: PropTypes.object.isRequired, 41 | routes: PropTypes.node.isRequired, 42 | type: PropTypes.string, 43 | renderProps: PropTypes.object 44 | }; 45 | -------------------------------------------------------------------------------- /src/containers/Root/Root.js: -------------------------------------------------------------------------------- 1 | if (process.env.NODE_ENV === 'production') { 2 | module.exports = require('./Root.prod'); // eslint-disable-line global-require 3 | } else { 4 | module.exports = require('./Root.dev'); // eslint-disable-line global-require 5 | } 6 | -------------------------------------------------------------------------------- /src/containers/Root/Root.prod.js: -------------------------------------------------------------------------------- 1 | import has from 'lodash/has'; 2 | import React, { Component, PropTypes } from 'react'; 3 | import { Provider } from 'react-redux'; 4 | import { Router, RouterContext } from 'react-router'; 5 | import GoogleAnalytics from 'react-ga'; 6 | 7 | export default class Root extends Component { 8 | constructor(props) { 9 | super(props); 10 | this.onUpdate = this.onUpdate.bind(this); 11 | } 12 | 13 | onUpdate() { 14 | const { store, type } = this.props; 15 | if (type !== 'server') { 16 | const state = store.getState(); 17 | if (has(state, 'router.pathname')) { 18 | GoogleAnalytics.pageview(state.router.pathname); 19 | } 20 | } 21 | } 22 | render() { 23 | const { store, history, routes, type, renderProps } = this.props; 24 | return ( 25 | 26 |
27 | {type === 'server' 28 | ? 29 | : } 30 |
31 |
32 | ); 33 | } 34 | } 35 | 36 | Root.propTypes = { 37 | store: PropTypes.object.isRequired, 38 | history: PropTypes.object.isRequired, 39 | routes: PropTypes.node.isRequired, 40 | type: PropTypes.object, 41 | renderProps: PropTypes.object 42 | }; 43 | -------------------------------------------------------------------------------- /src/containers/UserPage/UserPage.js: -------------------------------------------------------------------------------- 1 | import zip from 'lodash/zip'; 2 | import React, { Component, PropTypes } from 'react'; 3 | import { connect } from 'react-redux'; 4 | import { loadUserPage, loadMoreStarred } from '../../actions'; 5 | import { loadUser, loadStarred } from '../../sagas'; 6 | import { User, Repo, List } from 'components'; 7 | 8 | class UserPage extends Component { 9 | constructor(props) { 10 | super(props); 11 | this.renderRepo = this.renderRepo.bind(this); 12 | this.handleLoadMoreClick = this.handleLoadMoreClick.bind(this); 13 | } 14 | 15 | componentWillMount() { 16 | this.props.loadUserPage(this.props.login); 17 | } 18 | 19 | componentWillReceiveProps(nextProps) { 20 | if (this.props.login !== nextProps.login) { 21 | this.props.loadUserPage(nextProps.login); 22 | } 23 | } 24 | 25 | handleLoadMoreClick() { 26 | this.props.loadMoreStarred(this.props.login); 27 | } 28 | 29 | renderRepo([repo, owner]) { 30 | return (); 31 | } 32 | 33 | render() { 34 | const { user, login } = this.props; 35 | 36 | if (!user) { 37 | return (

Loading {login}’s profile...

); 38 | } 39 | 40 | const { starredRepos, starredRepoOwners, starredPagination } = this.props; 41 | return ( 42 |
43 | 44 |
45 | 52 |
53 | ); 54 | } 55 | } 56 | 57 | UserPage.propTypes = { 58 | login: PropTypes.string.isRequired, 59 | user: PropTypes.object, 60 | starredPagination: PropTypes.object, 61 | starredRepos: PropTypes.array.isRequired, 62 | starredRepoOwners: PropTypes.array.isRequired, 63 | loadUserPage: PropTypes.func.isRequired, 64 | loadMoreStarred: PropTypes.func.isRequired 65 | }; 66 | 67 | function mapStateToProps(state) { 68 | const { login } = state.router.params; 69 | const { 70 | pagination: { starredByUser }, 71 | entities: { users, repos } 72 | } = state; 73 | 74 | const starredPagination = starredByUser[login] || { ids: [] }; 75 | const starredRepos = starredPagination.ids.map(id => repos[id]); 76 | const starredRepoOwners = starredRepos.map(repo => users[repo.owner]); 77 | 78 | return { 79 | login, 80 | starredRepos, 81 | starredRepoOwners, 82 | starredPagination, 83 | user: users[login] 84 | }; 85 | } 86 | 87 | function preload({ login }) { 88 | return [ 89 | [loadUser, login, []], 90 | [loadStarred, login] 91 | ]; 92 | } 93 | UserPage.preload = preload; 94 | 95 | export default connect(mapStateToProps, { 96 | loadUserPage, 97 | loadMoreStarred 98 | })(UserPage); 99 | -------------------------------------------------------------------------------- /src/containers/index.js: -------------------------------------------------------------------------------- 1 | export Root from './Root/Root'; 2 | export App from './App/App'; 3 | export UserPage from './UserPage/UserPage'; 4 | export RepoPage from './RepoPage/RepoPage'; 5 | export NotFound from './NotFound/NotFound'; 6 | -------------------------------------------------------------------------------- /src/helpers/Html.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import React, { Component, PropTypes } from 'react'; 3 | import ReactDOM from 'react-dom/server'; 4 | import serialize from 'serialize-javascript'; 5 | import Helmet from 'react-helmet'; 6 | import config from '../config'; 7 | 8 | /** 9 | * Wrapper component containing HTML metadata and boilerplate tags. 10 | * Used in server-side code only to wrap the string output of the 11 | * rendered route component. 12 | * 13 | * The only thing this component doesn't (and can't) include is the 14 | * HTML doctype declaration, which is added to the rendered output 15 | * by the server.js file. 16 | */ 17 | class Html extends Component { 18 | render() { 19 | const { assets, component, store } = this.props; 20 | const content = component ? ReactDOM.renderToStaticMarkup(component) : ''; 21 | const head = Helmet.rewind(); 22 | 23 | return ( 24 | 25 | 26 | {head.base.toComponent()} 27 | {head.title.toComponent()} 28 | {head.meta.toComponent()} 29 | {head.link.toComponent()} 30 | {head.script.toComponent()} 31 | 32 | 33 | 34 | 35 | {/* styles (will be present only in production with webpack extract text plugin) */} 36 | {Object.keys(assets.styles).map((style, key) => 37 | 38 | )} 39 | 40 | 41 | 42 |
43 | 44 |