├── .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 |
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 |
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 |
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 |
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 |
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 |
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 | };
--------------------------------------------------------------------------------