├── src ├── static │ ├── robots.txt │ ├── favicon.ico │ ├── images │ │ └── bootstrap-logo.png │ ├── fonts │ │ ├── OpenSans-Regular-webfont.eot │ │ ├── OpenSans-Regular-webfont.ttf │ │ └── OpenSans-Regular-webfont.woff │ └── humans.txt ├── routes │ ├── Home │ │ ├── index.js │ │ └── Home.js │ ├── Detail │ │ ├── index.js │ │ └── Detail.js │ └── index.js ├── components │ ├── Header │ │ ├── index.js │ │ ├── Header.scss │ │ └── Header.js │ ├── Search │ │ ├── index.js │ │ ├── assets │ │ │ ├── Octocat.png │ │ │ └── light-searchIcon.png │ │ ├── Search.scss │ │ ├── SearchStories.js │ │ └── Search.js │ ├── CardView │ │ ├── index.js │ │ ├── CardViewStories.js │ │ └── CardView.js │ ├── RepoDetail │ │ ├── index.js │ │ └── RepoDetail.js │ ├── RepoList │ │ ├── index.js │ │ ├── RepoListStories.js │ │ └── RepoList.js │ ├── TextQuestion │ │ ├── index.js │ │ ├── TextQuestion.scss │ │ ├── TextQuestionStories.js │ │ └── TextQuestion.js │ └── Counter │ │ ├── index.js │ │ ├── Counter.scss │ │ └── Counter.js ├── containers │ ├── SearchContainer │ │ ├── index.js │ │ ├── SearchDucks.js │ │ └── SearchContainer.js │ ├── RepositoriesContainer │ │ ├── index.js │ │ ├── RepositoriesContainer.js │ │ └── RepositoriesDucks.js │ ├── RepositoriesDetailContainer │ │ ├── index.js │ │ ├── RepositoriesDetailContainer.js │ │ └── RepositoriesDetailDuck.js │ └── AppContainer.js ├── layouts │ └── CoreLayout │ │ ├── CoreLayout.scss │ │ ├── index.js │ │ └── CoreLayout.js ├── styles │ ├── core.scss │ ├── _bootstrap_post_variables.scss │ ├── resources │ │ ├── readme.txt │ │ ├── _colors.scss │ │ └── _typography.scss │ ├── _bootstrap_pre_variables.scss │ ├── _base.scss │ └── fonts │ │ └── open-sans │ │ └── open-sans.css ├── index.html ├── store │ ├── reducers.js │ └── createStore.js ├── utils │ └── ApiUtils.js └── main.js ├── .gitignore ├── blueprints ├── component │ ├── files │ │ └── src │ │ │ └── components │ │ │ └── __name__ │ │ │ ├── __name__.scss │ │ │ ├── index.js │ │ │ └── __name__.js │ └── index.js ├── route │ ├── files │ │ └── src │ │ │ └── routes │ │ │ └── __name__ │ │ │ ├── components │ │ │ ├── __name__.scss │ │ │ └── __name__.js │ │ │ ├── index.js │ │ │ ├── containers │ │ │ └── __name__Container.js │ │ │ └── modules │ │ │ └── __name__.js │ └── index.js └── .eslintrc ├── nodemon.json ├── .eslintignore ├── .reduxrc ├── tests ├── .eslintrc ├── routes │ └── Home │ │ └── Home.spec.js ├── layouts │ └── CoreLayout.spec.js ├── framework.spec.js ├── test-bundler.js └── components │ ├── RepoList │ └── RepoList.spec.js │ └── CardView │ └── CardView.spec.js ├── .storybook ├── webpack.config.js └── config.js ├── .travis.yml ├── bin ├── server.js └── compile.js ├── .babelrc ├── server ├── lib │ └── apply-express-middleware.js ├── middleware │ ├── webpack-hmr.js │ └── webpack-dev.js └── main.js ├── .eslintrc ├── .editorconfig ├── LICENSE ├── config ├── environments.js └── index.js ├── CONTRIBUTING.md ├── .bootstraprc ├── README.md ├── package.json ├── README-redux.md └── CHANGELOG.md /src/static/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: 3 | -------------------------------------------------------------------------------- /src/routes/Home/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './Home.js'; 2 | -------------------------------------------------------------------------------- /src/components/Header/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './Header'; 2 | -------------------------------------------------------------------------------- /src/components/Search/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './Search.js'; 2 | -------------------------------------------------------------------------------- /src/routes/Detail/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './Detail.js'; 2 | -------------------------------------------------------------------------------- /src/components/CardView/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './CardView.js'; 2 | -------------------------------------------------------------------------------- /src/components/RepoDetail/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './RepoDetail'; 2 | -------------------------------------------------------------------------------- /src/components/RepoList/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './RepoList.js'; 2 | -------------------------------------------------------------------------------- /src/components/TextQuestion/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './TextQuestion.js'; 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_STORE 2 | *.log 3 | 4 | node_modules 5 | .idea 6 | 7 | dist 8 | coverage 9 | -------------------------------------------------------------------------------- /src/containers/SearchContainer/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './SearchContainer.js'; 2 | -------------------------------------------------------------------------------- /src/layouts/CoreLayout/CoreLayout.scss: -------------------------------------------------------------------------------- 1 | .mainContainer { 2 | padding-top:20px; 3 | } 4 | -------------------------------------------------------------------------------- /blueprints/component/files/src/components/__name__/__name__.scss: -------------------------------------------------------------------------------- 1 | .<%= pascalEntityName %> { 2 | } 3 | -------------------------------------------------------------------------------- /nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "verbose": false, 3 | "ignore": ["dist", "coverage", "tests", "src"] 4 | } 5 | -------------------------------------------------------------------------------- /src/components/Counter/index.js: -------------------------------------------------------------------------------- 1 | import Counter from './Counter'; 2 | 3 | export default Counter; 4 | -------------------------------------------------------------------------------- /src/containers/RepositoriesContainer/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './RepositoriesContainer.js'; 2 | -------------------------------------------------------------------------------- /blueprints/route/files/src/routes/__name__/components/__name__.scss: -------------------------------------------------------------------------------- 1 | .<%= pascalEntityName %> { 2 | 3 | } 4 | -------------------------------------------------------------------------------- /src/layouts/CoreLayout/index.js: -------------------------------------------------------------------------------- 1 | import CoreLayout from './CoreLayout'; 2 | 3 | export default CoreLayout; 4 | -------------------------------------------------------------------------------- /src/containers/RepositoriesDetailContainer/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './RepositoriesDetailContainer.js'; 2 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | blueprints/**/files/** 2 | coverage/** 3 | node_modules/** 4 | dist/** 5 | *.spec.js 6 | src/index.html 7 | -------------------------------------------------------------------------------- /src/components/Header/Header.scss: -------------------------------------------------------------------------------- 1 | .activeRoute { 2 | font-weight: bold; 3 | text-decoration: underline; 4 | } 5 | -------------------------------------------------------------------------------- /src/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeloitteDigitalUK/react-redux-starter-app/HEAD/src/static/favicon.ico -------------------------------------------------------------------------------- /src/static/images/bootstrap-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeloitteDigitalUK/react-redux-starter-app/HEAD/src/static/images/bootstrap-logo.png -------------------------------------------------------------------------------- /src/components/Search/assets/Octocat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeloitteDigitalUK/react-redux-starter-app/HEAD/src/components/Search/assets/Octocat.png -------------------------------------------------------------------------------- /.reduxrc: -------------------------------------------------------------------------------- 1 | { 2 | "sourceBase":"src", 3 | "testBase":"tests", 4 | "smartPath":"containers", 5 | "dumbPath":"components", 6 | "fileCasing":"pascal" 7 | } 8 | -------------------------------------------------------------------------------- /src/static/fonts/OpenSans-Regular-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeloitteDigitalUK/react-redux-starter-app/HEAD/src/static/fonts/OpenSans-Regular-webfont.eot -------------------------------------------------------------------------------- /src/static/fonts/OpenSans-Regular-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeloitteDigitalUK/react-redux-starter-app/HEAD/src/static/fonts/OpenSans-Regular-webfont.ttf -------------------------------------------------------------------------------- /src/static/fonts/OpenSans-Regular-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeloitteDigitalUK/react-redux-starter-app/HEAD/src/static/fonts/OpenSans-Regular-webfont.woff -------------------------------------------------------------------------------- /src/static/humans.txt: -------------------------------------------------------------------------------- 1 | # Check it out: http://humanstxt.org/ 2 | 3 | # TEAM 4 | 5 | -- -- 6 | 7 | # THANKS 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/styles/core.scss: -------------------------------------------------------------------------------- 1 | // Sample custom font import. Keep outside the :global scope 2 | @import 'fonts/open-sans/open-sans'; 3 | 4 | :global { 5 | @import 'base'; 6 | } 7 | -------------------------------------------------------------------------------- /blueprints/component/files/src/components/__name__/index.js: -------------------------------------------------------------------------------- 1 | import <%= pascalEntityName %> from './<%= pascalEntityName %>' 2 | 3 | export default <%= pascalEntityName %> 4 | -------------------------------------------------------------------------------- /src/components/Search/assets/light-searchIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeloitteDigitalUK/react-redux-starter-app/HEAD/src/components/Search/assets/light-searchIcon.png -------------------------------------------------------------------------------- /src/components/TextQuestion/TextQuestion.scss: -------------------------------------------------------------------------------- 1 | .label { 2 | font-size: 14px; 3 | line-height: 2rem; 4 | } 5 | 6 | .input { 7 | border: 1px solid #ccc; 8 | height: 2rem; 9 | } 10 | -------------------------------------------------------------------------------- /tests/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends" : "../.eslintrc", 3 | "env" : { 4 | "mocha" : true 5 | }, 6 | "globals" : { 7 | "expect" : false, 8 | "should" : false, 9 | "sinon" : false 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /blueprints/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends" : "../.eslintrc", 3 | "env" : { 4 | "mocha" : true 5 | }, 6 | "globals" : { 7 | "expect" : false, 8 | "should" : false, 9 | "sinon" : false 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/components/Counter/Counter.scss: -------------------------------------------------------------------------------- 1 | .counter { 2 | font-weight: bold; 3 | } 4 | 5 | .counter--green { 6 | composes: counter; 7 | color: rgb(25,200,25); 8 | } 9 | 10 | .counterContainer { 11 | margin: 1em auto; 12 | } 13 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | React Redux Starter App 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | -------------------------------------------------------------------------------- /src/styles/_bootstrap_post_variables.scss: -------------------------------------------------------------------------------- 1 | // This gets loaded after bootstrap/variables is loaded 2 | // Thus, you may customize Bootstrap variables 3 | // based on the values established in the Bootstrap _variables.scss file 4 | // 5 | // Defaults here: https://github.com/twbs/bootstrap/blob/v4-dev/scss/_variables.scss 6 | -------------------------------------------------------------------------------- /src/styles/resources/readme.txt: -------------------------------------------------------------------------------- 1 | To make the SCSS resource files in this folder available to component CSS files, 2 | you must add each new file to `webpackConfig.sassResources` in webpack.config.js 3 | 4 | Variables etc will then be available in your component's local CSS files. 5 | 6 | Search for `/_typography` as an example. 7 | -------------------------------------------------------------------------------- /blueprints/component/files/src/components/__name__/__name__.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import classes from './<%= pascalEntityName %>.scss' 3 | 4 | export const <%= pascalEntityName %> = () => ( 5 |
']}> 6 |

<%= pascalEntityName %>

7 |
8 | ) 9 | 10 | export default <%= pascalEntityName %> 11 | -------------------------------------------------------------------------------- /blueprints/route/files/src/routes/__name__/components/__name__.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import classes from './<%= pascalEntityName %>.scss' 3 | 4 | export const <%= pascalEntityName %> = () => ( 5 |
']}> 6 |

<%= pascalEntityName %>

7 |
8 | ) 9 | 10 | export default <%= pascalEntityName %> 11 | -------------------------------------------------------------------------------- /.storybook/webpack.config.js: -------------------------------------------------------------------------------- 1 | require('babel-register'); 2 | var mainWebpackConfig = require('../build/webpack.config.js').default; 3 | 4 | module.exports = (storybookBaseConfig, configType) => { 5 | storybookBaseConfig.module.loaders = mainWebpackConfig.module.loaders; 6 | storybookBaseConfig.sassResources = mainWebpackConfig.sassResources; 7 | return storybookBaseConfig; 8 | }; 9 | -------------------------------------------------------------------------------- /src/styles/_bootstrap_pre_variables.scss: -------------------------------------------------------------------------------- 1 | // Customize Bootstrap variables that get imported before the original Bootstrap variables. 2 | // Thus, derived Bootstrap variables can depend on values from here. 3 | // See the Bootstrap _variables.scss file for examples of derived Bootstrap variables. 4 | // 5 | // Defaults here: https://github.com/twbs/bootstrap/blob/v4-dev/scss/_variables.scss 6 | -------------------------------------------------------------------------------- /src/routes/Home/Home.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import Repositories from '../../containers/RepositoriesContainer/'; 4 | import Search from '../../containers/SearchContainer/'; 5 | 6 | export const Home = () => ( 7 |
8 |
9 | 10 | 11 |
12 |
13 | ); 14 | 15 | export default Home; 16 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - "6" 5 | 6 | cache: 7 | directories: 8 | - node_modules 9 | 10 | install: 11 | - npm install 12 | 13 | script: 14 | - npm run lint 15 | - npm run test 16 | - NODE_ENV=development npm run deploy 17 | - NODE_ENV=staging npm run deploy 18 | - NODE_ENV=production npm run deploy 19 | 20 | after_success: 21 | - npm run codecov 22 | -------------------------------------------------------------------------------- /bin/server.js: -------------------------------------------------------------------------------- 1 | import config from '../config' 2 | import server from '../server/main' 3 | import _debug from 'debug' 4 | 5 | const debug = _debug('app:bin:server') 6 | const port = config.server_port 7 | const host = config.server_host 8 | 9 | server.listen(port) 10 | debug(`Server is now running at http://${host}:${port}.`) 11 | debug(`Server accessible via localhost:${port} if you are using the project defaults.`) 12 | -------------------------------------------------------------------------------- /src/styles/resources/_colors.scss: -------------------------------------------------------------------------------- 1 | /*Variables can be used globally without importing*/ 2 | /*BRAND COLORS */ 3 | $gitHubBlue: #1DA1F2; 4 | $white: #ffffff; 5 | $black: #14171A; 6 | $darkGray: #657786; 7 | $lightGray:#AAB8C2; 8 | $lighterGray: #E1E8ED; 9 | $lightestGray: #F5F8FA; 10 | 11 | /*Secondary*/ 12 | $alertRed: #ff0000; 13 | $yellow: #ffff00; 14 | $green: #92C842; 15 | $darkBlue: #39afff; 16 | $lightBlue: #9DDCF2; 17 | -------------------------------------------------------------------------------- /src/routes/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Route, IndexRoute } from 'react-router'; 3 | 4 | import CoreLayout from '../layouts/CoreLayout/CoreLayout'; 5 | import Home from './Home'; 6 | import Detail from './Detail'; 7 | 8 | export default ( 9 | 10 | 11 | 12 | 13 | 14 | ); 15 | -------------------------------------------------------------------------------- /src/components/Header/Header.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { IndexLink } from 'react-router'; 3 | import classes from './Header.scss'; 4 | 5 | export const Header = () => ( 6 |
7 |

React Redux Starter App

8 |
    9 |
  • 10 | 11 | Home 12 | 13 |
  • 14 |
15 |
16 | ); 17 | 18 | export default Header; 19 | -------------------------------------------------------------------------------- /src/styles/_base.scss: -------------------------------------------------------------------------------- 1 | html { 2 | box-sizing: border-box; 3 | } 4 | 5 | html, 6 | body { 7 | height: 100%; 8 | font-family: 'open_sansregular', Helvetica Neue, Helvetica, Arial, sans-serif; 9 | } 10 | 11 | * { 12 | margin: 0; 13 | padding: 0; 14 | } 15 | 16 | *, 17 | *:before, 18 | *:after { 19 | box-sizing: inherit; 20 | } 21 | 22 | // Vertically center components in storybook. 23 | .storybook-container { 24 | height: 100vh; 25 | align-items: center; 26 | } 27 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | // NOTE: These options are overriden by the babel-loader configuration 2 | // for webpack, which can be found in ~/build/webpack.config. 3 | // 4 | // Why? The react-transform-hmr plugin depends on HMR (and throws if 5 | // module.hot is disabled), so keeping it and related plugins contained 6 | // within webpack helps prevent unexpected errors. 7 | { 8 | "presets": ["es2015", "react", "stage-0"], 9 | "plugins": ["transform-runtime", "transform-object-rest-spread"] 10 | } 11 | -------------------------------------------------------------------------------- /src/routes/Detail/Detail.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | import RepoDetail from '../../containers/RepositoriesDetailContainer/'; 3 | 4 | const Detail = (props) => ( 5 |
6 |
7 | 8 |
9 |
10 | ); 11 | 12 | Detail.propTypes = { 13 | params: PropTypes.shape({ 14 | id: PropTypes.string.isRequired, 15 | }), 16 | }; 17 | 18 | export default Detail; 19 | -------------------------------------------------------------------------------- /server/lib/apply-express-middleware.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | // Based on: https://github.com/dayAlone/koa-webpack-hot-middleware/blob/master/index.js 3 | export default function applyExpressMiddleware(fn, req, res) { 4 | const originalEnd = res.end; 5 | 6 | return new Promise((resolve) => { 7 | res.end = function () { 8 | originalEnd.apply(this, arguments); 9 | resolve(false); 10 | }; 11 | fn(req, res, function () { 12 | resolve(true); 13 | }); 14 | }); 15 | } 16 | -------------------------------------------------------------------------------- /src/components/TextQuestion/TextQuestionStories.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { storiesOf } from '@kadira/storybook'; 3 | 4 | import TextQuestion from './index'; 5 | 6 | storiesOf('TextQuestion', module) 7 | .add('Default State', () => ( 8 |
9 | 10 |
11 | )) 12 | .add('With a Value', () => ( 13 |
14 | 15 |
16 | )); 17 | -------------------------------------------------------------------------------- /src/layouts/CoreLayout/CoreLayout.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Header from '../../components/Header'; 3 | import classes from './CoreLayout.scss'; 4 | import '../../styles/core.scss'; 5 | 6 | export const CoreLayout = ({ children }) => ( 7 |
8 |
9 |
10 | {children} 11 |
12 |
13 | ); 14 | 15 | CoreLayout.propTypes = { 16 | children: React.PropTypes.element.isRequired, 17 | }; 18 | 19 | export default CoreLayout; 20 | -------------------------------------------------------------------------------- /src/styles/resources/_typography.scss: -------------------------------------------------------------------------------- 1 | /*Variables can be used globally without importing*/ 2 | //Font Sizes 3 | $typography1: 70px; 4 | $typography2: 32px; 5 | $typography3: 26px; 6 | $typography4: 16px; 7 | $typography5: 15px; 8 | $typography6: 12px; 9 | 10 | h1 { 11 | font-size: $typography1; 12 | } 13 | 14 | h2 { 15 | font-size: $typography2; 16 | } 17 | 18 | h3 { 19 | font-size: $typography3; 20 | } 21 | 22 | h4 { 23 | font-size: $typography4; 24 | } 25 | 26 | p { 27 | font-size: $typography5; 28 | } 29 | 30 | li { 31 | font-size: $typography6; 32 | } 33 | -------------------------------------------------------------------------------- /src/styles/fonts/open-sans/open-sans.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'open_sansregular'; 3 | src: url('../static/fonts/OpenSans-Regular-webfont.eot'); 4 | src: url('../static/fonts/OpenSans-Regular-webfont.eot?#iefix') format('embedded-opentype'), 5 | url('../static/fonts/OpenSans-Regular-webfont.woff') format('woff'), 6 | url('../static/fonts/OpenSans-Regular-webfont.ttf') format('truetype'), 7 | url('../static/fonts/OpenSans-Regular-webfont.svg#open_sansregular') format('svg'); 8 | font-weight: normal; 9 | font-style: normal; 10 | } 11 | -------------------------------------------------------------------------------- /.storybook/config.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { configure, addDecorator } from '@kadira/storybook'; 3 | 4 | // CSS 5 | import 'bootstrap-loader'; 6 | import '../src/styles/core.scss'; 7 | 8 | // Load all files ending in Stories.js 9 | const req = require.context('../src/', true, /Stories\.js$/); 10 | 11 | // Wrap all components in a container, and vertically center them. 12 | addDecorator(story => ( 13 |
14 |
15 | {story()} 16 |
17 |
18 | )); 19 | 20 | configure(() => { 21 | req.keys().forEach(req); 22 | }, module); 23 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | parser: 'babel-eslint' 2 | 3 | extends: ['airbnb'] 4 | 5 | plugins: ['babel', 'react', 'import'] 6 | 7 | env: 8 | browser: true 9 | 10 | globals: 11 | __DEV__: false 12 | __PROD__: false 13 | __DEBUG__: false 14 | __COVERAGE__: false 15 | __BASENAME__: false 16 | 17 | rules: 18 | #eslint base rules 19 | "arrow-parens": 0 20 | "generator-star-spacing": 0 21 | "max-len": [2, 120, 2] 22 | 23 | #react-plugin rules 24 | "react/jsx-filename-extension": [2, { "extensions": [".js"] }] 25 | "react/forbid-prop-types": 0 26 | "react/no-unused-prop-types": 0 27 | 28 | #import-plugin rules 29 | "import/no-named-as-default": 0 30 | -------------------------------------------------------------------------------- /server/middleware/webpack-hmr.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import WebpackHotMiddleware from 'webpack-hot-middleware'; 3 | import applyExpressMiddleware from '../lib/apply-express-middleware'; 4 | import _debug from 'debug'; 5 | 6 | const debug = _debug('app:server:webpack-hmr'); 7 | 8 | export default function (compiler, opts) { 9 | debug('Enable Webpack Hot Module Replacement (HMR).'); 10 | 11 | const middleware = WebpackHotMiddleware(compiler, opts); 12 | return async function koaWebpackHMR(ctx, next) { 13 | const hasNext = await applyExpressMiddleware(middleware, ctx.req, ctx.res); 14 | 15 | if (hasNext && next) { 16 | await next(); 17 | } 18 | }; 19 | } 20 | -------------------------------------------------------------------------------- /src/components/Search/Search.scss: -------------------------------------------------------------------------------- 1 | 2 | .input { 3 | height: 35px; 4 | width: 100%; 5 | border-radius: 5px; 6 | border: solid $darkGray 2px; 7 | padding-left: 15px; 8 | color: $darkGray; 9 | margin-bottom: 15px; 10 | } 11 | 12 | .label { 13 | padding-left: 0px; 14 | margin-top: 30px; 15 | } 16 | 17 | .icon { 18 | position: absolute; 19 | top: 10px; 20 | right: 30px; 21 | } 22 | 23 | .button { 24 | background-color: $gitHubBlue; 25 | color: $white; 26 | } 27 | 28 | .button:hover { 29 | background-color: $lightGray; 30 | } 31 | 32 | .gitHubIcon { 33 | margin-bottom: 20px; 34 | margin-right: 15px; 35 | } 36 | 37 | .error { 38 | margin-left: 15px; 39 | color: $alertRed; 40 | } 41 | -------------------------------------------------------------------------------- /blueprints/route/index.js: -------------------------------------------------------------------------------- 1 | // module.exports = { 2 | // locals: function(options) { 3 | // // Return custom template variables here. 4 | // return {}; 5 | // }, 6 | 7 | // fileMapTokens: function(options) ( 8 | // // Return custom tokens to be replaced in your files 9 | // return { 10 | // __token__: function(options){ 11 | // // logic to determine value goes here 12 | // return 'value'; 13 | // } 14 | // } 15 | // }, 16 | 17 | // Should probably never need to be overriden 18 | // 19 | // filesPath: function() { 20 | // return path.join(this.path, 'files'); 21 | // }, 22 | 23 | // beforeInstall: function(options) {}, 24 | // afterInstall: function(options) {}, 25 | // }; 26 | -------------------------------------------------------------------------------- /blueprints/component/index.js: -------------------------------------------------------------------------------- 1 | // module.exports = { 2 | // locals: function(options) { 3 | // // Return custom template variables here. 4 | // return {}; 5 | // }, 6 | 7 | // fileMapTokens: function(options) ( 8 | // // Return custom tokens to be replaced in your files 9 | // return { 10 | // __token__: function(options){ 11 | // // logic to determine value goes here 12 | // return 'value'; 13 | // } 14 | // } 15 | // }, 16 | 17 | // Should probably never need to be overriden 18 | // 19 | // filesPath: function() { 20 | // return path.join(this.path, 'files'); 21 | // }, 22 | 23 | // beforeInstall: function(options) {}, 24 | // afterInstall: function(options) {}, 25 | // }; 26 | -------------------------------------------------------------------------------- /src/components/TextQuestion/TextQuestion.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | import cx from 'classnames'; 3 | 4 | import styles from './TextQuestion.scss'; 5 | 6 | const TextQuestion = (props) => { 7 | const { label, value } = props; 8 | return ( 9 |
10 |
11 | {label} 12 |
13 | 14 |
15 | 16 |
17 |
18 | ); 19 | }; 20 | 21 | TextQuestion.propTypes = { 22 | label: PropTypes.string.isRequired, 23 | value: PropTypes.string, 24 | }; 25 | 26 | export default TextQuestion; 27 | -------------------------------------------------------------------------------- /tests/routes/Home/Home.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Home from '../../../src/routes/Home/'; 3 | import Search from '../../../src/containers/SearchContainer/'; 4 | import Repositories from '../../../src/containers/RepositoriesContainer/'; 5 | import { shallow } from 'enzyme'; 6 | 7 | describe('(Route) Home', () => { 8 | let wrapper; 9 | 10 | beforeEach(() => { 11 | wrapper = shallow(); 12 | }); 13 | 14 | it('renders successfully', () => { 15 | expect(wrapper).to.have.length(1); 16 | }); 17 | 18 | it('renders a SearchContainer', () => { 19 | expect(wrapper.find(Search)).to.have.length(1); 20 | }); 21 | 22 | it('renders a RepositoriesContainer', () => { 23 | expect(wrapper.find(Repositories)).to.have.length(1); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/containers/SearchContainer/SearchDucks.js: -------------------------------------------------------------------------------- 1 | // Actions 2 | const SEARCH = 'react-redux-starter-app/search/SEARCH'; 3 | 4 | const initialState = { 5 | heading: 'Search Github', 6 | inputValue: undefined, 7 | buttonText: 'Search', 8 | errorMessage: undefined, 9 | }; 10 | 11 | // Reducers 12 | const REDUCERS = { 13 | [SEARCH]: (state, action) => ({ 14 | ...state, 15 | inputValue: action.payload, 16 | }), 17 | }; 18 | 19 | export default function reducer(state = initialState, action = {}) { 20 | const handler = REDUCERS[action.type]; 21 | return handler ? handler(state, action) : state; 22 | } 23 | 24 | /* create search with inputValue */ 25 | export function createSearch(inputValue) { 26 | return { 27 | type: SEARCH, 28 | payload: inputValue, 29 | }; 30 | } 31 | -------------------------------------------------------------------------------- /src/containers/AppContainer.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | import { Router } from 'react-router'; 3 | import { Provider } from 'react-redux'; 4 | 5 | const AppContainer = (props) => { 6 | const { history, routes, routerKey, store } = props; 7 | 8 | /* eslint-disable react/no-children-prop */ 9 | return ( 10 | 11 |
12 | 13 |
14 |
15 | ); 16 | /* eslint-enable react/no-children-prop */ 17 | }; 18 | 19 | AppContainer.propTypes = { 20 | history: PropTypes.object.isRequired, 21 | routes: PropTypes.object.isRequired, 22 | routerKey: PropTypes.number, 23 | store: PropTypes.object.isRequired, 24 | }; 25 | 26 | export default AppContainer; 27 | -------------------------------------------------------------------------------- /src/components/Counter/Counter.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import classes from './Counter.scss'; 3 | 4 | export const Counter = props => ( 5 |
6 |

7 | Counter: 8 | {' '} 9 | 10 | {props.counter} 11 | 12 |

13 | 16 | {' '} 17 | 20 |
21 | ); 22 | 23 | Counter.propTypes = { 24 | counter: React.PropTypes.number.isRequired, 25 | doubleAsync: React.PropTypes.func.isRequired, 26 | increment: React.PropTypes.func.isRequired, 27 | }; 28 | 29 | export default Counter; 30 | -------------------------------------------------------------------------------- /bin/compile.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs-extra' 2 | import _debug from 'debug' 3 | import webpackCompiler from '../build/webpack-compiler' 4 | import webpackConfig from '../build/webpack.config' 5 | import config from '../config' 6 | 7 | const debug = _debug('app:bin:compile') 8 | const paths = config.utils_paths 9 | 10 | ;(async function () { 11 | try { 12 | debug('Run compiler') 13 | const stats = await webpackCompiler(webpackConfig) 14 | if (stats.warnings.length && config.compiler_fail_on_warning) { 15 | debug('Config set to fail on warning, exiting with status code "1".') 16 | process.exit(1) 17 | } 18 | debug('Copy static assets to dist folder.') 19 | fs.copySync(paths.client('static'), paths.dist()) 20 | } catch (e) { 21 | debug('Compiler encountered an error.', e) 22 | process.exit(1) 23 | } 24 | })() 25 | -------------------------------------------------------------------------------- /src/components/RepoDetail/RepoDetail.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | 3 | const RepoDetail = (props) => { 4 | const { isLoading, author, avatar, description, name, starsCount } = props; 5 | if (isLoading) { 6 | return

Loading...

; 7 | } 8 | 9 | return (
10 |

{name}

11 |
{description}
12 | {name} 13 |

Stars - {starsCount}

14 |

Author - {author}

15 |
); 16 | }; 17 | 18 | RepoDetail.propTypes = { 19 | isLoading: PropTypes.bool, 20 | author: PropTypes.string, 21 | avatar: PropTypes.string, 22 | description: PropTypes.string, 23 | id: PropTypes.number, 24 | name: PropTypes.string, 25 | starsCount: PropTypes.number, 26 | url: PropTypes.string, 27 | }; 28 | 29 | export default RepoDetail; 30 | -------------------------------------------------------------------------------- /tests/layouts/CoreLayout.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import TestUtils from 'react-addons-test-utils' 3 | import CoreLayout from 'layouts/CoreLayout/CoreLayout' 4 | 5 | function shallowRender (component) { 6 | const renderer = TestUtils.createRenderer() 7 | 8 | renderer.render(component) 9 | return renderer.getRenderOutput() 10 | } 11 | 12 | function shallowRenderWithProps (props = {}) { 13 | return shallowRender() 14 | } 15 | 16 | describe('(Layout) Core', function () { 17 | let _component 18 | let _props 19 | let _child 20 | 21 | beforeEach(function () { 22 | _child =

Child

23 | _props = { 24 | children: _child 25 | } 26 | 27 | _component = shallowRenderWithProps(_props) 28 | }) 29 | 30 | it('Should render as a
.', function () { 31 | expect(_component.type).to.equal('div') 32 | }) 33 | }) 34 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | # A special property that should be specified at the top of the file outside of 4 | # any sections. Set to true to stop .editor config file search on current file 5 | root = true 6 | 7 | [*] 8 | # Indentation style 9 | # Possible values - tab, space 10 | indent_style = space 11 | 12 | # Indentation size in single-spaced characters 13 | # Possible values - an integer, tab 14 | indent_size = 2 15 | 16 | # Line ending file format 17 | # Possible values - lf, crlf, cr 18 | end_of_line = lf 19 | 20 | # File character encoding 21 | # Possible values - latin1, utf-8, utf-16be, utf-16le 22 | charset = utf-8 23 | 24 | # Denotes whether to trim whitespace at the end of lines 25 | # Possible values - true, false 26 | trim_trailing_whitespace = true 27 | 28 | # Denotes whether file should end with a newline 29 | # Possible values - true, false 30 | insert_final_newline = true 31 | -------------------------------------------------------------------------------- /src/store/reducers.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import { routerReducer as router } from 'react-router-redux'; 3 | import { reducer as formReducer } from 'redux-form'; 4 | 5 | // Reducers 6 | import repositories from '../containers/RepositoriesContainer/RepositoriesDucks'; 7 | import repositoriesDetail from '../containers/RepositoriesDetailContainer/RepositoriesDetailDuck'; 8 | import search from '../containers/SearchContainer/SearchDucks'; 9 | 10 | export const makeRootReducer = asyncReducers => combineReducers({ 11 | form: formReducer, 12 | // Add sync reducers here 13 | search, 14 | repositories, 15 | repositoriesDetail, 16 | router, 17 | ...asyncReducers, 18 | }); 19 | 20 | export const injectReducer = (store, { key, reducer }) => { 21 | store.asyncReducers[key] = reducer; // eslint-disable-line no-param-reassign 22 | store.replaceReducer(makeRootReducer(store.asyncReducers)); 23 | }; 24 | 25 | export default makeRootReducer; 26 | -------------------------------------------------------------------------------- /blueprints/route/files/src/routes/__name__/index.js: -------------------------------------------------------------------------------- 1 | import { injectReducer } from '../../store/reducers' 2 | 3 | export default (store) => ({ 4 | path: '<%= dashesEntityName %>', 5 | /* Async getComponent is only invoked when route matches */ 6 | getComponent (nextState, cb) { 7 | /* Webpack - use 'require.ensure' to create a split point 8 | and embed an async module loader (jsonp) when bundling */ 9 | require.ensure([], (require) => { 10 | /* Webpack - use require callback to define 11 | dependencies for bundling */ 12 | const <%= pascalEntityName %> = require('./containers/<%= pascalEntityName %>Container').default 13 | const reducer = require('./modules/<%= pascalEntityName %>').default 14 | 15 | /* Add the reducer to the store on key 'counter' */ 16 | injectReducer(store, { key: '<%= pascalEntityName %>', reducer }) 17 | 18 | /* Return getComponent */ 19 | cb(null, <%= pascalEntityName %>) 20 | 21 | /* Webpack named bundle */ 22 | }, '<%= pascalEntityName %>') 23 | } 24 | }) 25 | -------------------------------------------------------------------------------- /server/middleware/webpack-dev.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import WebpackDevMiddleware from 'webpack-dev-middleware'; 3 | import applyExpressMiddleware from '../lib/apply-express-middleware'; 4 | import _debug from 'debug'; 5 | import config from '../../config'; 6 | 7 | const paths = config.utils_paths; 8 | const debug = _debug('app:server:webpack-dev'); 9 | 10 | export default function (compiler, publicPath) { 11 | debug('Enable webpack dev middleware.'); 12 | 13 | const middleware = WebpackDevMiddleware(compiler, { 14 | publicPath, 15 | contentBase: paths.client(), 16 | hot: true, 17 | quiet: config.compiler_quiet, 18 | noInfo: config.compiler_quiet, 19 | lazy: false, 20 | stats: config.compiler_stats, 21 | }); 22 | 23 | return async function koaWebpackDevMiddleware(ctx, next) { 24 | const hasNext = await applyExpressMiddleware(middleware, ctx.req, { 25 | end: (content) => (ctx.body = content), 26 | setHeader() { 27 | ctx.set.apply(ctx, arguments); 28 | }, 29 | }); 30 | 31 | if (hasNext) { 32 | await next(); 33 | } 34 | }; 35 | } 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 David Zukowski 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. 22 | -------------------------------------------------------------------------------- /src/utils/ApiUtils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Throw an error if the given response was not a 2xx status. 3 | * 4 | * @param {Object} response Fetch response object. 5 | * @return {Object|Error} The unmodified response if 2xx. Error otherwise. 6 | */ 7 | const checkStatus = response => { 8 | const { status, statusText } = response; 9 | if (status >= 200 && status < 300) { 10 | return response; 11 | } 12 | 13 | const error = new Error(statusText); 14 | error.response = response; 15 | throw error; 16 | }; 17 | 18 | /** 19 | * Parse the given response body as JSON. 20 | * 21 | * @param {Object} response Fetch response object. 22 | * @return {Object} Payload as JSON. 23 | */ 24 | const parseJSON = response => response.json(); 25 | 26 | export default { 27 | /** 28 | * HTTP GET to the given URL. 29 | * 30 | * @param {String} url URL to fetch. 31 | * @return {Promise} Resolves successfully on 2xx statuses. 32 | */ 33 | get: (url) => ( 34 | fetch(url) 35 | .then(checkStatus) 36 | .then(parseJSON) 37 | .catch(err => console.warn(err)) // eslint-disable-line no-console 38 | ), 39 | }; 40 | -------------------------------------------------------------------------------- /src/containers/RepositoriesContainer/RepositoriesContainer.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes, Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { bindActionCreators } from 'redux'; 4 | import _ from 'lodash'; 5 | 6 | // Actions 7 | import * as actionCreators from './RepositoriesDucks'; 8 | 9 | // Components 10 | import RepoList from '../../components/RepoList/'; 11 | 12 | const mapStateToProps = state => ({ 13 | isLoading: state.repositories.isLoading, 14 | repos: state.repositories.repos, 15 | }); 16 | 17 | const mapDispatchToProps = dispatch => bindActionCreators(actionCreators, dispatch); 18 | 19 | class RepositoriesContainer extends Component { 20 | 21 | static propTypes = { 22 | isLoading: PropTypes.bool.isRequired, 23 | repos: PropTypes.array, 24 | load: PropTypes.func.isRequired, 25 | }; 26 | 27 | constructor(props) { 28 | super(props); 29 | props.load(); 30 | } 31 | 32 | render() { 33 | const repoListProps = _.pick(this.props, ['isLoading', 'repos']); 34 | return ; 35 | } 36 | } 37 | 38 | export default connect(mapStateToProps, mapDispatchToProps)(RepositoriesContainer); 39 | -------------------------------------------------------------------------------- /src/containers/SearchContainer/SearchContainer.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { bindActionCreators } from 'redux'; 4 | import _ from 'lodash'; 5 | 6 | // Actions 7 | import * as actionCreators from './SearchDucks'; 8 | 9 | // Components 10 | import Search from '../../components/Search/'; 11 | 12 | const mapDispatchToProps = dispatch => bindActionCreators(actionCreators, dispatch); 13 | 14 | const mapStateToProps = state => ({ 15 | heading: state.search.heading, 16 | inputValue: state.search.inputValue, 17 | buttonText: state.search.buttonText, 18 | errorMessage: state.search.errorMessage, 19 | }); 20 | 21 | const SearchContainer = (props) => { 22 | const searchProps = _.pick(props, ['heading', 'inputValue', 'buttonText', 'errorMessage']); 23 | return ; 24 | }; 25 | 26 | SearchContainer.propTypes = { 27 | heading: PropTypes.string.isRequired, 28 | inputValue: PropTypes.string, 29 | buttonText: PropTypes.string.isRequired, 30 | errorMessage: PropTypes.string, 31 | createSearch: PropTypes.func.isRequired, 32 | }; 33 | 34 | export default connect(mapStateToProps, mapDispatchToProps)(SearchContainer); 35 | -------------------------------------------------------------------------------- /src/containers/RepositoriesDetailContainer/RepositoriesDetailContainer.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes, Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { bindActionCreators } from 'redux'; 4 | import _ from 'lodash'; 5 | 6 | // Actions 7 | import * as actionCreators from './RepositoriesDetailDuck'; 8 | 9 | // Components 10 | import RepoDetail from '../../components/RepoDetail/'; 11 | 12 | const mapStateToProps = (state, props) => 13 | ({ 14 | repos: state.repositoriesDetail.repos, 15 | id: props.id, 16 | }) 17 | ; 18 | 19 | const mapDispatchToProps = dispatch => bindActionCreators(actionCreators, dispatch); 20 | 21 | class RepositoriesDetailContainer extends Component { 22 | 23 | static propTypes = { 24 | id: PropTypes.string.isRequired, 25 | repos: PropTypes.array, 26 | load: PropTypes.func.isRequired, 27 | }; 28 | 29 | constructor(props) { 30 | super(props); 31 | props.load(); 32 | } 33 | 34 | render() { 35 | const repoId = parseInt(this.props.id, 10); 36 | const repo = _.find(this.props.repos, { id: repoId }); 37 | return ; 38 | } 39 | } 40 | 41 | export default connect(mapStateToProps, mapDispatchToProps)(RepositoriesDetailContainer); 42 | -------------------------------------------------------------------------------- /src/components/RepoList/RepoListStories.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { storiesOf } from '@kadira/storybook'; 3 | import _ from 'lodash'; 4 | 5 | import RepoList from './index'; 6 | 7 | const repo = { 8 | author: 'DeloitteDigitalUK', 9 | avatar: 'https://avatars.githubusercontent.com/u/10812267?v=3', 10 | description: 'Starter app with React, Redux, Webpack, CSS Modules, API calls etc.', 11 | name: 'react-redux-starter-app', 12 | starsCount: 3, 13 | url: 'https://github.com/DeloitteDigitalUK/react-redux-starter-app', 14 | }; 15 | 16 | const repos = _.range(0, 5).map(i => ({ 17 | id: i, 18 | ...repo, 19 | })); 20 | 21 | storiesOf('RepoList', module) 22 | .addDecorator(story => ( 23 |
24 | {story()} 25 |
26 | )) 27 | .add('Multiple Repos', () => ( 28 | 33 | )) 34 | .add('No Repos', () => ( 35 | 40 | )) 41 | .add('Loading', () => ( 42 | 47 | )); 48 | -------------------------------------------------------------------------------- /src/components/Search/SearchStories.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { storiesOf } from '@kadira/storybook'; 3 | 4 | import Search from './index'; 5 | 6 | storiesOf('Search', module) 7 | .addDecorator(story => ( 8 |
9 | {story()} 10 |
11 | )) 12 | .add('Default State', () => ( 13 | 17 | )) 18 | .add('With A Valid Search Term', () => ( 19 | 24 | )) 25 | .add('With an invalid Character', () => ( 26 | 32 | )) 33 | .add('Submitted without entering anything', () => ( 34 | 39 | )) 40 | .add('Clicking off Search box', () => ( 41 | 46 | )); 47 | -------------------------------------------------------------------------------- /tests/framework.spec.js: -------------------------------------------------------------------------------- 1 | import assert from 'assert' 2 | import React from 'react' 3 | import {mount, render, shallow} from 'enzyme' 4 | 5 | class Fixture extends React.Component { 6 | render () { 7 | return ( 8 |
9 | 10 | 11 |
12 | ) 13 | } 14 | } 15 | 16 | describe('(Framework) Karma Plugins', function () { 17 | it('Should expose "expect" globally.', function () { 18 | assert.ok(expect) 19 | }) 20 | 21 | it('Should expose "should" globally.', function () { 22 | assert.ok(should) 23 | }) 24 | 25 | it('Should have chai-as-promised helpers.', function () { 26 | const pass = new Promise(res => res('test')) 27 | const fail = new Promise((res, rej) => rej()) 28 | 29 | return Promise.all([ 30 | expect(pass).to.be.fulfilled, 31 | expect(fail).to.not.be.fulfilled 32 | ]) 33 | }) 34 | 35 | it('should have chai-enzyme working', function() { 36 | let wrapper = shallow() 37 | expect(wrapper.find('#checked')).to.be.checked() 38 | 39 | wrapper = mount() 40 | expect(wrapper.find('#checked')).to.be.checked() 41 | 42 | wrapper = render() 43 | expect(wrapper.find('#checked')).to.be.checked() 44 | }) 45 | }) 46 | -------------------------------------------------------------------------------- /src/components/RepoList/RepoList.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | import CardView from '../CardView/'; 3 | 4 | const RepoList = ({ isLoading, noReposText, repos }) => { 5 | if (isLoading) { 6 | return

Loading...

; 7 | } 8 | if (!repos || repos.length === 0) { 9 | return

{noReposText}

; 10 | } 11 | 12 | return (
13 | {repos.map(repo => 14 | , 27 | )} 28 |
); 29 | }; 30 | 31 | RepoList.propTypes = { 32 | isLoading: PropTypes.bool.isRequired, 33 | noReposText: PropTypes.string.isRequired, 34 | repos: PropTypes.arrayOf(PropTypes.shape({ 35 | author: PropTypes.string, 36 | avatar: PropTypes.string, 37 | description: PropTypes.string.isRequired, 38 | id: PropTypes.number.isRequired, 39 | name: PropTypes.string.isRequired, 40 | starsCount: PropTypes.number, 41 | url: PropTypes.string.isRequired, 42 | }).isRequired), 43 | }; 44 | 45 | export default RepoList; 46 | -------------------------------------------------------------------------------- /src/components/CardView/CardViewStories.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { storiesOf } from '@kadira/storybook'; 3 | 4 | import CardView from './index'; 5 | 6 | import logo from '../../static/images/bootstrap-logo.png'; 7 | 8 | storiesOf('CardView', module) 9 | .addDecorator(story => ( 10 |
11 | {story()} 12 |
13 | )) 14 | .add('Text Only', () => ( 15 | 19 | )) 20 | .add('With Button', () => ( 21 | 27 | )) 28 | .add('With Image', () => ( 29 | 35 | )) 36 | .add('With Everything', () => ( 37 | 45 | )); 46 | -------------------------------------------------------------------------------- /config/environments.js: -------------------------------------------------------------------------------- 1 | // Here is where you can define configuration overrides based on the execution environment. 2 | // Supply a key to the default export matching the NODE_ENV that you wish to target, and 3 | // the base configuration will apply your overrides before exporting itself. 4 | export default { 5 | // ====================================================== 6 | // Overrides when NODE_ENV === 'development' 7 | // ====================================================== 8 | // NOTE: In development, we use an explicit public path when the assets 9 | // are served webpack by to fix this issue: 10 | // http://stackoverflow.com/questions/34133808/webpack-ots-parsing-error-loading-fonts/34133809#34133809 11 | development: (config) => ({ 12 | compiler_public_path: `http://${config.server_host}:${config.server_port}/`, 13 | proxy: { 14 | enabled: false, 15 | options: { 16 | host: 'http://localhost:8000', 17 | match: /^\/api\/.*/, 18 | }, 19 | }, 20 | }), 21 | 22 | // ====================================================== 23 | // Overrides when NODE_ENV === 'production' 24 | // ====================================================== 25 | production: (config) => ({ 26 | compiler_public_path: '/', 27 | compiler_fail_on_warning: false, 28 | compiler_hash_type: 'chunkhash', 29 | compiler_devtool: null, 30 | compiler_stats: { 31 | chunks: true, 32 | chunkModules: true, 33 | colors: true, 34 | }, 35 | }), 36 | }; 37 | -------------------------------------------------------------------------------- /src/store/createStore.js: -------------------------------------------------------------------------------- 1 | import { applyMiddleware, compose, createStore } from 'redux'; 2 | import { routerMiddleware } from 'react-router-redux'; 3 | import thunk from 'redux-thunk'; 4 | import makeRootReducer from './reducers'; 5 | 6 | export default (initialState = {}, history) => { 7 | // ====================================================== 8 | // Middleware Configuration 9 | // ====================================================== 10 | const middleware = [thunk, routerMiddleware(history)]; 11 | 12 | // ====================================================== 13 | // Store Enhancers 14 | // ====================================================== 15 | const enhancers = []; 16 | if (__DEBUG__) { 17 | const devToolsExtension = window.devToolsExtension; 18 | if (typeof devToolsExtension === 'function') { 19 | enhancers.push(devToolsExtension()); 20 | } 21 | } 22 | 23 | // ====================================================== 24 | // Store Instantiation and HMR Setup 25 | // ====================================================== 26 | const store = createStore( 27 | makeRootReducer(), 28 | initialState, 29 | compose( 30 | applyMiddleware(...middleware), 31 | ...enhancers, 32 | ), 33 | ); 34 | store.asyncReducers = {}; 35 | 36 | if (module.hot) { 37 | module.hot.accept('./reducers', () => { 38 | const reducers = require('./reducers').default; // eslint-disable-line global-require 39 | 40 | store.replaceReducer(reducers); 41 | }); 42 | } 43 | 44 | return store; 45 | }; 46 | -------------------------------------------------------------------------------- /tests/test-bundler.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies */ 2 | // --------------------------------------- 3 | // Test Environment Setup 4 | // --------------------------------------- 5 | import 'babel-polyfill'; 6 | import sinon from 'sinon'; 7 | import chai from 'chai'; 8 | import sinonChai from 'sinon-chai'; 9 | import chaiAsPromised from 'chai-as-promised'; 10 | import chaiEnzyme from 'chai-enzyme'; 11 | 12 | chai.use(sinonChai); 13 | chai.use(chaiAsPromised); 14 | chai.use(chaiEnzyme()); 15 | 16 | global.chai = chai; 17 | global.sinon = sinon; 18 | global.expect = chai.expect; 19 | global.should = chai.should(); 20 | 21 | // --------------------------------------- 22 | // Require Tests 23 | // --------------------------------------- 24 | // for use with karma-webpack-with-fast-source-maps 25 | // NOTE: `new Array()` is used rather than an array literal since 26 | // for some reason an array literal without a trailing `;` causes 27 | // some build environments to fail. 28 | const __karmaWebpackManifest__ = new Array() // eslint-disable-line 29 | const inManifest = path => __karmaWebpackManifest__.indexOf(path) === -1; 30 | 31 | // require all `tests/**/*.spec.js` 32 | const testsContext = require.context('./', true, /\.spec\.js$/); 33 | 34 | // only run tests that have changed after the first pass. 35 | const testsToRun = testsContext.keys().filter(inManifest) 36 | ;(testsToRun.length ? testsToRun : testsContext.keys()).forEach(testsContext); 37 | 38 | // require all `src/**/*.js` except for `main.js` (for isparta coverage reporting) 39 | if (__COVERAGE__) { 40 | const componentsContext = require.context('../src/', true, /^((?!main|Stories).)*\.js$/); 41 | componentsContext.keys().forEach(componentsContext); 42 | } 43 | -------------------------------------------------------------------------------- /blueprints/route/files/src/routes/__name__/containers/__name__Container.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux' 2 | import { increment, doubleAsync } from '../modules/<%= pascalEntityName %>' 3 | 4 | /* This is a container component. Notice it does not contain any JSX, 5 | nor does it import React. This component is **only** responsible for 6 | wiring in the actions and state necessary to render a presentational 7 | component - in this case, the counter: */ 8 | 9 | import <%= pascalEntityName %> from '../components/<%= pascalEntityName %>' 10 | 11 | /* Object of action creators (can also be function that returns object). 12 | Keys will be passed as props to presentational components. Here we are 13 | implementing our wrapper around increment; the component doesn't care */ 14 | 15 | const mapActionCreators = { 16 | increment: () => increment(1), 17 | doubleAsync 18 | } 19 | 20 | const mapStateToProps = (state) => ({ 21 | counter: state.counter 22 | }) 23 | 24 | /* Note: mapStateToProps is where you should use `reselect` to create selectors, ie: 25 | 26 | import { createSelector } from 'reselect' 27 | const counter = (state) => state.counter 28 | const tripleCount = createSelector(counter, (count) => count * 3) 29 | const mapStateToProps = (state) => ({ 30 | counter: tripleCount(state) 31 | }) 32 | 33 | Selectors can compute derived data, allowing Redux to store the minimal possible state. 34 | Selectors are efficient. A selector is not recomputed unless one of its arguments change. 35 | Selectors are composable. They can be used as input to other selectors. 36 | https://github.com/reactjs/reselect */ 37 | 38 | export default connect(mapStateToProps, mapActionCreators)(<%= pascalEntityName %>) 39 | -------------------------------------------------------------------------------- /src/components/CardView/CardView.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | 3 | const CardView = (props) => { 4 | const { title, text, imageUrl, imageAlt, imageWidth, imageHeight, buttonUrl, buttonText, linkUrl, linkText } = props; 5 | 6 | const image = imageUrl 7 | ? ( 8 | {imageAlt} 15 | ) 16 | : undefined; 17 | 18 | const button = (buttonUrl && buttonText) 19 | ? ( 20 | 26 | {buttonText} 27 | 28 | ) 29 | : undefined; 30 | 31 | const link = (linkText && linkUrl) 32 | ? ( 33 | 37 | {linkText} 38 | 39 | ) 40 | : undefined; 41 | 42 | return ( 43 |
44 | {image} 45 |
46 |

{title}

47 |

{text}

48 | {button} 49 | {link} 50 |
51 |
52 | ); 53 | }; 54 | 55 | CardView.propTypes = { 56 | title: PropTypes.string.isRequired, 57 | text: PropTypes.string.isRequired, 58 | imageUrl: PropTypes.string, 59 | imageAlt: PropTypes.string, 60 | imageWidth: PropTypes.number, 61 | imageHeight: PropTypes.number, 62 | buttonUrl: PropTypes.string, 63 | buttonText: PropTypes.string, 64 | linkUrl: PropTypes.string, 65 | linkText: PropTypes.string, 66 | }; 67 | 68 | export default CardView; 69 | -------------------------------------------------------------------------------- /blueprints/route/files/src/routes/__name__/modules/__name__.js: -------------------------------------------------------------------------------- 1 | // ------------------------------------ 2 | // Constants 3 | // ------------------------------------ 4 | export const COUNTER_INCREMENT = '<%= pascalEntityName %>.COUNTER_INCREMENT' 5 | 6 | // ------------------------------------ 7 | // Actions 8 | // ------------------------------------ 9 | export function increment (value = 1) { 10 | return { 11 | type: COUNTER_INCREMENT, 12 | payload: value 13 | } 14 | } 15 | 16 | /* This is a thunk, meaning it is a function that immediately 17 | returns a function for lazy evaluation. It is incredibly useful for 18 | creating async actions, especially when combined with redux-thunk! 19 | 20 | NOTE: This is solely for demonstration purposes. In a real application, 21 | you'd probably want to dispatch an action of COUNTER_DOUBLE and let the 22 | reducer take care of this logic. */ 23 | 24 | export const doubleAsync = () => { 25 | return (dispatch, getState) => { 26 | return new Promise((resolve) => { 27 | setTimeout(() => { 28 | dispatch(increment(getState().counter)) 29 | resolve() 30 | }, 200) 31 | }) 32 | } 33 | } 34 | 35 | export const actions = { 36 | increment, 37 | doubleAsync 38 | } 39 | 40 | // ------------------------------------ 41 | // Action Handlers 42 | // ------------------------------------ 43 | const ACTION_HANDLERS = { 44 | [COUNTER_INCREMENT]: (state, action) => state + action.payload 45 | } 46 | 47 | // ------------------------------------ 48 | // Reducer 49 | // ------------------------------------ 50 | const initialState = 0 51 | export default function counterReducer (state = initialState, action) { 52 | const handler = ACTION_HANDLERS[action.type] 53 | 54 | return handler ? handler(state, action) : state 55 | } 56 | -------------------------------------------------------------------------------- /tests/components/RepoList/RepoList.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import RepoList from '../../../src/components/RepoList/'; 3 | import CardView from '../../../src/components/CardView/'; 4 | import { shallow } from 'enzyme'; 5 | 6 | 7 | describe('(Component) RepoList', () => { 8 | let wrapper, props; 9 | 10 | beforeEach(() => { 11 | props = { 12 | isLoading: false, 13 | noReposText: 'No repositories to display.', 14 | }; 15 | 16 | wrapper = undefined; 17 | }); 18 | 19 | // i.e. It isn't totally broken. Only component test you need? 20 | // https://gist.github.com/thevangelist/e2002bc6b9834def92d46e4d92f15874 21 | it('renders successfully', () => { 22 | wrapper = shallow(); 23 | expect(wrapper).to.have.length(1); 24 | }); 25 | 26 | it('shows a loading message when still loading', () => { 27 | wrapper = shallow(); 28 | 29 | const paragraphs = wrapper.find('p'); 30 | expect(paragraphs).to.have.length(1); 31 | expect(paragraphs.text()).to.match(/Loading/); 32 | }); 33 | 34 | it('shows a message when there are no repos', () => { 35 | wrapper = shallow(); 36 | 37 | const paragraphs = wrapper.find('p'); 38 | expect(paragraphs).to.have.length(1); 39 | expect(paragraphs.text()).to.match(/No repositories/); 40 | }); 41 | 42 | it('creates a CardView for each repo', () => { 43 | const repos = [ 44 | { 45 | id: 1, 46 | name: 'Test 1', 47 | description: 'Desc', 48 | url: 'http://site.com', 49 | }, 50 | { 51 | id: 2, 52 | name: 'Test 2', 53 | description: 'Desc', 54 | url: 'http://site.com', 55 | } 56 | ]; 57 | 58 | wrapper = shallow(); 59 | expect(wrapper.find(CardView)).to.have.length(repos.length); 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /src/components/Search/Search.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes, Component } from 'react'; 2 | import cx from 'classnames'; 3 | 4 | import styles from './Search.scss'; 5 | import SearchIcon from './assets/light-searchIcon.png'; 6 | import GitHubIcon from './assets/Octocat.png'; 7 | 8 | class Search extends Component { 9 | 10 | static propTypes = { 11 | heading: PropTypes.string.isRequired, 12 | inputValue: PropTypes.string, 13 | buttonText: PropTypes.string.isRequired, 14 | errorMessage: PropTypes.string, 15 | onSubmit: PropTypes.func, 16 | }; 17 | 18 | 19 | render() { 20 | const { heading, inputValue, buttonText, errorMessage, onSubmit } = this.props; 21 | return ( 22 |
23 |
24 | twitter-icon 25 |
26 |

{heading}

27 |
28 |
29 |
30 |
31 | { this.textInput = node; }} 36 | /> 37 | Search 38 |
39 |
40 | 47 |
48 |
49 |
50 |
51 |
{errorMessage}
52 |
53 |
54 |
55 | ); 56 | } 57 | } 58 | 59 | export default Search; 60 | -------------------------------------------------------------------------------- /src/containers/RepositoriesDetailContainer/RepositoriesDetailDuck.js: -------------------------------------------------------------------------------- 1 | import ApiUtils from '../../utils/ApiUtils'; 2 | 3 | // Actions 4 | const LOAD = 'react-redux-starter-app/repositoriesDetail/LOAD'; 5 | const LOAD_SUCCESS = 'react-redux-starter-app/repositoriesDetail/LOAD_SUCCESS'; 6 | const LOAD_ERROR = 'react-redux-starter-app/repositoriesDetail/LOAD_ERROR'; 7 | 8 | const initialState = { 9 | isLoading: false, 10 | repos: undefined, 11 | }; 12 | 13 | // Reducers 14 | const REDUCERS = { 15 | [LOAD]: (state) => ({ 16 | ...state, 17 | isLoading: true, 18 | }), 19 | [LOAD_SUCCESS]: (state, action) => ({ 20 | ...state, 21 | isLoading: false, 22 | repos: action.data.map(repo => ({ 23 | author: repo.owner.login, 24 | avatar: repo.owner.avatar_url, 25 | description: repo.description, 26 | id: repo.id, 27 | name: repo.name, 28 | starsCount: repo.stargazers_count, 29 | url: repo.html_url, 30 | })), 31 | }), 32 | [LOAD_ERROR]: (state, action) => ({ 33 | ...state, 34 | isLoading: false, 35 | error: action.error, 36 | }), 37 | }; 38 | 39 | export default function reducer(state = initialState, action = {}) { 40 | const handler = REDUCERS[action.type]; 41 | return handler ? handler(state, action) : state; 42 | } 43 | 44 | function loadSuccess(data) { 45 | return { 46 | type: LOAD_SUCCESS, 47 | data, 48 | }; 49 | } 50 | 51 | function loadError(error) { 52 | return { 53 | type: LOAD_ERROR, 54 | error, 55 | }; 56 | } 57 | 58 | // Action Creators 59 | export function load() { 60 | return (dispatch, getState) => { 61 | // Already loading? 62 | if (getState().repositories.isLoading) { 63 | return undefined; 64 | } 65 | 66 | // Notify that we're loading. 67 | dispatch({ 68 | type: LOAD, 69 | }); 70 | 71 | return ApiUtils.get('https://api.github.com/users/michael-martin/starred') 72 | .then(data => dispatch(loadSuccess(data))) 73 | .catch(err => dispatch(loadError(err))); 74 | }; 75 | } 76 | -------------------------------------------------------------------------------- /tests/components/CardView/CardView.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import CardView from '../../../src/components/CardView/'; 3 | import { shallow } from 'enzyme'; 4 | 5 | describe('(Component) CardView', () => { 6 | let wrapper, props; 7 | 8 | beforeEach(() => { 9 | props = { 10 | title: 'Test Title', 11 | text: 'Test text', 12 | }; 13 | 14 | wrapper = shallow(); 15 | }); 16 | 17 | // i.e. It isn't totally broken. Only component test you need? 18 | // https://gist.github.com/thevangelist/e2002bc6b9834def92d46e4d92f15874 19 | it('renders successfully', () => { 20 | expect(wrapper).to.have.length(1); 21 | }); 22 | 23 | it('renders the given title', () => { 24 | expect(wrapper.find('h4').text()).to.match(/Test Title/); 25 | }); 26 | 27 | it('renders the given text', () => { 28 | expect(wrapper.find('p').text()).to.match(/Test text/); 29 | }); 30 | 31 | describe('with an image', () => { 32 | beforeEach(() => { 33 | props = { 34 | ...props, 35 | imageUrl: 'mypic.png', 36 | imageAlt: 'Pic Alt', 37 | imageWidth: 100, 38 | imageHeight: 50, 39 | }; 40 | 41 | wrapper = shallow(); 42 | }); 43 | 44 | it('renders an image', () => { 45 | let image = wrapper.find('img'); 46 | expect(image).to.have.length(1); 47 | expect(image.prop('src')).to.equal('mypic.png'); 48 | expect(image.prop('alt')).to.equal('Pic Alt'); 49 | expect(image.prop('width')).to.equal(100); 50 | expect(image.prop('height')).to.equal(50); 51 | }); 52 | }); 53 | 54 | describe('with a button', () => { 55 | beforeEach(() => { 56 | props = { 57 | ...props, 58 | buttonUrl: 'http://site.com', 59 | buttonText: 'Click Me', 60 | }; 61 | 62 | wrapper = shallow(); 63 | }); 64 | 65 | it('renders a button', () => { 66 | let button = wrapper.find('a'); 67 | expect(button).to.have.length(1); 68 | expect(button.prop('href')).to.equal('http://site.com'); 69 | expect(button.text()).to.match(/Click Me/); 70 | }); 71 | }); 72 | }); 73 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Some basic conventions for contributing to this project. 4 | 5 | ### General 6 | 7 | Please make sure that there aren't existing pull requests attempting to address the issue mentioned. Likewise, please check for issues related to update, as someone else may be working on the issue in a branch or fork. 8 | 9 | * Non-trivial changes should be discussed in an issue first 10 | * Develop in a topic branch, not master 11 | * Squash your commits 12 | 13 | ### Linting 14 | 15 | Please check your code using `npm run lint` before submitting your pull requests, as the CI build will fail if `eslint` fails. 16 | 17 | ### Commit Message Format 18 | 19 | Each commit message should include a **type**, a **scope** and a **subject**: 20 | 21 | ``` 22 | (): 23 | ``` 24 | 25 | Lines should not exceed 100 characters. This allows the message to be easier to read on github as well as in various git tools and produces a nice, neat commit log ie: 26 | 27 | ``` 28 | #271 feat(standard): add style config and refactor to match 29 | #270 fix(config): only override publicPath when served by webpack 30 | #269 feat(eslint-config-defaults): replace eslint-config-airbnb 31 | #268 feat(config): allow user to configure webpack stats output 32 | ``` 33 | 34 | #### Type 35 | 36 | Must be one of the following: 37 | 38 | * **feat**: A new feature 39 | * **fix**: A bug fix 40 | * **docs**: Documentation only changes 41 | * **style**: Changes that do not affect the meaning of the code (white-space, formatting, missing 42 | semi-colons, etc) 43 | * **refactor**: A code change that neither fixes a bug or adds a feature 44 | * **test**: Adding missing tests 45 | * **chore**: Changes to the build process or auxiliary tools and libraries such as documentation 46 | generation 47 | 48 | #### Scope 49 | 50 | The scope could be anything specifying place of the commit change. For example `webpack`, 51 | `babel`, `redux` etc... 52 | 53 | #### Subject 54 | 55 | The subject contains succinct description of the change: 56 | 57 | * use the imperative, present tense: "change" not "changed" nor "changes" 58 | * don't capitalize first letter 59 | * no dot (.) at the end 60 | -------------------------------------------------------------------------------- /src/containers/RepositoriesContainer/RepositoriesDucks.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import ApiUtils from '../../utils/ApiUtils'; 3 | 4 | // Actions 5 | const LOAD = 'react-redux-starter-app/repositories/LOAD'; 6 | const LOAD_SUCCESS = 'react-redux-starter-app/repositories/LOAD_SUCCESS'; 7 | const LOAD_ERROR = 'react-redux-starter-app/repositories/LOAD_ERROR'; 8 | const SEARCH = 'react-redux-starter-app/search/SEARCH'; 9 | 10 | const initialState = { 11 | isLoading: false, 12 | repos: undefined, 13 | }; 14 | 15 | // Reducers 16 | const REDUCERS = { 17 | [LOAD]: state => ({ 18 | ...state, 19 | isLoading: true, 20 | }), 21 | [LOAD_SUCCESS]: (state, action) => ({ 22 | ...state, 23 | isLoading: false, 24 | repos: action.data.map(repo => ({ 25 | author: repo.owner.login, 26 | avatar: repo.owner.avatar_url, 27 | description: repo.description, 28 | id: repo.id, 29 | name: repo.name, 30 | starsCount: repo.stargazers_count, 31 | url: repo.html_url, 32 | })), 33 | }), 34 | [LOAD_ERROR]: (state, action) => ({ 35 | ...state, 36 | isLoading: false, 37 | error: action.error, 38 | }), 39 | // When SEARCH action is triggered --filter the repos by the name 40 | // that starts with the inputValue payload. 41 | [SEARCH]: (state, action) => ({ 42 | ...state, 43 | repos: state.repos.filter((repo) => _.startsWith(repo.name, action.payload)), 44 | }), 45 | }; 46 | 47 | export default function reducer(state = initialState, action = {}) { 48 | const handler = REDUCERS[action.type]; 49 | return handler ? handler(state, action) : state; 50 | } 51 | 52 | function loadSuccess(data) { 53 | return { 54 | type: LOAD_SUCCESS, 55 | data, 56 | }; 57 | } 58 | 59 | function loadError(error) { 60 | return { 61 | type: LOAD_ERROR, 62 | error, 63 | }; 64 | } 65 | 66 | // Action Creators 67 | export function load() { 68 | return (dispatch, getState) => { 69 | // Already loading? 70 | if (getState().repositories.isLoading) { 71 | return undefined; 72 | } 73 | 74 | // Notify that we're loading. 75 | dispatch({ 76 | type: LOAD, 77 | }); 78 | 79 | return ApiUtils.get('https://api.github.com/users/michael-martin/starred') 80 | .then(data => dispatch(loadSuccess(data))) 81 | .catch(err => dispatch(loadError(err))); 82 | }; 83 | } 84 | -------------------------------------------------------------------------------- /server/main.js: -------------------------------------------------------------------------------- 1 | import Koa from 'koa'; 2 | import convert from 'koa-convert'; 3 | import webpack from 'webpack'; 4 | import historyApiFallback from 'koa-connect-history-api-fallback'; 5 | import serve from 'koa-static'; 6 | import proxy from 'koa-proxy'; 7 | import _debug from 'debug'; 8 | 9 | import webpackConfig from '../build/webpack.config'; 10 | import config from '../config'; 11 | import webpackDevMiddleware from './middleware/webpack-dev'; 12 | import webpackHMRMiddleware from './middleware/webpack-hmr'; 13 | 14 | const debug = _debug('app:server'); 15 | const paths = config.utils_paths; 16 | const app = new Koa(); 17 | 18 | // Enable koa-proxy if it has been enabled in the config. 19 | if (config.proxy && config.proxy.enabled) { 20 | app.use(convert(proxy(config.proxy.options))); 21 | } 22 | 23 | // This rewrites all routes requests to the root /index.html file 24 | // (ignoring file requests). If you want to implement isomorphic 25 | // rendering, you'll want to remove this middleware. 26 | app.use(convert(historyApiFallback({ 27 | verbose: false, 28 | }))); 29 | 30 | // ------------------------------------ 31 | // Apply Webpack HMR Middleware 32 | // ------------------------------------ 33 | if (config.env === 'development') { 34 | const compiler = webpack(webpackConfig); 35 | 36 | // Enable webpack-dev and webpack-hot middleware 37 | const { publicPath } = webpackConfig.output; 38 | 39 | app.use(webpackDevMiddleware(compiler, publicPath)); 40 | app.use(webpackHMRMiddleware(compiler)); 41 | 42 | // Serve static assets from ~/src/static since Webpack is unaware of 43 | // these files. This middleware doesn't need to be enabled outside 44 | // of development since this directory will be copied into ~/dist 45 | // when the application is compiled. 46 | app.use(serve(paths.client('static'))); 47 | } else { 48 | debug( 49 | 'Server is being run outside of live development mode, meaning it will ' + 50 | 'only serve the compiled application bundle in ~/dist. Generally you ' + 51 | 'do not need an application server for this and can instead use a web ' + 52 | 'server such as nginx to serve your static files. See the "deployment" ' + 53 | 'section in the README for more information on deployment strategies.', 54 | ); 55 | 56 | // Serving ~/dist by default. Ideally these files should be served by 57 | // the web server and not the app server, but this helps to demo the 58 | // server in production. 59 | app.use(serve(paths.dist())); 60 | } 61 | 62 | export default app; 63 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import createBrowserHistory from 'history/lib/createBrowserHistory'; 4 | import { useRouterHistory } from 'react-router'; 5 | import { syncHistoryWithStore } from 'react-router-redux'; 6 | import createStore from './store/createStore'; 7 | import AppContainer from './containers/AppContainer'; 8 | import routes from './routes'; 9 | 10 | // ======================================================== 11 | // Browser History Setup 12 | // ======================================================== 13 | const browserHistory = useRouterHistory(createBrowserHistory)({ 14 | basename: __BASENAME__, 15 | }); 16 | 17 | // ======================================================== 18 | // Store and History Instantiation 19 | // ======================================================== 20 | // Create redux store and sync with react-router-redux. We have installed the 21 | // react-router-redux reducer under the routerKey "router" in src/routes/index.js, 22 | // so we need to provide a custom `selectLocationState` to inform 23 | // react-router-redux of its location. 24 | const initialState = window.___INITIAL_STATE__; // eslint-disable-line no-underscore-dangle 25 | const store = createStore(initialState, browserHistory); 26 | const history = syncHistoryWithStore(browserHistory, store, { 27 | selectLocationState: state => state.router, 28 | }); 29 | 30 | // ======================================================== 31 | // Developer Tools Setup 32 | // ======================================================== 33 | if (__DEBUG__) { 34 | if (window.devToolsExtension) { 35 | window.devToolsExtension.open(); 36 | } 37 | } 38 | 39 | // ======================================================== 40 | // Render Setup 41 | // ======================================================== 42 | const MOUNT_NODE = document.getElementById('root'); 43 | 44 | let render = (routerKey = null) => { 45 | ReactDOM.render( 46 | , 52 | MOUNT_NODE, 53 | ); 54 | }; 55 | 56 | // Enable HMR and catch runtime errors in RedBox 57 | // This code is excluded from production bundle 58 | if (__DEV__ && module.hot) { 59 | const renderApp = render; 60 | const renderError = (error) => { 61 | const RedBox = require('redbox-react').default; // eslint-disable-line global-require 62 | 63 | ReactDOM.render(, MOUNT_NODE); 64 | }; 65 | render = () => { 66 | try { 67 | renderApp(Math.random()); 68 | } catch (error) { 69 | renderError(error); 70 | } 71 | }; 72 | module.hot.accept(['./routes/index'], () => render()); 73 | } 74 | 75 | // ======================================================== 76 | // Go! 77 | // ======================================================== 78 | render(); 79 | -------------------------------------------------------------------------------- /.bootstraprc: -------------------------------------------------------------------------------- 1 | --- 2 | # Output debugging info 3 | # loglevel: debug 4 | 5 | # Major version of Bootstrap: 3 or 4 6 | bootstrapVersion: 4 7 | 8 | # If Bootstrap version 4 is used - turn on/off flexbox model 9 | useFlexbox: true 10 | 11 | # Webpack loaders, order matters 12 | styleLoaders: 13 | - style 14 | - css 15 | - postcss 16 | - sass 17 | 18 | # Extract styles to stand-alone css file 19 | # Different settings for different environments can be used, 20 | # It depends on value of NODE_ENV environment variable 21 | # 22 | # This param can also be set in webpack config: <-- HAS now been set there. 23 | # entry: 'bootstrap-loader/extractStyles' 24 | # extractStyles: false 25 | # env: 26 | # development: 27 | # extractStyles: false 28 | # production: 29 | # extractStyles: true 30 | 31 | 32 | # Customize Bootstrap variables that get imported before the original Bootstrap variables. 33 | # Thus, derived Bootstrap variables can depend on values from here. 34 | # See the Bootstrap _variables.scss file for examples of derived Bootstrap variables. 35 | # 36 | preBootstrapCustomizations: ./src/styles/_bootstrap_pre_variables.scss 37 | 38 | 39 | # This gets loaded after bootstrap/variables is loaded 40 | # Thus, you may customize Bootstrap variables 41 | # based on the values established in the Bootstrap _variables.scss file 42 | # 43 | bootstrapCustomizations: ./src/styles/_bootstrap_post_variables.scss 44 | 45 | 46 | # Import your custom styles here 47 | # Usually this endpoint-file contains list of @imports of your application styles 48 | # 49 | # appStyles: ./path/to/your/app/styles/endpoint.scss 50 | 51 | 52 | ### Bootstrap styles 53 | styles: 54 | 55 | # Mixins 56 | mixins: true 57 | 58 | # Reset and dependencies 59 | normalize: true 60 | print: true 61 | 62 | # Core CSS 63 | reboot: true 64 | type: true 65 | images: true 66 | code: false 67 | grid: true 68 | tables: false 69 | forms: true 70 | buttons: true 71 | 72 | # Components 73 | animation: false 74 | dropdown: false 75 | button-group: true 76 | input-group: true 77 | custom-forms: false 78 | nav: false 79 | navbar: false 80 | card: true 81 | breadcrumb: false 82 | pagination: false 83 | pager: false 84 | labels: false 85 | jumbotron: true 86 | alert: false 87 | progress: false 88 | media: false 89 | list-group: false 90 | responsive-embed: false 91 | close: false 92 | 93 | # Components w/ JavaScript 94 | modal: false 95 | tooltip: false 96 | popover: false 97 | carousel: false 98 | 99 | # Utility classes 100 | utilities: true 101 | utilities-background: false 102 | utilities-spacing: true 103 | utilities-responsive: true 104 | 105 | ### Bootstrap scripts 106 | scripts: 107 | alert: false 108 | button: false 109 | carousel: false 110 | collapse: false 111 | dropdown: false 112 | modal: false 113 | popover: false 114 | scrollspy: false 115 | tab: false 116 | tooltip: false 117 | util: false 118 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/DeloitteDigitalUK/react-redux-starter-app.svg?branch=master)](https://travis-ci.org/DeloitteDigitalUK/react-redux-starter-app) 2 | 3 | # React Redux Starter App 4 | 5 | This app is intended as an exemplar of how we like to build React apps. You can clone this, and start the app to see 6 | 7 | It is built off the awesome [React Redux Starter Kit](https://github.com/davezuko/react-redux-starter-kit), but unlike the starter kit, this repo is intended to be **very opinionated**. 8 | 9 | We'll explain all our choices though and if you don't like them, just swap them out for something else! 10 | 11 | ## What's Included? 12 | 13 | * React Redux, React Router (obviously) 14 | * Webpack & Babel (Un-changed from the starter kit, so all the same tasks work. See README-redux.md) 15 | * [React Storybook](https://github.com/kadirahq/react-storybook) - Separate your presentational components from your containers, then view your presentational components individually in an this cookbook-style app. 16 | * CSS Modules with SASS 17 | * Responsive grids with flex, via Bootstrap 4 - We use [Bootstrap Loader](https://github.com/shakacode/bootstrap-loader) to load only the normalize.css reset file, and the responsive grid classes. All other CSS should be written in your own CSS modules, but the grid classes are too handy not to use. 18 | 19 | ## Setup Instructions 20 | 21 | ```bash 22 | $ git clone https://github.com/DeloitteDigitalUK/react-redux-starter-app.git 23 | $ cd react-redux-starter-app 24 | $ npm install # Install project dependencies 25 | $ npm start # Compile and launch 26 | ``` 27 | 28 | Common commands: 29 | 30 | |`npm run