├── .babelrc ├── .eslintignore ├── .eslintrc ├── .gitignore ├── LICENSE ├── README.md ├── TODO.md ├── assets ├── images │ └── favicon.ico └── stylesheets │ ├── index.css │ └── page.css ├── index.template.html ├── lib ├── Root.js ├── actions │ ├── application.js │ └── github.js ├── components │ ├── Application.js │ ├── DevTools.js │ ├── DisplayError.js │ ├── Footer.js │ ├── Menu.js │ ├── MenuListItem.js │ ├── github │ │ ├── Explore.js │ │ ├── Pagination.js │ │ ├── Repo.js │ │ ├── StargazersRepo.js │ │ ├── StargazersUser.js │ │ └── User.js │ ├── index.js │ └── pages │ │ ├── About.js │ │ ├── Account.js │ │ ├── AccountHome.js │ │ ├── Forbidden.js │ │ ├── GithubStargazers.js │ │ ├── Home.js │ │ ├── Login.js │ │ └── SuperSecretArea.js ├── constants.js ├── decorators │ ├── fetchOnUpdate.js │ ├── index.js │ └── secure.js ├── i18n │ ├── de.js │ ├── index.js │ └── it.js ├── index.js ├── middleware │ └── logger.js ├── persistence │ ├── storage.js │ └── store.js ├── reducers │ ├── application.js │ ├── github.js │ └── index.js └── utils │ ├── configure-store.js │ ├── create-reducer.js │ ├── handle-action-error.js │ └── process-response.js ├── package.json ├── scripts └── i18nToXliff.js ├── server.js ├── webpack.config.js └── webpack.config.production.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["transform-runtime"], 3 | "presets": [ "es2015", "stage-0", "react" ], 4 | "env": { 5 | "development": { 6 | "plugins": [ 7 | [ "react-transform", { 8 | "transforms": [ 9 | { 10 | "transform": "react-transform-hmr", 11 | "imports": ["react"], 12 | "locals": ["module"] 13 | }, 14 | { 15 | "transform": "react-transform-catch-errors", 16 | "imports": [ "react", "redbox-react" ] 17 | } 18 | ] 19 | }] 20 | ] 21 | }, 22 | "i18n": { 23 | "plugins": [ 24 | [ "react-intl", { 25 | "messagesDir": "./_translations", 26 | "enforceDescriptions": false 27 | }] 28 | ] 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | webpack.* 2 | server.js 3 | dist -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | parser: 'babel-eslint' 2 | 3 | plugins: [ 'react' ] 4 | 5 | ecmaFeatures: 6 | jsx: true 7 | 8 | env: 9 | es6: true 10 | browser: true 11 | jasmine: true 12 | node: true 13 | 14 | rules: 15 | array-bracket-spacing: 0 16 | camelcase: 2 17 | computed-property-spacing: [ 2, 'never' ] 18 | consistent-return: 2 19 | curly: [ 2, 'multi' ] 20 | dot-notation: 0 21 | eol-last: 2 22 | eqeqeq: 2 23 | max-len: [ 2, 80, 4 ] 24 | new-cap: [ 2, { capIsNew: false } ] 25 | no-eq-null: 2 26 | no-mixed-spaces-and-tabs: 2 27 | no-multiple-empty-lines: [ 2, { max: 2 } ] 28 | no-trailing-spaces: 2 29 | no-use-before-define: [ 2, 'nofunc' ] 30 | no-undef: 2 31 | no-underscore-dangle: 0 32 | no-unused-vars: 2 33 | no-var: 2 34 | object-curly-spacing: [ 2, 'always' ] 35 | quotes: [ 2, 'single' ] 36 | semi: [ 2, 'never' ] 37 | space-after-keywords: [ 2, 'always' ] 38 | space-before-blocks: [ 2, 'always' ] 39 | space-before-function-paren: [ 2, 'always' ] 40 | no-warning-comments: [1, { terms: [ 'todo', 'fixme' ], location: 'anywhere' }] 41 | space-return-throw-case: 2 42 | strict: 0 43 | vars-on-top: 2 44 | indent: [ 2, 2, { SwitchCase: 1 } ] 45 | 46 | # React stuff. 47 | react/display-name: 0 48 | react/jsx-boolean-value: 2 49 | react/jsx-no-undef: 2 50 | react/jsx-sort-props: 0 51 | react/jsx-uses-react: 2 52 | react/jsx-uses-vars: 2 53 | react/no-did-mount-set-state: 2 54 | react/no-did-update-set-state: 2 55 | react/no-multi-comp: 0 56 | react/no-unknown-property: 2 57 | react/prop-types: 2 58 | react/react-in-jsx-scope: 2 59 | react/self-closing-comp: 2 60 | react/wrap-multilines: 2 61 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | npm-debug.log 3 | node_modules 4 | dist 5 | _translations 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Nicola Molinari 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 |

This repository is deprecated and not maintained anymore

2 | 3 | Check out those resources: 4 | * https://github.com/react-boilerplate/react-boilerplate 5 | * https://github.com/facebookincubator/create-react-app/ 6 | * https://redux.js.org/ 7 | * https://reacttraining.com/react-router 8 | 9 | --- 10 | 11 | http://emmenko.github.io/redux-react-router-async-example 12 | 13 | This is a boilerplate example showcasing _mostly_ [Redux](https://github.com/gaearon/redux) and [React Router](https://github.com/rackt/react-router) and it aims to provide different examples or use cases with the two libraries. 14 | 15 | > Check out the [migration to babel 6](https://github.com/emmenko/redux-react-router-async-example/pull/52). 16 | 17 | ## Features 18 | 19 | - latest `react`, `redux`, `react-router` 20 | - `devtools` + `HMR` for development 21 | - `react` 22 | - higher-order components 23 | - higher-order functions 24 | - `redux` 25 | - bootstrap initial state 26 | - store enhancers (e.g.: localStorage persistence) 27 | - middleware composition 28 | - easy-to-configure reducers 29 | - async actions 30 | - `react-router` 31 | - nested routes 32 | - secured area by login 33 | - keep routes into state for easy time travel 34 | - `i18n` support by `react-intl` 35 | - scripts to extract messages and generate `XLIFF` files for translators 36 | - GitHub API with pagination 37 | - CSSnext bundles 38 | - injected into HTML by webpack for development (allows `HMR`) 39 | - bundled as a separate file for production 40 | - development tools such as `webpack`, `babel`, `eslint` 41 | 42 | ## Development 43 | 44 | ```bash 45 | $ npm install 46 | $ npm start 47 | ``` 48 | 49 | ### I18n support 50 | 51 | All messages in this website are localized and rendered using `react-intl@2.0`. 52 | 53 | There is also a [babel plugin](https://github.com/yahoo/babel-plugin-react-intl) to extract all the default messages into `./_translations/lib` to be provided to translators. 54 | 55 | ```bash 56 | $ npm run build:i18n 57 | ``` 58 | 59 | You can also run a script to extract all those translations as key-value. 60 | 61 | ```bash 62 | $ npm run build:i18n:langs 63 | ``` 64 | 65 | 66 | #### Thanks 67 | 68 | - [Redux](https://github.com/gaearon/redux) for the _Atomic Flux_ architecture. 69 | - [React](https://github.com/facebook/react) for all the goodness. 70 | - [React-Router](https://github.com/rackt/react-router) for the other goodness. 71 | - [React-Transform](https://github.com/gaearon/react-transform-boilerplate) for development fun (and productivity). 72 | - [Webpack](https://github.com/webpack/webpack) for keeping everything together. 73 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | # TODO 2 | 3 | - JSDoc 4 | - loading spinner for data fetching 5 | - use inline styles https://github.com/emmenko/redux-react-router-async-example/issues/7 6 | - add use-case for `AsyncProps` 7 | - add use-case for external plugin https://dl.dropboxusercontent.com/u/4803975/ReactEurope/slides.pdf 8 | 9 | - tests? 10 | - other examples? 11 | -------------------------------------------------------------------------------- /assets/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emmenko/redux-react-router-async-example/f289912c3562694157ac82a1a3c498eb681a831f/assets/images/favicon.ico -------------------------------------------------------------------------------- /assets/stylesheets/index.css: -------------------------------------------------------------------------------- 1 | /* 2 | Use `border-box` properly: 3 | https://css-tricks.com/inheriting-box-sizing-probably-slightly-better-best-practice/ 4 | */ 5 | html { box-sizing: border-box; } 6 | *, *:before, *:after { box-sizing: inherit; } 7 | 8 | /* External CSS */ 9 | @import "purecss/build/pure-min.css"; 10 | @import "purecss/build/grids-responsive-min.css"; 11 | 12 | /* Global CSS */ 13 | @import "page.css"; 14 | -------------------------------------------------------------------------------- /assets/stylesheets/page.css: -------------------------------------------------------------------------------- 1 | /* custom properties */ 2 | :root { 3 | --fontFamily: "Raleway", "Helvetica Neue", Helvetica, Arial, sans-serif; 4 | --bkgColor: #FFF; 5 | --fontColor: #999; 6 | --colorWhite: #FFF; 7 | --colorRed: #DD514C; 8 | } 9 | 10 | /* custom selectors */ 11 | @custom-selector :--layout #layout; 12 | @custom-selector :--menu #menu; 13 | @custom-selector :--header .header; 14 | @custom-selector :--content .content; 15 | @custom-selector :--footer .footer; 16 | @custom-selector :--stargazers .stargazers; 17 | 18 | /* custom media queries */ 19 | @custom-media --viewport-small (max-width: 58em); 20 | @custom-media --viewport-medium (min-width: 58em); 21 | 22 | /* styleguide */ 23 | html, button, input, select, textarea, .pure-g [class *= "pure-u"] { 24 | font-family: sans-serif; font-weight: 100; letter-spacing: 0.01em; 25 | } 26 | 27 | body { min-width: 320px; color: #777; line-height: 1.6; } 28 | 29 | h1, h2, h3, h4, h5, h6 { font-weight: bold; color: rgb(75, 75, 75); } 30 | 31 | h2 { margin: 2em 0 1em 0; font-weight: 300; color: #888; position: relative; } 32 | 33 | a:visited { color: #265778; } 34 | 35 | .ellipsis { white-space: nowrap; text-overflow: ellipsis; overflow: hidden; } 36 | 37 | .link a i { font-size: 200%; color: var(--fontColor); } 38 | 39 | .pure-form legend { color: var(--fontColor); } 40 | 41 | .error-message { margin-left: 45px; width: 100%; } 42 | .error-message > div { 43 | padding: 20px; padding-right: 60px; color: var(--colorWhite); 44 | width: 100%; position: relative; background-color: var(--colorRed); 45 | } 46 | .error-message p { margin: 0; } 47 | .error-message .close-button { 48 | float: right; font-size: 21px; line-height: 1; border: 0; 49 | background: none; color: var(--colorWhite); font-size: 1em; 50 | } 51 | 52 | /* scaffolding */ 53 | :--layout { position: relative; padding-left: 0; } 54 | :--layout, #menu, .menu-link { transition: all 0.2s ease-out; } 55 | :--layout.active .menu-link { left: 160px; } 56 | :--layout.active #menu { left: 160px; width: 160px; } 57 | 58 | :--menu { margin-left: -160px; width: 160px; position: fixed; top: 0; left: 0; 59 | bottom: 0; z-index: 1000; overflow-y: auto; -webkit-overflow-scrolling: touch; 60 | border-right: 1px solid #ccc; 61 | } 62 | :--menu a { color: var(--fontColor); border: none; white-space: normal; padding: 0.625em 1em; } 63 | :--menu .pure-menu-heading { font-size: 125%; font-weight: 300; letter-spacing: 0.1em; 64 | color: #222; margin-top: 0; padding: 0.5em 0.8em; text-transform: uppercase; 65 | } 66 | :--menu .pure-menu-heading:hover, 67 | :--menu .pure-menu-heading:focus { color: var(--fontColor); } 68 | 69 | .menu-link { position: fixed; display: block; top: 0; left: 0; 70 | width: 4em; height: 4em; background: #ccc; 71 | font-size: 11px; z-index: 10; padding: 1em; 72 | } 73 | .menu-link span { position: relative; display: block; margin-top: 0.9em; } 74 | .menu-link span, 75 | .menu-link span:before, 76 | .menu-link span:after { background-color: #fff; width: 100%; height: .2em; 77 | transition: all 0.4s; 78 | } 79 | .menu-link span:before, 80 | .menu-link span:after { position: absolute; top: -.55em; content: " "; } 81 | .menu-link span:after { top: .55em; } 82 | .menu-link.active span { background: transparent; } 83 | .menu-link.active span:after { transform: rotate(-45deg) translate(.4em, -.3em); } 84 | .menu-link.active span:before { transform: rotate(45deg) translate(.5em, .4em); } 85 | 86 | :--menu .language-switcher { padding: 0.625em 1em; border-top: 1px solid #ccc; } 87 | 88 | :--header { 89 | font-family: var(--fontFamily); background: var(--bkgColor); 90 | max-width: 768px; margin: 0 auto; padding: 1em; text-align: center; 91 | border-bottom: 1px solid #eee; letter-spacing: 0.05em; 92 | } 93 | :--header h1 { font-size: 300%; font-weight: 100; margin: 0; } 94 | :--header h2 { font-size: 125%; font-weight: 100; line-height: 1.5; margin: 0; 95 | color: #666; letter-spacing: -0.02em; 96 | } 97 | 98 | #main { min-height: 500px; } 99 | 100 | :--content { margin-left: auto; margin-right: auto; padding-left: 1em; 101 | padding-right: 1em; max-width: 768px; 102 | } 103 | 104 | :--footer { font-size: 87.5%; border-top: 1px solid #eee; margin-top: 3.4286em; 105 | padding: 1.1429em; 106 | } 107 | 108 | :--stargazers { background: rgb(250, 250, 250); margin: 2em auto; 109 | border-top: 1px solid #ddd; border-bottom: 1px solid #ddd; 110 | } 111 | 112 | .l-box { padding: 1em; } 113 | .l-box-top { border-right: 1px solid var(--fontColor); } 114 | .l-box-top a { margin-right: 15px; float: left; } 115 | .l-box-top .repo-info { text-align: right; margin-right: 5px; } 116 | 117 | .explore { margin: 50px 0; text-align: center; } 118 | 119 | .section-user { text-align: center; } 120 | .section-repo { margin: 0 auto; max-width: 300px; } 121 | 122 | .pagination { text-align: center; } 123 | .pagination ul { margin: 0; padding: 0; list-style: none; } 124 | .pagination li { display: inline-block; margin-right: 10px; cursor: pointer; } 125 | .pagination .disabled { color: #ddd; } 126 | 127 | /* media queries */ 128 | @media (--viewport-small) { 129 | :--layout.active { position: relative; left: 160px; } 130 | } 131 | 132 | @media (--viewport-medium) { 133 | :--layout { padding-left: 160px; left: 0; } 134 | :--menu { left: 160px; } 135 | .menu-link { position: fixed; left: 160px; display: none; } 136 | .error-message { margin-left: 0; } 137 | .error-message > div { padding-right: 20px; } 138 | } 139 | 140 | @media screen and (min-width: 48em) { 141 | :--header, .content { padding-left: 2em; padding-right: 2em; } 142 | :--header h1 { font-size: 320%; } 143 | :--header h2 { font-size: 128%; } 144 | } 145 | 146 | -------------------------------------------------------------------------------- /index.template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {%=o.htmlWebpackPlugin.options.title || 'Redux React Router Async Example'%} 5 | 6 | 7 | 8 | {% if (o.htmlWebpackPlugin.files.favicon) { %} 9 | 10 | {% } %} 11 | 12 | 13 | 16 | 17 | 18 | {% for (var css in o.htmlWebpackPlugin.files.css) { %} 19 | 20 | {% } %} 21 | 22 | 23 |
24 | {% for (var chunk in o.htmlWebpackPlugin.files.chunks) { %} 25 | 26 | {% } %} 27 | 28 | 29 | -------------------------------------------------------------------------------- /lib/Root.js: -------------------------------------------------------------------------------- 1 | /* global __DEVTOOLS__ */ 2 | import '../assets/stylesheets/index.css' 3 | 4 | import React, { PropTypes } from 'react' 5 | import { Redirect, Route } from 'react-router' 6 | import { ReduxRouter } from 'redux-router' 7 | import { connect } from 'react-redux' 8 | import { IntlProvider } from 'react-intl' 9 | import configureStore from './utils/configure-store' 10 | import * as storage from './persistence/storage' 11 | import * as components from './components' 12 | import * as constants from './constants' 13 | import * as i18n from './i18n' 14 | 15 | 16 | const { 17 | About, 18 | Account, 19 | AccountHome, 20 | Application, 21 | GithubStargazers, 22 | GithubRepo, 23 | GithubUser, 24 | Home, 25 | Login, 26 | SuperSecretArea 27 | } = components 28 | 29 | const initialState = { 30 | application: { 31 | token: storage.get('token'), 32 | locale: storage.get('locale') || 'en', 33 | user: { permissions: [/*'manage_account'*/] } 34 | } 35 | } 36 | 37 | export const store = configureStore(initialState) 38 | 39 | function getRootChildren (props) { 40 | const intlData = { 41 | locale: props.application.locale, 42 | messages: i18n[props.application.locale] 43 | } 44 | const rootChildren = [ 45 | 46 | {renderRoutes()} 47 | 48 | ] 49 | 50 | if (__DEVTOOLS__) { 51 | const DevTools = require('./components/DevTools').default 52 | rootChildren.push() 53 | } 54 | return rootChildren 55 | } 56 | 57 | function renderRoutes () { 58 | return ( 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | ) 77 | } 78 | 79 | function requireAuth (nextState, replaceState) { 80 | const state = store.getState() 81 | const isLoggedIn = Boolean(state.application.token) 82 | if (!isLoggedIn) 83 | replaceState({ 84 | nextPathname: nextState.location.pathname 85 | }, '/login') 86 | } 87 | 88 | function logout (nextState, replaceState) { 89 | store.dispatch({ type: constants.LOG_OUT }) 90 | replaceState({}, '/login') 91 | } 92 | 93 | class Root extends React.Component { 94 | static propTypes = { 95 | application: PropTypes.object.isRequired 96 | }; 97 | 98 | render () { 99 | return ( 100 |
{getRootChildren(this.props)}
101 | ) 102 | } 103 | } 104 | 105 | export default connect(({ application }) => ({ application }))(Root) 106 | -------------------------------------------------------------------------------- /lib/actions/application.js: -------------------------------------------------------------------------------- 1 | import * as constants from '../constants' 2 | 3 | export function login (form, redirect) { 4 | return dispatch => { 5 | // simulate request 6 | setTimeout(() => { 7 | const token = Math.random().toString(36).substring(7) 8 | dispatch({ 9 | type: constants.LOGGED_IN, 10 | payload: { token } 11 | }) 12 | // Can be used to navigate to a new route 13 | if (redirect) redirect() 14 | }, 300) 15 | } 16 | } 17 | 18 | export function switchLocale (locale) { 19 | return { type: constants.LOCALE_SWITCHED, payload: locale } 20 | } 21 | 22 | export function hideError () { 23 | return { type: constants.HIDE_ERROR } 24 | } 25 | -------------------------------------------------------------------------------- /lib/actions/github.js: -------------------------------------------------------------------------------- 1 | import 'whatwg-fetch' 2 | import parseLinkHeader from 'parse-link-header' 3 | import handleActionError from '../utils/handle-action-error' 4 | import processResponse from '../utils/process-response' 5 | import { 6 | FETCH_USER, 7 | FETCH_REPO, 8 | FETCH_USER_STARGAZERS, 9 | FETCH_REPO_STARGAZERS 10 | } from '../constants' 11 | 12 | const GITHUB_API = 'https://api.github.com' 13 | 14 | export function fetchUser (options) { 15 | const { username } = options 16 | 17 | return dispatch => { 18 | fetch(`${GITHUB_API}/users/${username}`) 19 | .then(processResponse) 20 | .then(res => dispatch({ 21 | type: FETCH_USER, 22 | user: res 23 | })) 24 | .catch(error => handleActionError(dispatch, error, FETCH_USER)) 25 | } 26 | } 27 | 28 | export function fetchUserStargazers (options) { 29 | const { page, username } = options 30 | const url = page ? page : 31 | `${GITHUB_API}/users/${username}/starred` 32 | 33 | return dispatch => { 34 | fetch(url) 35 | .then(res => { 36 | const pagination = parseLinkHeader(res.headers.get('link')) 37 | return processResponse(res) 38 | .then(result => dispatch({ 39 | type: FETCH_USER_STARGAZERS, 40 | stargazers: result, 41 | pagination 42 | })) 43 | }) 44 | .catch(error => handleActionError(dispatch, error, FETCH_USER_STARGAZERS)) 45 | } 46 | } 47 | 48 | export function fetchRepo (options) { 49 | const { username, repo } = options 50 | 51 | return dispatch => { 52 | fetch(`${GITHUB_API}/repos/${username}/${repo}`) 53 | .then(processResponse) 54 | .then(res => dispatch({ 55 | type: FETCH_REPO, 56 | repo: res 57 | })) 58 | .catch(error => handleActionError(dispatch, error, FETCH_REPO)) 59 | } 60 | } 61 | 62 | export function fetchRepoStargazers (options) { 63 | const { page, username, repo } = options 64 | const url = page ? page : 65 | `${GITHUB_API}/repos/${username}/${repo}/stargazers` 66 | 67 | return dispatch => { 68 | fetch(url) 69 | .then(res => { 70 | const pagination = parseLinkHeader(res.headers.get('link')) 71 | return processResponse(res) 72 | .then(result => dispatch({ 73 | type: FETCH_REPO_STARGAZERS, 74 | stargazers: result, 75 | pagination 76 | })) 77 | }) 78 | .catch(error => handleActionError(dispatch, error, FETCH_REPO_STARGAZERS)) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /lib/components/Application.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react' 2 | import classnames from 'classnames' 3 | import Menu from './Menu' 4 | import Footer from './Footer' 5 | import DisplayError from './DisplayError' 6 | 7 | export default class Application extends React.Component { 8 | 9 | static propTypes = { 10 | children: PropTypes.any 11 | }; 12 | 13 | constructor (props, context) { 14 | super(props, context) 15 | 16 | this.handleMenuClick = this.handleMenuClick.bind(this) 17 | 18 | this.state = { 19 | isMenuActive: false 20 | } 21 | } 22 | 23 | handleMenuClick (evt) { 24 | evt.preventDefault() 25 | this.setState({ isMenuActive: !this.state.isMenuActive }) 26 | } 27 | 28 | render () { 29 | const { isMenuActive } = this.state 30 | const activeClass = isMenuActive ? 'active' : '' 31 | 32 | return ( 33 |
34 | 37 | 38 | 39 | 40 | 41 | 42 |
43 | 44 | {/* this will render the child routes */} 45 | {this.props.children} 46 |
47 | 48 |
49 |
50 | ) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /lib/components/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 | 10 | 11 | 12 | ) 13 | -------------------------------------------------------------------------------- /lib/components/DisplayError.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react' 2 | import { connect } from 'react-redux' 3 | import * as applicationActions from '../actions/application' 4 | 5 | class DisplayError extends React.Component { 6 | 7 | static propTypes = { 8 | hideError: PropTypes.func.isRequired, 9 | error: PropTypes.object 10 | }; 11 | 12 | render () { 13 | const { props: { hideError, error } } = this 14 | 15 | if (!error) return null 16 | 17 | return ( 18 |
19 |
20 | 26 |

{error.message}

27 |
28 |             {JSON.stringify(error.body, null, 2)}
29 |           
30 |
31 |
32 | ) 33 | } 34 | } 35 | 36 | export default connect( 37 | ({ application }) => ({ error: application.error }), 38 | applicationActions 39 | )(DisplayError) 40 | -------------------------------------------------------------------------------- /lib/components/Footer.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { defineMessages, FormattedMessage } from 'react-intl' 3 | 4 | const messages = defineMessages({ 5 | builtWith: { 6 | id: 'footer.builtWith', 7 | description: 'Mention how the site is built with', 8 | defaultMessage: 'This site is built with <3 using {link}' 9 | } 10 | }) 11 | 12 | export default class Footer extends React.Component { 13 | 14 | render () { 15 | const link = (PureCSS) 16 | return ( 17 |
18 |
19 |
20 | 21 |
22 |
23 | © 2015 24 |
25 |
26 |
27 | ) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /lib/components/Menu.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react' 2 | import { Link } from 'react-router' 3 | import { connect } from 'react-redux' 4 | import * as applicationActions from '../actions/application' 5 | import MenuListItem from './MenuListItem' 6 | 7 | const GITHUB_REPO = 8 | 'https://github.com/emmenko/redux-react-router-async-example' 9 | const menuItems = [ 10 | { text: 'Stargazers', link: '/stargazers/emmenko', icon: 'fa fa-star' }, 11 | { text: 'Account', link: '/account', icon: 'fa fa-user' }, 12 | { text: 'About', link: '/about', icon: 'fa fa-dot-circle-o' }, 13 | { text: 'Fork Me', link: GITHUB_REPO, icon: 'fa fa-github', isExternal: true } 14 | ] 15 | 16 | class Menu extends React.Component { 17 | 18 | static propTypes = { 19 | activeClass: PropTypes.string.isRequired, 20 | application: PropTypes.object.isRequired, 21 | switchLocale: PropTypes.func.isRequired 22 | }; 23 | 24 | constructor (props, context) { 25 | super(props, context) 26 | 27 | this.handleLanguageSwitch = this.handleLanguageSwitch.bind(this) 28 | } 29 | 30 | handleLanguageSwitch (evt) { 31 | this.props.switchLocale(evt.target.value) 32 | } 33 | 34 | render () { 35 | const { application: { locale } } = this.props 36 | 37 | return ( 38 | 57 | ) 58 | } 59 | } 60 | 61 | export default connect( 62 | ({ application }) => ({ application }), 63 | applicationActions 64 | )(Menu) 65 | -------------------------------------------------------------------------------- /lib/components/MenuListItem.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes, Component } from 'react' 2 | import { Link } from 'react-router' 3 | 4 | export default class MenuListItem extends Component { 5 | 6 | static propTypes = { 7 | icon: PropTypes.string.isRequired, 8 | isExternal: PropTypes.bool, 9 | link: PropTypes.string.isRequired, 10 | text: PropTypes.string.isRequired 11 | }; 12 | 13 | static defaultProps = { isExternal: false }; 14 | 15 | render () { 16 | return ( 17 |
  • 18 | {this.renderLink()} 19 |
  • 20 | ) 21 | } 22 | 23 | renderLink () { 24 | if (this.props.isExternal) 25 | return ( 26 | 27 | {this.props.text} 28 | 29 | ) 30 | else 31 | return ( 32 | 33 | {this.props.text} 34 | 35 | ) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /lib/components/github/Explore.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react' 2 | import { findDOMNode } from 'react-dom' 3 | import shallowEqual from 'react-redux/lib/utils/shallowEqual' 4 | import { defineMessages, FormattedMessage } from 'react-intl' 5 | 6 | const messages = defineMessages({ 7 | legend: { 8 | id: 'explore.legend', 9 | description: 'Describe what to do with the input field', 10 | defaultMessage: 'Type a username or repo full name and hit \'Go\':' 11 | } 12 | }) 13 | 14 | const DEFAULT_USER = 'emmenko' 15 | 16 | function parseFullName (params) { 17 | const { username, repo } = params 18 | if (!username) return DEFAULT_USER 19 | 20 | return username + (repo ? '/' + repo : '') 21 | } 22 | 23 | export default class Explore extends React.Component { 24 | 25 | static propTypes = { 26 | params: PropTypes.shape({ 27 | repo: PropTypes.string, 28 | username: PropTypes.string 29 | }) 30 | }; 31 | 32 | static contextTypes = { 33 | history: PropTypes.object.isRequired 34 | }; 35 | 36 | constructor (props) { 37 | super(props) 38 | 39 | this.handleKeyUp = this.handleKeyUp.bind(this) 40 | this.handleOnChange = this.handleOnChange.bind(this) 41 | this.handleGoClick = this.handleGoClick.bind(this) 42 | this.getInputValue = this.getInputValue.bind(this) 43 | 44 | // State that depends on props is often an anti-pattern, but in our case 45 | // that's what we need to we can update the input both in response to route 46 | // change and in response to user typing. 47 | this.state = { 48 | usernameOrRepo: parseFullName(props.params) 49 | } 50 | } 51 | 52 | componentWillReceiveProps (nextProps) { 53 | if (!shallowEqual(this.props.params, nextProps.params)) 54 | this.setState({ 55 | usernameOrRepo: parseFullName(nextProps.params) 56 | }) 57 | } 58 | 59 | render () { 60 | return ( 61 |
    62 |
    e.preventDefault()}> 63 |
    64 | 65 | 72 | 76 |
    77 |
    78 |
    79 | ) 80 | } 81 | 82 | handleKeyUp (e) { 83 | if (e.keyCode === 13) 84 | this.handleGoClick() 85 | } 86 | 87 | handleOnChange () { 88 | // Update the internal state because we are using a controlled input. 89 | // This way we can update it *both* in response to user input *and* 90 | // in response to navigation in `componentWillReceiveProps`. 91 | this.setState({ 92 | usernameOrRepo: this.getInputValue() 93 | }) 94 | } 95 | 96 | handleGoClick () { 97 | this.context.history.pushState({}, `/stargazers/${this.getInputValue()}`) 98 | } 99 | 100 | getInputValue () { 101 | return findDOMNode(this.refs.usernameOrRepo).value 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /lib/components/github/Pagination.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react' 2 | import classnames from 'classnames' 3 | 4 | export default class Pagination extends React.Component { 5 | 6 | static propTypes = { 7 | onPagination: PropTypes.func, 8 | pagination: PropTypes.object 9 | }; 10 | 11 | handlePaginationClick (link) { 12 | const page = link.url 13 | this.props.onPagination({ page }) 14 | } 15 | 16 | render () { 17 | const { pagination } = this.props 18 | if (!pagination) return null 19 | 20 | const iconMap = { 21 | first: 'fa fa-fast-backward', 22 | prev: 'fa fa-backward', 23 | next: 'fa fa-forward', 24 | last: 'fa fa-fast-forward' 25 | } 26 | 27 | return ( 28 |
    29 |
      30 | {[ 'first', 'prev', 'next', 'last' ].map((key, i) => 31 |
    • 32 | {pagination[key] ? 33 | 35 | 36 | 37 | : 38 | } 39 |
    • 40 | )} 41 |
    42 |
    43 | ) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /lib/components/github/Repo.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react' 2 | import { Link } from 'react-router' 3 | import { fetchOnUpdate } from '../../decorators' 4 | import StargazersUser from './StargazersUser' 5 | import Pagination from './Pagination' 6 | 7 | class Repo extends React.Component { 8 | 9 | static propTypes = { 10 | actions: PropTypes.object, 11 | github: PropTypes.object 12 | }; 13 | 14 | render () { 15 | const { github: { repo, stargazers } } = this.props 16 | const { pagination } = stargazers 17 | 18 | return ( 19 |
    20 |
    21 |
    22 | {repo.owner && this.renderRepo(repo)} 23 |
    24 |
    25 | 26 |
    27 | 30 |
    31 | 32 |
    33 |
    34 | {stargazers.repo.map(user => 35 |
    37 | 38 |
    39 | )} 40 |
    41 |
    42 |
    43 | ) 44 | } 45 | 46 | renderRepo (repo) { 47 | const { owner } = repo 48 | 49 | return ( 50 |
    51 |
    52 | 54 | 59 | 60 |
    61 |

    {repo.stargazers_count}

    62 |

    {repo.subscribers_count}

    63 |
    64 |
    65 |
    66 | 69 |
    70 | 71 | {' ' + repo.name} 72 |
    73 | 74 | {repo.description} 75 |

    76 | 77 | 78 | 79 |

    80 |
    81 |
    82 | ) 83 | } 84 | } 85 | 86 | export default fetchOnUpdate([ 'username', 'repo' ], (params, actions) => { 87 | const { username, repo } = params 88 | actions.fetchRepo({ username, repo }) 89 | actions.fetchRepoStargazers({ username, repo }) 90 | })(Repo) 91 | -------------------------------------------------------------------------------- /lib/components/github/StargazersRepo.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react' 2 | import { Link } from 'react-router' 3 | 4 | export default class StargazersRepo extends React.Component { 5 | 6 | static propTypes = { 7 | repo: PropTypes.object.isRequired 8 | }; 9 | 10 | render () { 11 | const { repo } = this.props 12 | const { owner } = repo 13 | 14 | return ( 15 |
    16 |
    17 | 19 | 24 | 25 |
    26 |

    {repo.stargazers_count}

    27 | {/* FIXME: it seems that the correct field `subscribers_count` 28 | is missing from the API response */} 29 | {/*

    {repo.subscribers_count}

    */} 30 |
    31 |
    32 |
    33 | 36 |
    37 | 38 | {' ' + repo.name} 39 |
    40 | 41 |
    42 |
    43 | ) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /lib/components/github/StargazersUser.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react' 2 | import { Link } from 'react-router' 3 | 4 | export default class StargazersUser extends React.Component { 5 | 6 | static propTypes = { 7 | user: PropTypes.object.isRequired 8 | }; 9 | 10 | render () { 11 | const { user } = this.props 12 | 13 | return ( 14 |
    15 |
    16 | 18 | 23 | 24 |
    25 |
    26 | 28 |
    29 | 30 | {' ' + user.login} 31 |
    32 | 33 |
    34 |
    35 | ) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /lib/components/github/User.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react' 2 | import { Link } from 'react-router' 3 | import { fetchOnUpdate } from '../../decorators' 4 | import StargazersRepo from './StargazersRepo' 5 | import Pagination from './Pagination' 6 | 7 | class User extends React.Component { 8 | 9 | static propTypes = { 10 | actions: PropTypes.object, 11 | github: PropTypes.object 12 | }; 13 | 14 | render () { 15 | const { github: { user, stargazers } } = this.props 16 | const { pagination } = stargazers 17 | 18 | return ( 19 |
    20 | {this.renderUser(user)} 21 | 22 |
    23 | 26 |
    27 | 28 |
    29 |
    30 | {stargazers.user.map(repo => 31 |
    33 | 34 |
    35 | )} 36 |
    37 |
    38 |
    39 | ) 40 | } 41 | 42 | renderUser (user) { 43 | return ( 44 |
    45 |
    46 | 47 | 49 |

    50 | {user.login} 51 | {' '} 52 | {user.name && ({user.name})} 53 |

    54 | 55 |

    56 | 57 | 58 | 59 |

    60 |
    61 |
    62 | ) 63 | } 64 | } 65 | 66 | export default fetchOnUpdate(['username'], (params, actions) => { 67 | const { username } = params 68 | actions.fetchUser({ username }) 69 | actions.fetchUserStargazers({ username }) 70 | })(User) 71 | -------------------------------------------------------------------------------- /lib/components/index.js: -------------------------------------------------------------------------------- 1 | export { default as Application } from './Application' 2 | 3 | export { default as About } from './pages/About' 4 | export { default as Account } from './pages/Account' 5 | export { default as AccountHome } from './pages/AccountHome' 6 | export { default as Forbidden } from './pages/Forbidden' 7 | export { default as GithubStargazers } from './pages/GithubStargazers' 8 | export { default as Login } from './pages/Login' 9 | export { default as Home } from './pages/Home' 10 | export { default as SuperSecretArea } from './pages/SuperSecretArea' 11 | 12 | export { default as GithubRepo } from './github/Repo' 13 | export { default as GithubUser } from './github/User' 14 | -------------------------------------------------------------------------------- /lib/components/pages/About.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Link } from 'react-router' 3 | import { defineMessages, FormattedMessage } from 'react-intl' 4 | 5 | const messages = defineMessages({ 6 | aboutThanks: { 7 | id: 'about.specialThanks', 8 | description: 'Thanks to Dan Abramov for the idea of Redux', 9 | defaultMessage: 'A special thanks to {link} ' + 10 | 'for kicking out the idea of {redux}!' 11 | }, 12 | librariesIntro: { 13 | id: 'about.librariesIntro', 14 | description: 'Intro about technologies used on the website', 15 | defaultMessage: 'This website is a showcase of different technologies ' + 16 | 'and libraries such as:' 17 | } 18 | }) 19 | 20 | export default class About extends React.Component { 21 | render () { 22 | const link = (@gaearon) 23 | const redux = (Redux) 24 | 25 | return ( 26 |
    27 |
    28 |

    About

    29 |
    30 |
    31 |

    33 | 34 |

    Libraries

    35 |

    36 | 37 |

    38 | 55 |
    56 |
    57 | ) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /lib/components/pages/Account.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react' 2 | 3 | export default class Account extends React.Component { 4 | 5 | static propTypes = { 6 | children: PropTypes.any 7 | }; 8 | 9 | render () { 10 | return
    {this.props.children}
    11 | } 12 | } 13 | -------------------------------------------------------------------------------- /lib/components/pages/AccountHome.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Link } from 'react-router' 3 | import { defineMessages, FormattedMessage } from 'react-intl' 4 | 5 | const messages = defineMessages({ 6 | accountIntro: { 7 | id: 'account.home.intro', 8 | description: 'Introduction message of the account home page', 9 | defaultMessage: 'Congratulations, you\'ve entered an area secured by login!' 10 | }, 11 | accountSteps: { 12 | id: 'account.home.steps', 13 | description: 'Introduction message of the account home page', 14 | defaultMessage: 'You can {logoutLink} or try to access a {secretAreaLink} ' 15 | + 'without a necessary permissions.' 16 | }, 17 | accountSuperSecretArea: { 18 | id: 'account.home.link.superSecretArea', 19 | description: 'Link text to super secret area', 20 | defaultMessage: 'super secret area' 21 | } 22 | }) 23 | 24 | export default class AccountHome extends React.Component { 25 | render () { 26 | const logoutLink = (logout) 27 | const secretAreaLink = ( 28 | 29 | 30 | ) 31 | 32 | return ( 33 |
    34 |
    35 |

    Account

    36 |
    37 |
    38 |

    39 | 40 |
    41 | 43 |

    44 |
    45 |
    46 | ) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /lib/components/pages/Forbidden.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { defineMessages, FormattedMessage } from 'react-intl' 3 | 4 | const messages = defineMessages({ 5 | forbidden: { 6 | id: 'forbidden', 7 | defaultMessage: 'Forbidden' 8 | }, 9 | notAuthorized: { 10 | id: 'forbiddenReason', 11 | description: 'The user doesn\'t have permissions to access the page', 12 | defaultMessage: 'You are not authorized to see the XXX page' 13 | } 14 | }) 15 | 16 | export default class Forbidden extends React.Component { 17 | render () { 18 | return ( 19 |
    20 |
    21 | 22 | {text =>

    {text}

    } 23 |
    24 |
    25 |
    26 |

    27 | {/* TODO: get some info about the error */} 28 | 29 |

    30 |
    31 |
    32 | ) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /lib/components/pages/GithubStargazers.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react' 2 | import { connect } from 'react-redux' 3 | import { bindActionCreators } from 'redux' 4 | import { defineMessages, FormattedMessage } from 'react-intl' 5 | import Explore from '../github/Explore' 6 | import * as githubActions from '../../actions/github' 7 | 8 | const messages = defineMessages({ 9 | subtitle: { 10 | id: 'stargazers.subtitle', 11 | description: 'Subtitle of the page', 12 | defaultMessage: 'See the stargazers for your GitHub account and repos.' 13 | } 14 | }) 15 | 16 | class GithubStargazers extends React.Component { 17 | 18 | static propTypes = { 19 | children: PropTypes.any, 20 | actions: PropTypes.object 21 | }; 22 | 23 | render () { 24 | return ( 25 |
    26 |
    27 |

    Stargazers

    28 | 29 | {text =>

    {text}

    } 30 |
    31 |
    32 | 33 | 34 | 35 | {/* this will render the child routes */} 36 | {this.props.children && 37 | React.cloneElement(this.props.children, { ...this.props })} 38 |
    39 | ) 40 | } 41 | } 42 | 43 | export default connect( 44 | ({ github }) => ({ github }), 45 | dispatch => ({ actions: bindActionCreators(githubActions, dispatch) }) 46 | )(GithubStargazers) 47 | -------------------------------------------------------------------------------- /lib/components/pages/Home.js: -------------------------------------------------------------------------------- 1 | /*eslint-disable max-len*/ 2 | import React from 'react' 3 | import { defineMessages, FormattedMessage } from 'react-intl' 4 | 5 | const messages = defineMessages({ 6 | welcome: { 7 | id: 'home.welcome', 8 | description: 'Welcome message to the user', 9 | defaultMessage: 'Welcome' 10 | }, 11 | intro: { 12 | id: 'home.intro', 13 | description: 'Introductive message about the website', 14 | defaultMessage: 'This website is a boilerplate example to showcase and ' + 15 | 'provide best practices around {linkRedux} and {linkRouter}.' 16 | }, 17 | intro2: { 18 | id: 'home.intro2', 19 | description: 'Recommendation and scope of the website', 20 | defaultMessage: 'I recommend looking into the source code for inspiration ' + 21 | 'and ideas on how to implement many different use cases.' + 22 | '{br}' + 23 | 'I also plan to continuously add and demo case different ' + 24 | 'kind of features that are commons in web applications.' 25 | }, 26 | intro3: { 27 | id: 'home.intro3', 28 | description: 'Mention contributions', 29 | defaultMessage: 'Stay tuned and enjoy! For any question feel free to ' + 30 | '{linkIssues}, I\'ll be happy to provide some help whenever possible. ' + 31 | 'And any pull-request is very much welcomed! ;)' 32 | }, 33 | dropIssue: { 34 | id: 'home.intro3.dropAnIssue', 35 | defaultMessage: 'drop an issue' 36 | } 37 | }) 38 | 39 | 40 | export default class Home extends React.Component { 41 | 42 | render () { 43 | const linkRedux = (Redux) 45 | const linkRouter = (React Router) 47 | const linkIssues = () 50 | return ( 51 |
    52 |
    53 | 54 | {text =>

    {text}

    } 55 |
    56 |
    57 |
    58 |

    59 | 60 |

    61 |

    62 | ) }} /> 63 |

    64 |

    65 | 66 |

    67 |
    68 |
    69 | ) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /lib/components/pages/Login.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react' 2 | import * as actions from '../../actions/application' 3 | 4 | export default class Login extends React.Component { 5 | 6 | static propTypes = { 7 | location: PropTypes.object 8 | }; 9 | 10 | static contextTypes = { 11 | store: PropTypes.any, 12 | history: PropTypes.object.isRequired 13 | }; 14 | 15 | constructor (props) { 16 | super(props) 17 | this.state = { email: null, password: null } 18 | } 19 | 20 | handleInputChange (evt) { 21 | this.setState({ 22 | [evt.target.name]: evt.target.value 23 | }) 24 | } 25 | 26 | handleSubmit (evt) { 27 | evt.preventDefault() 28 | const { history, store } = this.context 29 | const { location } = this.props 30 | 31 | let nextPath = '/account' 32 | if (location.state && location.state.nextPathname) 33 | nextPath = location.state.nextPathname 34 | 35 | store.dispatch(actions.login(this.state, () => { 36 | // redirect to a secure page 37 | history.pushState({}, nextPath) 38 | })) 39 | } 40 | 41 | render () { 42 | return ( 43 |
    44 |
    45 |

    Login

    46 |
    47 |
    48 |
    52 |
    53 |
    54 | 55 | 56 |
    57 |
    58 | 59 | 60 |
    61 | 64 |
    65 |
    66 |
    67 |
    68 | ) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /lib/components/pages/SuperSecretArea.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { defineMessages, FormattedMessage } from 'react-intl' 3 | import { secure } from '../../decorators' 4 | 5 | const messages = defineMessages({ 6 | info: { 7 | id: 'superSecretArea.info', 8 | description: 'Intro message describing the page', 9 | defaultMessage: 'This is a restricted area.' 10 | } 11 | }) 12 | 13 | class SuperSecretArea extends React.Component { 14 | render () { 15 | return ( 16 |
    17 |
    18 |

    SuperSecretArea

    19 |
    20 |
    21 |

    22 |
    23 |
    24 | ) 25 | } 26 | } 27 | 28 | export default secure('manage_account')(SuperSecretArea) 29 | -------------------------------------------------------------------------------- /lib/constants.js: -------------------------------------------------------------------------------- 1 | export const FETCH_USER = 'FETCH_USER' 2 | export const FETCH_REPO = 'FETCH_REPO' 3 | export const FETCH_USER_STARGAZERS = 'FETCH_USER_STARGAZERS' 4 | export const FETCH_REPO_STARGAZERS = 'FETCH_REPO_STARGAZERS' 5 | 6 | export const LOGGED_IN = 'LOGGED_IN' 7 | export const LOG_OUT = 'LOG_OUT' 8 | 9 | export const LOCALE_SWITCHED = 'LOCALE_SWITCHED' 10 | export const SHOW_ERROR = 'SHOW_ERROR' 11 | export const HIDE_ERROR = 'HIDE_ERROR' 12 | -------------------------------------------------------------------------------- /lib/decorators/fetchOnUpdate.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react' 2 | import shallowEqual from 'react-redux/lib/utils/shallowEqual' 3 | 4 | function mapParams (paramKeys, params) { 5 | return paramKeys.reduce((acc, key) => { 6 | return Object.assign({}, acc, { [key]: params[key] }) 7 | }, {}) 8 | } 9 | 10 | export default function fetchOnUpdate (paramKeys, fn) { 11 | 12 | return DecoratedComponent => 13 | class FetchOnUpdateDecorator extends React.Component { 14 | 15 | static propTypes = { 16 | actions: PropTypes.object, 17 | params: PropTypes.object 18 | }; 19 | 20 | componentWillMount () { 21 | fn(mapParams(paramKeys, this.props.params), this.props.actions) 22 | } 23 | 24 | componentDidUpdate (prevProps) { 25 | const params = mapParams(paramKeys, this.props.params) 26 | const prevParams = mapParams(paramKeys, prevProps.params) 27 | 28 | if (!shallowEqual(params, prevParams)) 29 | fn(params, this.props.actions) 30 | } 31 | 32 | render () { 33 | return ( 34 | 35 | ) 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /lib/decorators/index.js: -------------------------------------------------------------------------------- 1 | export { default as fetchOnUpdate } from './fetchOnUpdate' 2 | export { default as secure } from './secure' 3 | -------------------------------------------------------------------------------- /lib/decorators/secure.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react' 2 | import { Forbidden } from '../components' 3 | 4 | export default function secure (scope) { 5 | 6 | return DecoratedComponent => 7 | class SecureDecorator extends React.Component { 8 | 9 | static contextTypes = { 10 | store: PropTypes.any 11 | }; 12 | 13 | render () { 14 | const { store } = this.context 15 | const { application: { user } } = store.getState() 16 | 17 | if (user.permissions.some(s => s === scope)) 18 | return 19 | else 20 | return 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /lib/i18n/de.js: -------------------------------------------------------------------------------- 1 | /*eslint-disable max-len,quotes*/ 2 | export default { 3 | "about.librariesIntro": "Diese Webseite ist eine Demonstration diverser Technologien und Bibliotheken, wie z.B.:", 4 | "about.specialThanks": "Besonderer Dank an {link} für die Idee von {redux}!", 5 | "account.home.intro": "Gratuliere! Du bist nun in einem gesicherten Bereich!", 6 | "account.home.link.superSecretArea": "super geheimer Bereich", 7 | "account.home.steps": "Du kannst dich {logoutLink} oder versuche den Zugriff auf den {secretAreaLink} ohne erforderliche Erlaubnis.", 8 | "explore.legend": "Gib einen Benutzername oder den Namen eines Repositorys ein und drück 'Go':", 9 | "footer.builtWith": "Diese Webseite ist gebaut mit <3 durch {link}", 10 | "forbidden": "Verboten", 11 | "forbiddenReason": "Du bist nicht zugriffsberechtigt auf die XXX Seite.", 12 | "home.intro": "Diese Webseite ist ein Standardbeispiel um insbesondere {linkRedux} und {linkRouter} zu präsentieren.", 13 | "home.intro2": "Ich empfehle in den Source Code reinzuschauen um sich zu inspirieren und Ideen zu bekommen wie man diverse Anwendungsfälle implementiert.{br}Ich werde auch versuchen weitere Features und Best-Practices einzubauen, die man normalerweise in Webenwendungen findet.", 14 | "home.intro3": "Dran bleiben und geniessen! Für alle Fragen einfach ein {linkIssues}, ich freu mich zu helfen wenn's möglich ist. Alle Pull-Request sind natürlich sehr willkommen! ;)", 15 | "home.intro3.dropAnIssue": "Issue anlegen", 16 | "home.welcome": "Willkommen", 17 | "stargazers.subtitle": "Die Stargazers für dein GitHub Account und Repos anzeigen.", 18 | "superSecretArea.info": "Das ist ein gesperrter Bereich." 19 | } 20 | -------------------------------------------------------------------------------- /lib/i18n/index.js: -------------------------------------------------------------------------------- 1 | export { default as it } from './it' 2 | export { default as de } from './de' 3 | -------------------------------------------------------------------------------- /lib/i18n/it.js: -------------------------------------------------------------------------------- 1 | /*eslint-disable max-len,quotes*/ 2 | export default { 3 | "about.librariesIntro": "Questo sito è uno showcase di diverse tecnologie e biblioteche come ad esempio:", 4 | "about.specialThanks": "Un particolare ringraziamento a {link} per dare il via all'idea di {redux}!", 5 | "account.home.intro": "Congratulazioni, sei entrato in un area protetta da login!", 6 | "account.home.link.superSecretArea": "area super segreta", 7 | "account.home.steps": "Puoi fare il {logoutLink} o provare ad accedere all'{secretAreaLink} senza l'autorizzazione necessaria.", 8 | "explore.legend": "Inserisci uno username o il nome completo di un repository e premi 'Go':", 9 | "footer.builtWith": "Questo sito è fatto con <3 usando {link}", 10 | "forbidden": "Proibito", 11 | "forbiddenReason": "Non sei autorizzato a vedere la pagina XXX.", 12 | "home.intro": "Questo sito è un esempio per dimostrare best practices, in particolare di come utilizzare {linkRedux} e {linkRouter}.", 13 | "home.intro2": "Consiglio di guardare nel source code per ispirazioni o idee di come realizzare diversi use cases.{br}La mia idea è anche di mantenere e aggiungere nuovi e diversi scenari e funzionalità che sono comuni nelle applicazioni web.", 14 | "home.intro3": "Restate sintonizzati e divertitevi! Per qualsiasi domanda non esitate a {linkIssues}, sarò felice di aiutare in qualsiasi modo, nei limiti possibili. Oh, e qualsiasi pull-request è ovviamente molto apprezzata! ;)", 15 | "home.intro3.dropAnIssue": "lasciare un issue", 16 | "home.welcome": "Benvenuto", 17 | "stargazers.subtitle": "Vedi gli stargazers per il tuo GitHub account e repos.", 18 | "superSecretArea.info": "Questa è un'area privata." 19 | } 20 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | import '../assets/stylesheets/index.css' 2 | 3 | import React from 'react' 4 | import ReactDOM from 'react-dom' 5 | import { Provider } from 'react-redux' 6 | import Root, { store } from './Root' 7 | import { addLocaleData } from 'react-intl' 8 | import en from 'react-intl/lib/locale-data/en' 9 | import de from 'react-intl/lib/locale-data/de' 10 | import it from 'react-intl/lib/locale-data/it' 11 | 12 | addLocaleData(en) 13 | addLocaleData(de) 14 | addLocaleData(it) 15 | 16 | // All modern browsers, except `Safari`, have implemented 17 | // the `ECMAScript Internationalization API`. 18 | // For that we need to patch in on runtime. 19 | if (!global.Intl) 20 | require.ensure(['intl'], require => { 21 | require('intl').default 22 | start() 23 | }, 'IntlBundle') 24 | else start() 25 | 26 | function start () { 27 | ReactDOM.render( 28 | 29 | 30 | 31 | , document.getElementById('app')) 32 | } 33 | -------------------------------------------------------------------------------- /lib/middleware/logger.js: -------------------------------------------------------------------------------- 1 | export default function logger ({ getState }) { 2 | return next => action => { 3 | console.group() 4 | console.log('will dispatch', action) 5 | const result = next(action) 6 | console.log('state after dispatch', getState()) 7 | console.groupEnd() 8 | 9 | return result 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /lib/persistence/storage.js: -------------------------------------------------------------------------------- 1 | export function put (key, value) { 2 | window.localStorage.setItem(key, value) 3 | } 4 | 5 | export function get (key) { 6 | return window.localStorage.getItem(key) 7 | } 8 | 9 | export function remove (key) { 10 | return window.localStorage.removeItem(key) 11 | } 12 | 13 | export function clear () { 14 | window.localStorage.clear() 15 | } 16 | -------------------------------------------------------------------------------- /lib/persistence/store.js: -------------------------------------------------------------------------------- 1 | import * as constants from '../constants' 2 | import * as storage from './storage' 3 | 4 | export default function persistenceHandler (next) { 5 | return (reducer, initialState) => { 6 | const store = next(reducer, initialState) 7 | 8 | return Object.assign({}, store, { 9 | dispatch (action) { 10 | store.dispatch(action) 11 | 12 | storage.put('locale', store.getState().application.locale) 13 | 14 | if (action.type === constants.LOGGED_IN) 15 | storage.put('token', action.payload.token) 16 | 17 | if (action.type === constants.LOG_OUT) 18 | storage.remove('token') 19 | 20 | return action 21 | } 22 | }) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /lib/reducers/application.js: -------------------------------------------------------------------------------- 1 | import * as constants from '../constants' 2 | import createReducer from '../utils/create-reducer' 3 | 4 | const initialState = { 5 | token: null, 6 | locale: 'en', 7 | user: { 8 | // TODO: have a checkbox to update the state 9 | // e.g.: on the login page and/or menu 10 | // permissions: ['manage_account'] 11 | permissions: [] 12 | }, 13 | error: null 14 | } 15 | 16 | const actionHandlers = { 17 | [constants.LOGGED_IN]: (_, action) => action.payload, 18 | [constants.LOG_OUT]: () => ({ token: null }), 19 | [constants.LOCALE_SWITCHED]: (_, action) => ({ locale: action.payload }), 20 | 21 | // TODO: this handle only API error responses. 22 | // We should also handle all other kind of application errors, 23 | // report them and show some kind of helpful message to the user. 24 | [constants.SHOW_ERROR]: (state, action) => { 25 | const { payload, source } = action 26 | return Object.assign({}, state, { 27 | // TODO: ideally we want to map API error response codes 28 | // with some user-friendly messages. 29 | error: { 30 | source, 31 | message: payload.message, 32 | statusCode: payload.statusCode || payload.code, 33 | body: payload.body || (payload instanceof Error ? 34 | (payload.toString() + '\n' + payload.stack) : payload) 35 | } 36 | }) 37 | }, 38 | [constants.HIDE_ERROR]: state => ({ ...state, ...{ error: null } }), 39 | } 40 | 41 | export default createReducer(initialState, actionHandlers) 42 | -------------------------------------------------------------------------------- /lib/reducers/github.js: -------------------------------------------------------------------------------- 1 | import * as constants from '../constants' 2 | import createReducer from '../utils/create-reducer' 3 | 4 | const initialState = { 5 | user: {}, 6 | repo: {}, 7 | stargazers: { 8 | user: [], 9 | repo: [], 10 | pagination: {} 11 | } 12 | } 13 | 14 | const actionHandlers = { 15 | [constants.FETCH_USER]: (state, action) => ({ user: action.user }), 16 | [constants.FETCH_REPO]: (state, action) => ({ repo: action.repo }), 17 | [constants.FETCH_USER_STARGAZERS]: (state, action) => ( 18 | { 19 | stargazers: Object.assign({}, state.stargazers, { 20 | user: action.stargazers, 21 | pagination: action.pagination 22 | }) 23 | }), 24 | [constants.FETCH_REPO_STARGAZERS]: (state, action) => ( 25 | { 26 | stargazers: Object.assign({}, state.stargazers, { 27 | repo: action.stargazers, 28 | pagination: action.pagination 29 | }) 30 | }) 31 | } 32 | 33 | export default createReducer(initialState, actionHandlers) 34 | -------------------------------------------------------------------------------- /lib/reducers/index.js: -------------------------------------------------------------------------------- 1 | export { default as application } from './application' 2 | export { default as github } from './github' 3 | -------------------------------------------------------------------------------- /lib/utils/configure-store.js: -------------------------------------------------------------------------------- 1 | /* global __DEVTOOLS__ */ 2 | import { createStore, combineReducers, compose, applyMiddleware } from 'redux' 3 | import { reduxReactRouter, routerStateReducer } from 'redux-router' 4 | import createBrowserHistory from 'history/lib/createBrowserHistory' 5 | import createHashHistory from 'history/lib/createHashHistory' 6 | import thunk from 'redux-thunk' 7 | import logger from '../middleware/logger' 8 | import persistenceStore from '../persistence/store' 9 | import * as reducers from '../reducers' 10 | 11 | // Use hash location for Github Pages 12 | // but switch to HTML5 history locally. 13 | const createHistory = process.env.NODE_ENV === 'production' ? 14 | createHashHistory : createBrowserHistory 15 | 16 | const storeEnhancers = [ 17 | persistenceStore, 18 | reduxReactRouter({ createHistory }) 19 | ] 20 | 21 | if (__DEVTOOLS__) { 22 | const DevTools = require('../components/DevTools').default 23 | storeEnhancers.push(DevTools.instrument()) 24 | } 25 | 26 | const finalCreateStore = compose( 27 | applyMiddleware(thunk, logger), 28 | ...storeEnhancers 29 | )(createStore) 30 | 31 | const combinedReducer = combineReducers(Object.assign({ 32 | router: routerStateReducer 33 | }, reducers)) 34 | 35 | export default function configureStore (initialState) { 36 | 37 | const store = finalCreateStore(combinedReducer, initialState) 38 | 39 | if (module.hot) 40 | // Enable Webpack hot module replacement for reducers 41 | module.hot.accept('../reducers', () => { 42 | const nextRootReducer = require('../reducers').default 43 | store.replaceReducer(nextRootReducer) 44 | }) 45 | 46 | return store 47 | } 48 | -------------------------------------------------------------------------------- /lib/utils/create-reducer.js: -------------------------------------------------------------------------------- 1 | export default function createReducer (initialState, actionHandlers) { 2 | return (state = initialState, action) => { 3 | const reduceFn = actionHandlers[action.type] 4 | if (!reduceFn) return state 5 | 6 | return { ...state, ...reduceFn(state, action) } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /lib/utils/handle-action-error.js: -------------------------------------------------------------------------------- 1 | import { SHOW_ERROR } from '../constants' 2 | 3 | export default function handleActionError (dispatch, error, source) { 4 | return dispatch({ 5 | type: SHOW_ERROR, 6 | source, 7 | payload: error 8 | }) 9 | } 10 | -------------------------------------------------------------------------------- /lib/utils/process-response.js: -------------------------------------------------------------------------------- 1 | export default function processResponse (response) { 2 | let isOk = response.ok 3 | 4 | return response.text() 5 | .then(body => { 6 | try { body = JSON.parse(body) } 7 | catch (error) { if (isOk) isOk = false } 8 | 9 | if (isOk) return body 10 | 11 | throw { ...body, statusCode: response.status } 12 | }) 13 | } 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redux-react-router-async-example", 3 | "version": "0.0.1", 4 | "description": "", 5 | "homepage": "https://github.com/emmenko/redux-react-router-async-example", 6 | "author": "Nicola Molinari ", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/emmenko/redux-react-router-async-example.git" 10 | }, 11 | "license": "MIT", 12 | "bugs": { 13 | "url": "https://github.com/emmenko/redux-react-router-async-example/issues" 14 | }, 15 | "scripts": { 16 | "build": "npm run clean && npm run lint && webpack", 17 | "build:i18n": "rimraf _translations && BABEL_ENV=i18n babel --quiet lib > /dev/null", 18 | "build:i18n:langs": "npm run build:i18n && babel-node scripts/i18nToXliff.js", 19 | "clean": "rimraf dist", 20 | "deploy": "npm run dist && gh-pages -d dist", 21 | "dist": "npm run clean && npm run lint && NODE_ENV=production webpack --config webpack.config.production.js", 22 | "lint": "eslint lib", 23 | "start": "npm run build && node server.js", 24 | "start:devtools": "npm run build && DEVTOOLS=true node server.js", 25 | "test": "echo \"Error: no test specified\" && exit 1" 26 | }, 27 | "dependencies": { 28 | "classnames": "~2.2.3", 29 | "history": "~1.17.0", 30 | "intl": "~1.0.1", 31 | "parse-link-header": "~0.4.1", 32 | "purecss": "~0.6.0", 33 | "react": "~0.14.6", 34 | "react-dom": "~0.14.6", 35 | "react-intl": "2.0.0-beta-1", 36 | "react-pure-render": "~1.0.2", 37 | "react-redux": "~4.0.6", 38 | "react-router": "~1.0.3", 39 | "redux": "~3.0.5", 40 | "redux-router": "~1.0.0-beta7", 41 | "redux-thunk": "~1.0.3", 42 | "whatwg-fetch": "~0.10.1" 43 | }, 44 | "devDependencies": { 45 | "babel-cli": "^6.4.0", 46 | "babel-core": "^6.4.0", 47 | "babel-eslint": "^5.0.0-beta6", 48 | "babel-loader": "^6.2.1", 49 | "babel-plugin-react-intl": "^2.0.0", 50 | "babel-plugin-react-transform": "^2.0.0", 51 | "babel-plugin-transform-decorators": "^6.4.0", 52 | "babel-plugin-transform-runtime": "^6.4.0", 53 | "babel-polyfill": "^6.3.14", 54 | "babel-preset-es2015": "^6.3.13", 55 | "babel-preset-react": "^6.3.13", 56 | "babel-preset-stage-0": "^6.3.13", 57 | "babel-runtime": "^6.3.19", 58 | "css-loader": "^0.23.1", 59 | "cssnext-loader": "^1.0.1", 60 | "eslint": "^1.10.3", 61 | "eslint-plugin-react": "^3.14.0", 62 | "express": "^4.13.3", 63 | "extract-text-webpack-plugin": "^1.0.1", 64 | "gh-pages": "^0.8.0", 65 | "glob": "^6.0.4", 66 | "html-webpack-plugin": "^1.7.0", 67 | "mkdirp": "^0.5.1", 68 | "react-transform-catch-errors": "^1.0.1", 69 | "react-transform-hmr": "^1.0.1", 70 | "redbox-react": "^1.2.0", 71 | "redux-devtools": "~3.0.1", 72 | "redux-devtools-dock-monitor": "~1.0.1", 73 | "redux-devtools-log-monitor": "~1.0.1", 74 | "rimraf": "^2.5.0", 75 | "style-loader": "^0.13.0", 76 | "webpack": "^1.12.10", 77 | "webpack-dev-middleware": "^1.4.0", 78 | "webpack-hot-middleware": "^2.6.0" 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /scripts/i18nToXliff.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import { sync as globSync } from 'glob' 3 | import { sync as mkdirpSync } from 'mkdirp' 4 | import * as i18n from '../lib/i18n' 5 | 6 | const MESSAGES_PATTERN = './_translations/**/*.json' 7 | const LANG_DIR = './_translations/lang/' 8 | // Ensure output folder exists 9 | mkdirpSync(LANG_DIR) 10 | 11 | // Aggregates the default messages that were extracted from the example app's 12 | // React components via the React Intl Babel plugin. An error will be thrown if 13 | // there are messages in different components that use the same `id`. The result 14 | // is a flat collection of `id: message` pairs for the app's default locale. 15 | let defaultMessages = globSync(MESSAGES_PATTERN) 16 | .map(filename => fs.readFileSync(filename, 'utf8')) 17 | .map(file => JSON.parse(file)) 18 | .reduce((collection, descriptors) => { 19 | descriptors.forEach(({ id, defaultMessage, description }) => { 20 | if (collection.hasOwnProperty(id)) 21 | throw new Error(`Duplicate message id: ${id}`) 22 | 23 | collection[id] = { defaultMessage, description } 24 | }) 25 | return collection 26 | }, {}) 27 | 28 | // Sort keys by name 29 | const messageKeys = Object.keys(defaultMessages) 30 | messageKeys.sort() 31 | defaultMessages = messageKeys.reduce((acc, key) => { 32 | acc[key] = defaultMessages[key] 33 | return acc 34 | }, {}) 35 | 36 | // Build the XLIFF document for the available languages 37 | i18n.en = messageKeys.reduce((acc, key) => { 38 | acc[key] = defaultMessages[key].defaultMessage 39 | return acc 40 | }, {}) 41 | Object.keys(i18n).forEach(lang => { 42 | const langDoc = i18n[lang] 43 | const units = buildUnits(defaultMessages, langDoc) 44 | 45 | fs.writeFileSync(`${LANG_DIR}${lang}.xml`, buildXliffDocument(lang, units)) 46 | }) 47 | 48 | function buildUnits (source, target) { 49 | return Object.keys(source).map(key => { 50 | const sourceMessage = source[key] 51 | const targetMessage = target[key] 52 | 53 | return buildXliffUnit(key, 54 | sourceMessage.description, sourceMessage.defaultMessage, 55 | targetMessage, targetMessage ? 'translated' : undefined) 56 | }) 57 | } 58 | 59 | function buildXliffUnit (id, note, source, target, state = 'initial') { 60 | return ` 61 | 62 | 63 | ${note || ''} 64 | 65 | 66 | 67 | 68 | 69 | 70 | ` 71 | } 72 | 73 | function buildXliffDocument (lang, units) { 74 | /*eslint-disable max-len*/ 75 | return ` 76 | 77 | 78 | ${units.join('')} 79 | 80 | 81 | ` 82 | /*eslint-enable max-len*/ 83 | } 84 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var express = require('express'); 3 | var webpack = require('webpack'); 4 | var config = require('./webpack.config'); 5 | 6 | var app = express(); 7 | var compiler = webpack(config); 8 | 9 | app.use(require('webpack-dev-middleware')(compiler, { 10 | noInfo: true, 11 | publicPath: config.output.publicPath 12 | })); 13 | 14 | app.use(require('webpack-hot-middleware')(compiler)); 15 | 16 | app.get('*', function (req, res) { 17 | console.log(req.url); 18 | res.sendFile(path.join(__dirname, './dist/index.html')); 19 | }); 20 | 21 | app.listen(3001, 'localhost', function (err) { 22 | if (err) { 23 | console.error(err); 24 | return; 25 | } 26 | 27 | console.log('Listening at http://localhost:3001'); 28 | }); -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var webpack = require('webpack'); 3 | var HtmlWebpackPlugin = require('html-webpack-plugin'); 4 | 5 | module.exports = { 6 | devtool: 'cheap-module-eval-source-map', 7 | entry: [ 8 | 'babel-polyfill', 9 | 'webpack-hot-middleware/client', 10 | './lib/index' 11 | ], 12 | output: { 13 | filename: 'app.js', 14 | path: path.join(__dirname, 'dist'), 15 | publicPath: '/assets/' 16 | }, 17 | plugins: [ 18 | new webpack.HotModuleReplacementPlugin(), 19 | new webpack.NoErrorsPlugin(), 20 | new webpack.DefinePlugin({ 21 | 'process.env': { 22 | 'NODE_ENV': JSON.stringify('development') 23 | }, 24 | '__DEVTOOLS__': process.env.DEVTOOLS === 'true' ? true : false 25 | }), 26 | new HtmlWebpackPlugin({ 27 | title: 'Redux React Router Async Example', 28 | filename: 'index.html', 29 | template: 'index.template.html', 30 | favicon: path.join(__dirname, 'assets', 'images', 'favicon.ico') 31 | }) 32 | ], 33 | module: { 34 | loaders: [ 35 | { test: /\.css$/, loader: 'style-loader!css-loader!cssnext-loader' }, 36 | { test: /\.js$/, loader: 'babel', include: path.join(__dirname, 'lib') } 37 | ] 38 | }, 39 | cssnext: { 40 | browsers: 'last 2 versions' 41 | } 42 | }; 43 | -------------------------------------------------------------------------------- /webpack.config.production.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var webpack = require('webpack'); 3 | var ExtractTextPlugin = require('extract-text-webpack-plugin'); 4 | var HtmlWebpackPlugin = require('html-webpack-plugin'); 5 | 6 | module.exports = { 7 | entry: { 8 | app: [ 'babel-polyfill', './lib/index.js' ] 9 | }, 10 | output: { 11 | filename: '[name].min.js', 12 | path: path.join(__dirname, 'dist'), 13 | publicPath: '' 14 | }, 15 | plugins: [ 16 | new webpack.optimize.OccurenceOrderPlugin(), 17 | new webpack.NoErrorsPlugin(), 18 | new webpack.DefinePlugin({ 19 | 'process.env': { 20 | 'NODE_ENV': JSON.stringify('production') 21 | }, 22 | '__DEVTOOLS__': false 23 | }), 24 | new webpack.optimize.UglifyJsPlugin({ 25 | compressor: { 26 | warnings: false 27 | } 28 | }), 29 | new ExtractTextPlugin('app.css', { allChunks: true }), 30 | new HtmlWebpackPlugin({ 31 | title: 'Redux React Router Async Example', 32 | filename: 'index.html', 33 | template: 'index.template.html', 34 | favicon: path.join(__dirname, 'assets/images/favicon.ico') 35 | }) 36 | ], 37 | module: { 38 | loaders: [ 39 | { test: /\.css$/, loader: ExtractTextPlugin.extract('style-loader', 'css-loader!cssnext-loader') }, 40 | { test: /\.js$/, loaders: ['babel'], include: path.join(__dirname, 'lib') } 41 | ] 42 | }, 43 | cssnext: { 44 | browsers: 'last 2 versions' 45 | } 46 | }; --------------------------------------------------------------------------------