├── .editorconfig ├── .gitattributes ├── .github ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE.md └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── .soliumignore ├── .soliumrc.json ├── .travis.yml ├── CODE_OF_CONDUCT.md ├── Changelog.md ├── LICENSE.md ├── README.md ├── app ├── .htaccess ├── .nginx.conf ├── app.js ├── components │ ├── A │ │ ├── index.js │ │ ├── styles.css │ │ └── tests │ │ │ └── index.test.js │ ├── Button │ │ ├── index.js │ │ ├── styles.css │ │ └── tests │ │ │ └── index.test.js │ ├── Footer │ │ ├── index.js │ │ ├── messages.js │ │ ├── styles.css │ │ └── tests │ │ │ └── index.test.js │ ├── H1 │ │ ├── index.js │ │ ├── styles.css │ │ └── tests │ │ │ └── index.test.js │ ├── H2 │ │ ├── index.js │ │ ├── styles.css │ │ └── tests │ │ │ └── index.test.js │ ├── H3 │ │ ├── index.js │ │ └── tests │ │ │ └── index.test.js │ ├── Img │ │ ├── index.js │ │ └── tests │ │ │ └── index.test.js │ ├── IssueIcon │ │ ├── index.js │ │ └── tests │ │ │ └── index.test.js │ ├── List │ │ ├── index.js │ │ ├── styles.css │ │ └── tests │ │ │ └── index.test.js │ ├── ListItem │ │ ├── index.js │ │ ├── styles.css │ │ └── tests │ │ │ └── index.test.js │ ├── LoadingIndicator │ │ ├── index.js │ │ ├── styles.css │ │ └── tests │ │ │ └── index.test.js │ ├── Toggle │ │ ├── index.js │ │ ├── styles.css │ │ └── tests │ │ │ └── index.test.js │ └── ToggleOption │ │ ├── index.js │ │ └── tests │ │ └── index.test.js ├── containers │ ├── App │ │ ├── actions.js │ │ ├── banner-metal.jpg │ │ ├── constants.js │ │ ├── index.js │ │ ├── reducer.js │ │ ├── selectors.js │ │ ├── styles.css │ │ └── tests │ │ │ ├── actions.test.js │ │ │ ├── index.test.js │ │ │ ├── reducer.test.js │ │ │ └── selectors.test.js │ ├── FeaturePage │ │ ├── index.js │ │ ├── messages.js │ │ ├── styles.css │ │ └── tests │ │ │ └── index.test.js │ ├── HomePage │ │ ├── actions.js │ │ ├── constants.js │ │ ├── index.js │ │ ├── messages.js │ │ ├── reducer.js │ │ ├── sagas.js │ │ ├── selectors.js │ │ ├── styles.css │ │ └── tests │ │ │ ├── actions.test.js │ │ │ ├── index.test.js │ │ │ ├── reducer.test.js │ │ │ ├── sagas.test.js │ │ │ └── selectors.test.js │ ├── LanguageProvider │ │ ├── actions.js │ │ ├── constants.js │ │ ├── index.js │ │ ├── reducer.js │ │ ├── selectors.js │ │ └── tests │ │ │ ├── actions.test.js │ │ │ ├── index.test.js │ │ │ ├── reducer.test.js │ │ │ └── selectors.test.js │ ├── LocaleToggle │ │ ├── index.js │ │ ├── messages.js │ │ ├── styles.css │ │ └── tests │ │ │ ├── index.test.js │ │ │ └── messages.test.js │ ├── NotFoundPage │ │ ├── index.js │ │ ├── messages.js │ │ └── tests │ │ │ └── index.test.js │ └── RepoListItem │ │ ├── index.js │ │ ├── styles.css │ │ └── tests │ │ └── index.test.js ├── contracts │ ├── SimpleStore.sol │ ├── SimpleStoreFactory.sol │ ├── SimpleStoreRegistry.sol │ └── tests │ │ ├── test.SimpleStore.js │ │ ├── test.SimpleStore.sol │ │ ├── test.SimpleStoreFactory.js │ │ ├── test.SimpleStoreFactory.sol │ │ ├── test.SimpleStoreRegistry.js │ │ └── test.SimpleStoreRegistry.sol ├── dappfile ├── favicon.ico ├── i18n.js ├── index.html ├── manifest.json ├── reducers.js ├── routes.js ├── store.js ├── tests │ └── store.test.js ├── translations │ ├── de.json │ └── en.json ├── utils │ ├── asyncInjectors.js │ ├── request.js │ └── tests │ │ ├── asyncInjectors.test.js │ │ └── request.test.js └── web3.js ├── appveyor.yml ├── docs ├── README.md ├── css │ ├── README.md │ ├── css-modules.md │ ├── postcss.md │ ├── remove.md │ ├── sanitize.md │ ├── sass.md │ └── stylelint.md ├── general │ ├── README.md │ ├── commands.md │ ├── deployment.md │ ├── faq.md │ ├── files.md │ ├── gotchas.md │ ├── remove.md │ ├── server-configs.md │ ├── webstorm-debug.png │ └── webstorm-eslint.png ├── js │ ├── README.md │ ├── i18n.md │ ├── immutablejs.md │ ├── redux-saga.md │ ├── redux.md │ ├── remove.md │ ├── reselect.md │ └── routing.md └── testing │ ├── README.md │ ├── component-testing.md │ ├── remote-testing.md │ └── unit-testing.md ├── internals ├── config.js ├── ethdeploy │ ├── ethdeploy.base.js │ ├── ethdeploy.livenet.js │ ├── ethdeploy.testnet.js │ └── ethdeploy.testrpc.js ├── generators │ ├── component │ │ ├── es6.js.hbs │ │ ├── index.js │ │ ├── messages.js.hbs │ │ ├── stateless.js.hbs │ │ ├── styles.css.hbs │ │ └── test.js.hbs │ ├── container │ │ ├── actions.js.hbs │ │ ├── actions.test.js.hbs │ │ ├── constants.js.hbs │ │ ├── index.js │ │ ├── index.js.hbs │ │ ├── messages.js.hbs │ │ ├── reducer.js.hbs │ │ ├── reducer.test.js.hbs │ │ ├── sagas.js.hbs │ │ ├── sagas.test.js.hbs │ │ ├── selectors.js.hbs │ │ ├── selectors.test.js.hbs │ │ ├── styles.css.hbs │ │ └── test.js.hbs │ ├── contract │ │ ├── contract.sol.hbs │ │ ├── index.js │ │ ├── library.sol.hbs │ │ ├── test.contract.js.hbs │ │ └── test.contract.sol.hbs │ ├── index.js │ ├── language │ │ ├── add-locale-data.hbs │ │ ├── app-locale.hbs │ │ ├── format-translation-messages.hbs │ │ ├── index.js │ │ ├── intl-locale-data.hbs │ │ ├── polyfill-intl-locale.hbs │ │ ├── translation-messages.hbs │ │ └── translations-json.hbs │ ├── route │ │ ├── index.js │ │ ├── route.hbs │ │ └── routeWithReducer.hbs │ └── utils │ │ └── componentExists.js ├── scripts │ ├── analyze.js │ ├── clean.js │ ├── dependencies.js │ ├── deploy.js │ ├── extract-intl.js │ ├── generate-lightwallet.js │ ├── helpers │ │ ├── checkmark.js │ │ └── progress.js │ ├── npmcheckversion.js │ ├── pagespeed.js │ ├── setup.js │ └── solium.js ├── templates │ ├── app.js │ ├── appContainer.js │ ├── asyncInjectors.js │ ├── asyncInjectors.test.js │ ├── homePage │ │ ├── homePage.js │ │ └── messages.js │ ├── i18n.js │ ├── index.html │ ├── languageProvider │ │ ├── actions.js │ │ ├── constants.js │ │ ├── languageProvider.js │ │ ├── reducer.js │ │ └── selectors.js │ ├── notFoundPage │ │ ├── messages.js │ │ └── notFoundPage.js │ ├── reducers.js │ ├── routes.js │ ├── selectors.js │ ├── selectors.test.js │ ├── store.js │ ├── store.test.js │ ├── styles.css │ └── translations │ │ └── en.json ├── testing │ ├── dapple.js │ ├── karma.conf.js │ └── test-bundler.js └── webpack │ ├── webpack.base.babel.js │ ├── webpack.dev.babel.js │ ├── webpack.dll.babel.js │ ├── webpack.prod.babel.js │ └── webpack.test.babel.js ├── package.json └── server ├── index.js ├── logger.js └── middlewares └── frontendMiddleware.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = false 6 | indent_style = space 7 | indent_size = 2 8 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # From https://github.com/Danimoth/gitattributes/blob/master/Web.gitattributes 2 | 3 | # Handle line endings automatically for files detected as text 4 | # and leave all files detected as binary untouched. 5 | * text=auto 6 | 7 | # 8 | # The above will handle all files NOT found below 9 | # 10 | 11 | # 12 | ## These files are text and should be normalized (Convert crlf => lf) 13 | # 14 | 15 | # source code 16 | *.php text 17 | *.css text 18 | *.sass text 19 | *.scss text 20 | *.less text 21 | *.styl text 22 | *.js text eol=lf 23 | *.coffee text 24 | *.json text 25 | *.htm text 26 | *.html text 27 | *.xml text 28 | *.svg text 29 | *.txt text 30 | *.ini text 31 | *.inc text 32 | *.pl text 33 | *.rb text 34 | *.py text 35 | *.scm text 36 | *.sql text 37 | *.sh text 38 | *.bat text 39 | 40 | # templates 41 | *.ejs text 42 | *.hbt text 43 | *.jade text 44 | *.haml text 45 | *.hbs text 46 | *.dot text 47 | *.tmpl text 48 | *.phtml text 49 | 50 | # server config 51 | .htaccess text 52 | 53 | # git config 54 | .gitattributes text 55 | .gitignore text 56 | .gitconfig text 57 | 58 | # code analysis config 59 | .jshintrc text 60 | .jscsrc text 61 | .jshintignore text 62 | .csslintrc text 63 | 64 | # misc config 65 | *.yaml text 66 | *.yml text 67 | .editorconfig text 68 | 69 | # build config 70 | *.npmignore text 71 | *.bowerrc text 72 | 73 | # Heroku 74 | Procfile text 75 | .slugignore text 76 | 77 | # Documentation 78 | *.md text 79 | LICENSE text 80 | AUTHORS text 81 | 82 | 83 | # 84 | ## These files are binary and should be left untouched 85 | # 86 | 87 | # (binary is a macro for -text -diff) 88 | *.png binary 89 | *.jpg binary 90 | *.jpeg binary 91 | *.gif binary 92 | *.ico binary 93 | *.mov binary 94 | *.mp4 binary 95 | *.mp3 binary 96 | *.flv binary 97 | *.fla binary 98 | *.swf binary 99 | *.gz binary 100 | *.zip binary 101 | *.7z binary 102 | *.ttf binary 103 | *.eot binary 104 | *.woff binary 105 | *.pyc binary 106 | *.pdf binary 107 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | # React Boilerplate 2 | 3 | Before opening a new issue, please take a moment to review our [**community guidelines**](https://github.com/mxstbr/react-boilerplate/blob/master/.github/CONTRIBUTING.md) to make the contribution process easy and effective for everyone involved. 4 | 5 | Please direct redux-saga related questions to stack overflow: 6 | http://stackoverflow.com/questions/tagged/redux-saga 7 | 8 | For questions related to the boilerplate itself, you can also find answers on our gitter chat: 9 | https://gitter.im/mxstbr/react-boilerplate 10 | 11 | **Before opening a new issue, you may find an answer in already closed issues**: 12 | https://github.com/mxstbr/react-boilerplate/issues?q=is%3Aissue+is%3Aclosed 13 | 14 | ## Issue Type 15 | 16 | - [ ] Bug (https://github.com/mxstbr/react-boilerplate/blob/master/.github/CONTRIBUTING.md#bug-reports) 17 | - [ ] Feature (https://github.com/mxstbr/react-boilerplate/blob/master/.github/CONTRIBUTING.md#feature-requests) 18 | 19 | ## Description 20 | 21 | (Add images if possible) 22 | 23 | ## Steps to reproduce 24 | 25 | (Add link to a demo on https://jsfiddle.net or similar if possible) 26 | 27 | # Versions 28 | 29 | - Node/NPM: 30 | - Browser: 31 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## React Boilerplate 2 | 3 | Thank you for contributing! Please take a moment to review our [**contributing guidelines**](https://github.com/mxstbr/react-boilerplate/blob/master/.github/CONTRIBUTING.md) 4 | to make the process easy and effective for everyone involved. 5 | 6 | **Please open an issue** before embarking on any significant pull request, especially those that 7 | add a new library or change existing tests, otherwise you risk spending a lot of time working 8 | on something that might not end up being merged into the project. 9 | 10 | Before opening a pull request, please ensure: 11 | 12 | - [ ] You have followed our [**contributing guidelines**](https://github.com/mxstbr/react-boilerplate/blob/master/.github/CONTRIBUTING.md) 13 | - [ ] Pull request has tests (we are going for 100% coverage!) 14 | - [ ] Code is well-commented, linted and follows project conventions 15 | - [ ] Documentation is updated (if necessary) 16 | - [ ] Internal code generators and templates are updated (if necessary) 17 | - [ ] Description explains the issue/use-case resolved and auto-closes related issues 18 | 19 | Be kind to code reviewers, please try to keep pull requests as small and focused as possible :) 20 | 21 | **IMPORTANT**: By submitting a patch, you agree to allow the project 22 | owners to license your work under the terms of the [MIT License](https://github.com/mxstbr/react-boilerplate/blob/master/LICENSE.md). 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Don't check auto-generated stuff into git 2 | coverage 3 | build 4 | node_modules 5 | stats.json 6 | 7 | # Cruft 8 | .DS_Store 9 | npm-debug.log 10 | .idea 11 | ethereum-wallet.json 12 | -------------------------------------------------------------------------------- /.soliumignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /.soliumrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "custom-rules-filename": null, 3 | "rules": { 4 | "imports-on-top": true, 5 | "variable-declarations": true, 6 | "array-declarations": true, 7 | "operator-whitespace": true, 8 | "lbrace": false, 9 | "mixedcase": true, 10 | "camelcase": true, 11 | "uppercase": true, 12 | "no-with": true, 13 | "no-empty-blocks": true, 14 | "no-unused-vars": false, 15 | "double-quotes": true, 16 | "blank-lines": false, 17 | "indentation": false, 18 | "whitespace": true 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | sudo: true 3 | dist: trusty 4 | node_js: 5 | - "5.0" 6 | script: npm run build 7 | before_install: 8 | - export CHROME_BIN=/usr/bin/google-chrome 9 | - export DISPLAY=:99.0 10 | - sudo apt-get update 11 | - sudo apt-get install -y libappindicator1 fonts-liberation 12 | - wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb 13 | - sudo dpkg -i google-chrome*.deb 14 | - sh -e /etc/init.d/xvfb start 15 | notifications: 16 | email: 17 | on_failure: change 18 | after_success: 'npm run coveralls' 19 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Code of Conduct 2 | 3 | As contributors and maintainers of this project, and in the interest of 4 | fostering an open and welcoming community, we pledge to respect all people who 5 | contribute through reporting issues, posting feature requests, updating 6 | documentation, submitting pull requests or patches, and other activities. 7 | 8 | We are committed to making participation in this project a harassment-free 9 | experience for everyone, regardless of level of experience, gender, gender 10 | identity and expression, sexual orientation, disability, personal appearance, 11 | body size, race, ethnicity, age, religion, or nationality. 12 | 13 | Examples of unacceptable behavior by participants include: 14 | 15 | * The use of sexualized language or imagery 16 | * Personal attacks 17 | * Trolling or insulting/derogatory comments 18 | * Public or private harassment 19 | * Publishing other's private information, such as physical or electronic 20 | addresses, without explicit permission 21 | * Other unethical or unprofessional conduct 22 | 23 | Project maintainers have the right and responsibility to remove, edit, or 24 | reject comments, commits, code, wiki edits, issues, and other contributions 25 | that are not aligned to this Code of Conduct, or to ban temporarily or 26 | permanently any contributor for other behaviors that they deem inappropriate, 27 | threatening, offensive, or harmful. 28 | 29 | By adopting this Code of Conduct, project maintainers commit themselves to 30 | fairly and consistently applying these principles to every aspect of managing 31 | this project. Project maintainers who do not follow or enforce the Code of 32 | Conduct may be permanently removed from the project team. 33 | 34 | This Code of Conduct applies both within project spaces and in public spaces 35 | when an individual is representing the project or its community. 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 38 | reported by contacting the project maintainer at contact@mxstbr.com. All 39 | complaints will be reviewed and investigated and will result in a response that 40 | is deemed necessary and appropriate to the circumstances. Maintainers are 41 | obligated to maintain confidentiality with regard to the reporter of an 42 | incident. 43 | 44 | 45 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 46 | version 1.3.0, available at 47 | [http://contributor-covenant.org/version/1/3/0/][version] 48 | 49 | [homepage]: http://contributor-covenant.org 50 | [version]: http://contributor-covenant.org/version/1/3/0/ 51 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Maximilian Stoiber 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. -------------------------------------------------------------------------------- /app/.htaccess: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ####################################################################### 5 | # GENERAL # 6 | ####################################################################### 7 | 8 | # Make apache follow sym links to files 9 | Options +FollowSymLinks 10 | # If somebody opens a folder, hide all files from the resulting folder list 11 | IndexIgnore */* 12 | 13 | 14 | ####################################################################### 15 | # REWRITING # 16 | ####################################################################### 17 | 18 | # Enable rewriting 19 | RewriteEngine On 20 | 21 | # If its not HTTPS 22 | RewriteCond %{HTTPS} off 23 | 24 | # Comment out the RewriteCond above, and uncomment the RewriteCond below if you're using a load balancer (e.g. CloudFlare) for SSL 25 | # RewriteCond %{HTTP:X-Forwarded-Proto} !https 26 | 27 | # Redirect to the same URL with https://, ignoring all further rules if this one is in effect 28 | RewriteRule ^(.*) https://%{HTTP_HOST}/$1 [R,L] 29 | 30 | # If we get to here, it means we are on https:// 31 | 32 | # If the file with the specified name in the browser doesn't exist 33 | RewriteCond %{REQUEST_FILENAME} !-f 34 | 35 | # and the directory with the specified name in the browser doesn't exist 36 | RewriteCond %{REQUEST_FILENAME} !-d 37 | 38 | # and we are not opening the root already (otherwise we get a redirect loop) 39 | RewriteCond %{REQUEST_FILENAME} !\/$ 40 | 41 | # Rewrite all requests to the root 42 | RewriteRule ^(.*) / 43 | 44 | 45 | -------------------------------------------------------------------------------- /app/.nginx.conf: -------------------------------------------------------------------------------- 1 | ## 2 | # Put this file in /etc/nginx/conf.d folder and make sure 3 | # you have line 'include /etc/nginx/conf.d/*.conf;' 4 | # in your main nginx configuration file 5 | ## 6 | 7 | ## 8 | # Redirect to the same URL with https:// 9 | ## 10 | 11 | server { 12 | 13 | listen 80; 14 | 15 | # Type your domain name below 16 | server_name example.com; 17 | 18 | return 301 https://$server_name$request_uri; 19 | 20 | } 21 | 22 | ## 23 | # HTTPS configurations 24 | ## 25 | 26 | server { 27 | 28 | listen 443; 29 | 30 | # Type your domain name below 31 | server_name example.com; 32 | 33 | ssl on; 34 | ssl_certificate /path/to/certificate.crt; 35 | ssl_certificate_key /path/to/server.key; 36 | 37 | # Use only TSL protocols for more secure 38 | ssl_protocols TLSv1 TLSv1.1 TLSv1.2; 39 | 40 | # Always serve index.html for any request 41 | location / { 42 | # Set path 43 | root /var/www/; 44 | try_files $uri /index.html; 45 | } 46 | 47 | ## 48 | # If you want to use Node/Rails/etc. API server 49 | # on the same port (443) config Nginx as a reverse proxy. 50 | # For security reasons use a firewall like ufw in Ubuntu 51 | # and deny port 3000/tcp. 52 | ## 53 | 54 | # location /api/ { 55 | # 56 | # proxy_pass http://localhost:3000; 57 | # proxy_http_version 1.1; 58 | # proxy_set_header X-Forwarded-Proto https; 59 | # proxy_set_header Upgrade $http_upgrade; 60 | # proxy_set_header Connection 'upgrade'; 61 | # proxy_set_header Host $host; 62 | # proxy_cache_bypass $http_upgrade; 63 | # 64 | # } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /app/components/A/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * A link to a certain page, an anchor tag 3 | */ 4 | 5 | import React, { PropTypes } from 'react'; 6 | 7 | import styles from './styles.css'; 8 | 9 | function A(props) { 10 | return ( 11 | 17 | ); 18 | } 19 | 20 | A.propTypes = { 21 | className: PropTypes.string, 22 | href: PropTypes.string.isRequired, 23 | target: PropTypes.string, 24 | children: PropTypes.node.isRequired, 25 | }; 26 | 27 | export default A; 28 | -------------------------------------------------------------------------------- /app/components/A/styles.css: -------------------------------------------------------------------------------- 1 | .link { 2 | color: #41ADDD; 3 | } 4 | 5 | .link:hover { 6 | color: #6CC0E5; 7 | } 8 | -------------------------------------------------------------------------------- /app/components/A/tests/index.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Testing our link component 3 | */ 4 | 5 | import A from '../index'; 6 | 7 | import expect from 'expect'; 8 | import { shallow } from 'enzyme'; 9 | import React from 'react'; 10 | 11 | describe('', () => { 12 | it('should render its children', () => { 13 | const children = (

Test

); 14 | const renderedComponent = shallow( 15 |
16 | {children} 17 | 18 | ); 19 | expect(renderedComponent.contains(children)).toEqual(true); 20 | }); 21 | 22 | it('should adopt the className', () => { 23 | const renderedComponent = shallow(); 24 | expect(renderedComponent.find('a').hasClass('test')).toEqual(true); 25 | }); 26 | 27 | it('should adopt the href', () => { 28 | const renderedComponent = shallow(); 29 | expect(renderedComponent.prop('href')).toEqual('mxstbr.com'); 30 | }); 31 | 32 | it('should adopt the target', () => { 33 | const renderedComponent = shallow(); 34 | expect(renderedComponent.prop('target')).toEqual('_blank'); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /app/components/Button/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Button.react.js 4 | * 5 | * A common button, if you pass it a prop "route" it'll render a link to a react-router route 6 | * otherwise it'll render a link with an onclick 7 | */ 8 | 9 | import React, { PropTypes, Children } from 'react'; 10 | 11 | import styles from './styles.css'; 12 | 13 | function Button(props) { 14 | const className = props.className ? props.className : styles.button; 15 | 16 | // Render an anchor tag 17 | let button = ( 18 | 19 | {Children.toArray(props.children)} 20 | 21 | ); 22 | 23 | // If the Button has a handleRoute prop, we want to render a button 24 | if (props.handleRoute) { 25 | button = ( 26 | 29 | ); 30 | } 31 | 32 | return ( 33 |
34 | {button} 35 |
36 | ); 37 | } 38 | 39 | Button.propTypes = { 40 | className: PropTypes.string, 41 | handleRoute: PropTypes.func, 42 | href: PropTypes.string, 43 | onClick: PropTypes.func, 44 | children: PropTypes.node.isRequired, 45 | }; 46 | 47 | export default Button; 48 | -------------------------------------------------------------------------------- /app/components/Button/styles.css: -------------------------------------------------------------------------------- 1 | .buttonWrapper { 2 | width: 100%; 3 | text-align: center; 4 | margin: 4em 0; 5 | } 6 | 7 | .button { 8 | display: inline-block; 9 | box-sizing: border-box; 10 | padding: 0.25em 2em; 11 | margin: 0; 12 | border: 0; 13 | text-decoration: none; 14 | border-radius: 4px; 15 | -webkit-font-smoothing: antialiased; 16 | -webkit-touch-callout: none; 17 | user-select: none; 18 | cursor: pointer; 19 | outline: 0; 20 | font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; 21 | font-weight: bold; 22 | font-size: 16px; 23 | color: #FFF; 24 | border: 2px solid #41ADDD; 25 | color: #41ADDD; 26 | } 27 | 28 | .button:active { 29 | background: #41ADDD; 30 | color: #FFF; 31 | } 32 | -------------------------------------------------------------------------------- /app/components/Button/tests/index.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Testing our Button component 3 | */ 4 | 5 | import Button from '../index'; 6 | 7 | import expect from 'expect'; 8 | import { shallow } from 'enzyme'; 9 | import React from 'react'; 10 | 11 | describe(' 18 | ); 19 | expect(renderedComponent.contains(children)).toEqual(true); 20 | }); 21 | 22 | it('should adopt the className', () => { 23 | const renderedComponent = shallow( 96 | 97 | ); 98 | } 99 | } 100 | FeaturePage.propTypes = { 101 | changeRoute: React.PropTypes.func, 102 | }; 103 | 104 | function mapDispatchToProps(dispatch) { 105 | return { 106 | changeRoute: (url) => dispatch(push(url)), 107 | }; 108 | } 109 | 110 | export default connect(null, mapDispatchToProps)(FeaturePage); 111 | -------------------------------------------------------------------------------- /app/containers/FeaturePage/styles.css: -------------------------------------------------------------------------------- 1 | .list { 2 | font-family: Georgia, Times, 'Times New Roman', serif; 3 | padding-left: 1.75em; 4 | } 5 | 6 | .listItem { 7 | margin: 1em 0; 8 | } 9 | 10 | .listItemTitle { 11 | font-weight: bold; 12 | } 13 | -------------------------------------------------------------------------------- /app/containers/FeaturePage/tests/index.test.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect'; 2 | import { shallow, mount } from 'enzyme'; 3 | import React from 'react'; 4 | 5 | import { IntlProvider, FormattedMessage } from 'react-intl'; 6 | import messages from '../messages'; 7 | import { FeaturePage } from '../index'; 8 | import H1 from 'components/H1'; 9 | 10 | describe('', () => { 11 | it('should render its heading', () => { 12 | const renderedComponent = shallow( 13 | 14 | ); 15 | expect(renderedComponent.contains( 16 |

17 | 18 |

19 | )).toEqual(true); 20 | }); 21 | 22 | it('should link to "/"', () => { 23 | const openRouteSpy = expect.createSpy(); 24 | 25 | // Spy on the openRoute method of the FeaturePage 26 | const openRoute = (dest) => { 27 | if (dest === '/') { 28 | openRouteSpy(); 29 | } 30 | }; 31 | 32 | const renderedComponent = mount( 33 | 34 | 35 | 36 | ); 37 | const button = renderedComponent.find('button'); 38 | button.simulate('click'); 39 | expect(openRouteSpy).toHaveBeenCalled(); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /app/containers/HomePage/actions.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Home Actions 3 | * 4 | * Actions change things in your application 5 | * Since this boilerplate uses a uni-directional data flow, specifically redux, 6 | * we have these actions which are the only way your application interacts with 7 | * your appliction state. This guarantees that your state is up to date and nobody 8 | * messes it up weirdly somewhere. 9 | * 10 | * To add a new Action: 11 | * 1) Import your constant 12 | * 2) Add a function like this: 13 | * export function yourAction(var) { 14 | * return { type: YOUR_ACTION_CONSTANT, var: var } 15 | * } 16 | */ 17 | 18 | import { 19 | CHANGE_USERNAME, 20 | } from './constants'; 21 | 22 | /** 23 | * Changes the input field of the form 24 | * 25 | * @param {name} name The new text of the input field 26 | * 27 | * @return {object} An action object with a type of CHANGE_USERNAME 28 | */ 29 | export function changeUsername(name) { 30 | return { 31 | type: CHANGE_USERNAME, 32 | name, 33 | }; 34 | } 35 | -------------------------------------------------------------------------------- /app/containers/HomePage/constants.js: -------------------------------------------------------------------------------- 1 | /* 2 | * HomeConstants 3 | * Each action has a corresponding type, which the reducer knows and picks up on. 4 | * To avoid weird typos between the reducer and the actions, we save them as 5 | * constants here. We prefix them with 'yourproject/YourComponent' so we avoid 6 | * reducers accidentally picking up actions they shouldn't. 7 | * 8 | * Follow this format: 9 | * export const YOUR_ACTION_CONSTANT = 'yourproject/YourContainer/YOUR_ACTION_CONSTANT'; 10 | */ 11 | 12 | export const CHANGE_USERNAME = 'boilerplate/Home/CHANGE_USERNAME'; 13 | -------------------------------------------------------------------------------- /app/containers/HomePage/messages.js: -------------------------------------------------------------------------------- 1 | /* 2 | * HomePage Messages 3 | * 4 | * This contains all the text for the HomePage component. 5 | */ 6 | import { defineMessages } from 'react-intl'; 7 | 8 | export default defineMessages({ 9 | startProjectHeader: { 10 | id: 'boilerplate.containers.HomePage.start_project.header', 11 | defaultMessage: 'Start your next react project in seconds', 12 | }, 13 | startProjectMessage: { 14 | id: 'boilerplate.containers.HomePage.start_project.message', 15 | defaultMessage: 'A highly scalable, offline-first foundation with the best DX and a focus on performance and best practices', 16 | }, 17 | trymeHeader: { 18 | id: 'boilerplate.containers.HomePage.tryme.header', 19 | defaultMessage: 'Try me!', 20 | }, 21 | trymeMessage: { 22 | id: 'boilerplate.containers.HomePage.tryme.message', 23 | defaultMessage: 'Show Github repositories by', 24 | }, 25 | trymeAtPrefix: { 26 | id: 'boilerplate.containers.HomePage.tryme.atPrefix', 27 | defaultMessage: '@', 28 | }, 29 | featuresButton: { 30 | id: 'boilerplate.containers.HomePage.features.Button', 31 | defaultMessage: 'Features', 32 | }, 33 | }); 34 | -------------------------------------------------------------------------------- /app/containers/HomePage/reducer.js: -------------------------------------------------------------------------------- 1 | /* 2 | * HomeReducer 3 | * 4 | * The reducer takes care of our data. Using actions, we can change our 5 | * application state. 6 | * To add a new action, add it to the switch statement in the reducer function 7 | * 8 | * Example: 9 | * case YOUR_ACTION_CONSTANT: 10 | * return state.set('yourStateVariable', true); 11 | */ 12 | 13 | import { 14 | CHANGE_USERNAME, 15 | } from './constants'; 16 | import { fromJS } from 'immutable'; 17 | 18 | // The initial state of the App 19 | const initialState = fromJS({ 20 | username: '', 21 | }); 22 | 23 | function homeReducer(state = initialState, action) { 24 | switch (action.type) { 25 | case CHANGE_USERNAME: 26 | 27 | // Delete prefixed '@' from the github username 28 | return state 29 | .set('username', action.name.replace(/@/gi, '')); 30 | default: 31 | return state; 32 | } 33 | } 34 | 35 | export default homeReducer; 36 | -------------------------------------------------------------------------------- /app/containers/HomePage/sagas.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Gets the repositories of the user from Github 3 | */ 4 | 5 | import { take, call, put, select, fork, cancel } from 'redux-saga/effects'; 6 | import { LOCATION_CHANGE } from 'react-router-redux'; 7 | import { LOAD_REPOS } from 'containers/App/constants'; 8 | import { reposLoaded, repoLoadingError } from 'containers/App/actions'; 9 | 10 | import request from 'utils/request'; 11 | import { selectUsername } from 'containers/HomePage/selectors'; 12 | 13 | /** 14 | * Github repos request/response handler 15 | */ 16 | export function* getRepos() { 17 | // Select username from store 18 | const username = yield select(selectUsername()); 19 | const requestURL = `https://api.github.com/users/${username}/repos?type=all&sort=updated`; 20 | 21 | // Call our request helper (see 'utils/request') 22 | const repos = yield call(request, requestURL); 23 | 24 | if (!repos.err) { 25 | yield put(reposLoaded(repos.data, username)); 26 | } else { 27 | yield put(repoLoadingError(repos.err)); 28 | } 29 | } 30 | 31 | /** 32 | * Watches for LOAD_REPOS action and calls handler 33 | */ 34 | export function* getReposWatcher() { 35 | while (yield take(LOAD_REPOS)) { 36 | yield call(getRepos); 37 | } 38 | } 39 | 40 | /** 41 | * Root saga manages watcher lifecycle 42 | */ 43 | export function* githubData() { 44 | // Fork watcher so we can continue execution 45 | const watcher = yield fork(getReposWatcher); 46 | 47 | // Suspend execution until location changes 48 | yield take(LOCATION_CHANGE); 49 | yield cancel(watcher); 50 | } 51 | 52 | // Bootstrap sagas 53 | export default [ 54 | githubData, 55 | ]; 56 | -------------------------------------------------------------------------------- /app/containers/HomePage/selectors.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Homepage selectors 3 | */ 4 | 5 | import { createSelector } from 'reselect'; 6 | 7 | const selectHome = () => (state) => state.get('home'); 8 | 9 | const selectUsername = () => createSelector( 10 | selectHome(), 11 | (homeState) => homeState.get('username') 12 | ); 13 | 14 | export { 15 | selectHome, 16 | selectUsername, 17 | }; 18 | -------------------------------------------------------------------------------- /app/containers/HomePage/styles.css: -------------------------------------------------------------------------------- 1 | .textSection { 2 | margin: 3em auto; 3 | } 4 | 5 | .textSection:first-child { 6 | margin-top: 0; 7 | } 8 | 9 | .centered { 10 | text-align: center; 11 | } 12 | 13 | p, 14 | label { 15 | font-family: Georgia, Times, 'Times New Roman', serif; 16 | line-height: 1.5em; 17 | } 18 | 19 | .link { 20 | text-decoration: none; 21 | } 22 | 23 | .usernameForm { 24 | margin-bottom: 1em; 25 | } 26 | 27 | .input { 28 | outline: none; 29 | border-bottom: 1px dotted #999; 30 | } 31 | 32 | .atPrefix { 33 | color: black; 34 | margin-left: 0.4em; 35 | } 36 | -------------------------------------------------------------------------------- /app/containers/HomePage/tests/actions.test.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect'; 2 | 3 | import { 4 | CHANGE_USERNAME, 5 | } from '../constants'; 6 | 7 | import { 8 | changeUsername, 9 | } from '../actions'; 10 | 11 | describe('Home Actions', () => { 12 | describe('changeUsername', () => { 13 | it('should return the correct type and the passed name', () => { 14 | const fixture = 'Max'; 15 | const expectedResult = { 16 | type: CHANGE_USERNAME, 17 | name: fixture, 18 | }; 19 | 20 | expect(changeUsername(fixture)).toEqual(expectedResult); 21 | }); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /app/containers/HomePage/tests/index.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Test the HomePage 3 | */ 4 | 5 | import expect from 'expect'; 6 | import { shallow, mount } from 'enzyme'; 7 | import React from 'react'; 8 | 9 | import { IntlProvider } from 'react-intl'; 10 | import { HomePage } from '../index'; 11 | import RepoListItem from 'containers/RepoListItem'; 12 | import List from 'components/List'; 13 | import LoadingIndicator from 'components/LoadingIndicator'; 14 | 15 | describe('', () => { 16 | it('should render the loading indicator when its loading', () => { 17 | const renderedComponent = shallow( 18 | 19 | ); 20 | expect(renderedComponent.contains()).toEqual(true); 21 | }); 22 | 23 | it('should render an error if loading failed', () => { 24 | const renderedComponent = mount( 25 | 26 | 30 | 31 | ); 32 | expect( 33 | renderedComponent 34 | .text() 35 | .indexOf('Something went wrong, please try again!') 36 | ).toBeGreaterThan(-1); 37 | }); 38 | 39 | it('should render the repositories if loading was successful', () => { 40 | const repos = [{ 41 | owner: { 42 | login: 'mxstbr', 43 | }, 44 | html_url: 'https://github.com/mxstbr/react-boilerplate', 45 | name: 'react-boilerplate', 46 | open_issues_count: 20, 47 | full_name: 'mxstbr/react-boilerplate', 48 | }]; 49 | const renderedComponent = shallow( 50 | 54 | ); 55 | 56 | expect(renderedComponent.contains()).toEqual(true); 57 | }); 58 | 59 | it('should link to /features', () => { 60 | const openRouteSpy = expect.createSpy(); 61 | 62 | // Spy on the openRoute method of the HomePage 63 | const openRoute = (dest) => { 64 | if (dest === '/features') { 65 | openRouteSpy(); 66 | } 67 | }; 68 | 69 | const renderedComponent = mount( 70 | 71 | 72 | 73 | ); 74 | const button = renderedComponent.find('button'); 75 | button.simulate('click'); 76 | expect(openRouteSpy).toHaveBeenCalled(); 77 | }); 78 | }); 79 | -------------------------------------------------------------------------------- /app/containers/HomePage/tests/reducer.test.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect'; 2 | import homeReducer from '../reducer'; 3 | import { 4 | changeUsername, 5 | } from '../actions'; 6 | import { fromJS } from 'immutable'; 7 | 8 | describe('homeReducer', () => { 9 | let state; 10 | beforeEach(() => { 11 | state = fromJS({ 12 | username: '', 13 | }); 14 | }); 15 | 16 | it('should return the initial state', () => { 17 | const expectedResult = state; 18 | expect(homeReducer(undefined, {})).toEqual(expectedResult); 19 | }); 20 | 21 | it('should handle the changeUsername action correctly', () => { 22 | const fixture = 'mxstbr'; 23 | const expectedResult = state.set('username', fixture); 24 | 25 | expect(homeReducer(state, changeUsername(fixture))).toEqual(expectedResult); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /app/containers/HomePage/tests/selectors.test.js: -------------------------------------------------------------------------------- 1 | import { fromJS } from 'immutable'; 2 | import expect from 'expect'; 3 | 4 | import { 5 | selectHome, 6 | selectUsername, 7 | } from '../selectors'; 8 | 9 | describe('selectHome', () => { 10 | const homeSelector = selectHome(); 11 | it('should select the home state', () => { 12 | const homeState = fromJS({ 13 | userData: {}, 14 | }); 15 | const mockedState = fromJS({ 16 | home: homeState, 17 | }); 18 | expect(homeSelector(mockedState)).toEqual(homeState); 19 | }); 20 | }); 21 | 22 | describe('selectUsername', () => { 23 | const usernameSelector = selectUsername(); 24 | it('should select the username', () => { 25 | const username = 'mxstbr'; 26 | const mockedState = fromJS({ 27 | home: { 28 | username, 29 | }, 30 | }); 31 | expect(usernameSelector(mockedState)).toEqual(username); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /app/containers/LanguageProvider/actions.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * LanguageProvider actions 4 | * 5 | */ 6 | 7 | import { 8 | CHANGE_LOCALE, 9 | } from './constants'; 10 | 11 | export function changeLocale(languageLocale) { 12 | return { 13 | type: CHANGE_LOCALE, 14 | locale: languageLocale, 15 | }; 16 | } 17 | -------------------------------------------------------------------------------- /app/containers/LanguageProvider/constants.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * LanguageProvider constants 4 | * 5 | */ 6 | 7 | export const CHANGE_LOCALE = 'app/LanguageToggle/CHANGE_LOCALE'; 8 | -------------------------------------------------------------------------------- /app/containers/LanguageProvider/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * LanguageProvider 4 | * 5 | * this component connects the redux state language locale to the 6 | * IntlProvider component and i18n messages (loaded from `app/translations`) 7 | */ 8 | 9 | import React from 'react'; 10 | import { connect } from 'react-redux'; 11 | import { createSelector } from 'reselect'; 12 | import { IntlProvider } from 'react-intl'; 13 | import { selectLocale } from './selectors'; 14 | 15 | export class LanguageProvider extends React.Component { // eslint-disable-line react/prefer-stateless-function 16 | render() { 17 | return ( 18 | 19 | {React.Children.only(this.props.children)} 20 | 21 | ); 22 | } 23 | } 24 | 25 | LanguageProvider.propTypes = { 26 | locale: React.PropTypes.string, 27 | messages: React.PropTypes.object, 28 | children: React.PropTypes.element.isRequired, 29 | }; 30 | 31 | const mapStateToProps = createSelector( 32 | selectLocale(), 33 | (locale) => ({ locale }) 34 | ); 35 | 36 | export default connect(mapStateToProps)(LanguageProvider); 37 | -------------------------------------------------------------------------------- /app/containers/LanguageProvider/reducer.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * LanguageProvider reducer 4 | * 5 | */ 6 | 7 | import { fromJS } from 'immutable'; 8 | import { 9 | CHANGE_LOCALE, 10 | } from './constants'; 11 | 12 | const initialState = fromJS({ 13 | locale: 'en', 14 | }); 15 | 16 | function languageProviderReducer(state = initialState, action) { 17 | switch (action.type) { 18 | case CHANGE_LOCALE: 19 | return state 20 | .set('locale', action.locale); 21 | default: 22 | return state; 23 | } 24 | } 25 | 26 | export default languageProviderReducer; 27 | -------------------------------------------------------------------------------- /app/containers/LanguageProvider/selectors.js: -------------------------------------------------------------------------------- 1 | import { createSelector } from 'reselect'; 2 | 3 | /** 4 | * Direct selector to the languageToggle state domain 5 | */ 6 | const selectLanguage = () => state => state.get('language'); 7 | 8 | /** 9 | * Select the language locale 10 | */ 11 | 12 | const selectLocale = () => createSelector( 13 | selectLanguage(), 14 | (languageState) => languageState.get('locale') 15 | ); 16 | 17 | export { 18 | selectLanguage, 19 | selectLocale, 20 | }; 21 | -------------------------------------------------------------------------------- /app/containers/LanguageProvider/tests/actions.test.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect'; 2 | import { 3 | changeLocale, 4 | } from '../actions'; 5 | import { 6 | CHANGE_LOCALE, 7 | } from '../constants'; 8 | 9 | describe('LanguageProvider actions', () => { 10 | describe('Change Local Action', () => { 11 | it('has a type of CHANGE_LOCALE', () => { 12 | const expected = { 13 | type: CHANGE_LOCALE, 14 | locale: 'de', 15 | }; 16 | expect(changeLocale('de')).toEqual(expected); 17 | }); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /app/containers/LanguageProvider/tests/index.test.js: -------------------------------------------------------------------------------- 1 | import LanguageProvider from '../index'; 2 | 3 | import expect from 'expect'; 4 | import { shallow } from 'enzyme'; 5 | import { FormattedMessage, defineMessages } from 'react-intl'; 6 | import configureStore from '../../../store'; 7 | import React from 'react'; 8 | import { Provider } from 'react-redux'; 9 | import { browserHistory } from 'react-router'; 10 | import { translatedMessages } from '../../../i18n'; 11 | 12 | describe('', () => { 13 | let store; 14 | 15 | before(() => { 16 | store = configureStore({}, browserHistory); 17 | }); 18 | 19 | it('should render the default language messages', () => { 20 | const messages = defineMessages({ 21 | someMessage: { 22 | id: 'some.id', 23 | defaultMessage: 'This is some default message', 24 | }, 25 | }); 26 | const renderedComponent = shallow( 27 | 28 | 29 | 30 | 31 | 32 | ); 33 | expect(renderedComponent.contains()).toEqual(true); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /app/containers/LanguageProvider/tests/reducer.test.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect'; 2 | import languageProviderReducer from '../reducer'; 3 | import { fromJS } from 'immutable'; 4 | 5 | describe('languageProviderReducer', () => { 6 | it('returns the initial state', () => { 7 | expect(languageProviderReducer(undefined, {})).toEqual(fromJS({ 8 | locale: 'en', 9 | })); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /app/containers/LanguageProvider/tests/selectors.test.js: -------------------------------------------------------------------------------- 1 | import { 2 | selectLanguage, 3 | } from '../selectors'; 4 | import { fromJS } from 'immutable'; 5 | import expect from 'expect'; 6 | 7 | describe('selectLanguage', () => { 8 | const globalSelector = selectLanguage(); 9 | it('should select the global state', () => { 10 | const globalState = fromJS({}); 11 | const mockedState = fromJS({ 12 | language: globalState, 13 | }); 14 | expect(globalSelector(mockedState)).toEqual(globalState); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /app/containers/LocaleToggle/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * LanguageToggle 4 | * 5 | */ 6 | 7 | import React from 'react'; 8 | import { connect } from 'react-redux'; 9 | import { selectLocale } from '../LanguageProvider/selectors'; 10 | import { changeLocale } from '../LanguageProvider/actions'; 11 | import { appLocales } from '../../i18n'; 12 | import { createSelector } from 'reselect'; 13 | import styles from './styles.css'; 14 | import messages from './messages'; 15 | import Toggle from 'components/Toggle'; 16 | 17 | export class LocaleToggle extends React.Component { // eslint-disable-line 18 | render() { 19 | return ( 20 |
21 | 22 |
23 | ); 24 | } 25 | } 26 | 27 | LocaleToggle.propTypes = { 28 | onLocaleToggle: React.PropTypes.func, 29 | }; 30 | 31 | const mapStateToProps = createSelector( 32 | selectLocale(), 33 | (locale) => ({ locale }) 34 | ); 35 | 36 | function mapDispatchToProps(dispatch) { 37 | return { 38 | onLocaleToggle: (evt) => dispatch(changeLocale(evt.target.value)), 39 | dispatch, 40 | }; 41 | } 42 | 43 | export default connect(mapStateToProps, mapDispatchToProps)(LocaleToggle); 44 | -------------------------------------------------------------------------------- /app/containers/LocaleToggle/messages.js: -------------------------------------------------------------------------------- 1 | /* 2 | * LocaleToggle Messages 3 | * 4 | * This contains all the text for the LanguageToggle component. 5 | */ 6 | import { defineMessages } from 'react-intl'; 7 | import { appLocales } from '../../i18n'; 8 | 9 | export function getLocaleMessages(locales) { 10 | return locales.reduce((messages, locale) => 11 | Object.assign(messages, { 12 | [locale]: { 13 | id: `app.components.LocaleToggle.${locale}`, 14 | defaultMessage: `${locale}`, 15 | }, 16 | }), {}); 17 | } 18 | 19 | export default defineMessages( 20 | getLocaleMessages(appLocales) 21 | ); 22 | -------------------------------------------------------------------------------- /app/containers/LocaleToggle/styles.css: -------------------------------------------------------------------------------- 1 | .localeToggle { 2 | padding: 2px; 3 | } 4 | -------------------------------------------------------------------------------- /app/containers/LocaleToggle/tests/index.test.js: -------------------------------------------------------------------------------- 1 | import LocaleToggle from '../index'; 2 | import LanguageProvider from '../../LanguageProvider'; 3 | 4 | import expect from 'expect'; 5 | import { shallow, mount } from 'enzyme'; 6 | import configureStore from '../../../store'; 7 | import React from 'react'; 8 | import { Provider } from 'react-redux'; 9 | import { browserHistory } from 'react-router'; 10 | import { translationMessages } from '../../../i18n'; 11 | 12 | describe('', () => { 13 | let store; 14 | 15 | before(() => { 16 | store = configureStore({}, browserHistory); 17 | }); 18 | 19 | it('should render the default language messages', () => { 20 | const renderedComponent = shallow( 21 | 22 | 23 | 24 | 25 | 26 | ); 27 | expect(renderedComponent.contains()).toEqual(true); 28 | }); 29 | 30 | it('should present the default `en` english language option', () => { 31 | const renderedComponent = mount( 32 | 33 | 34 | 35 | 36 | 37 | ); 38 | expect(renderedComponent.contains()).toEqual(true); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /app/containers/LocaleToggle/tests/messages.test.js: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import { getLocaleMessages } from '../messages'; 3 | 4 | describe('getLocaleMessages', () => { 5 | it('should create i18n messages for all locales', () => { 6 | const expected = { 7 | en: { 8 | id: 'app.components.LocaleToggle.en', 9 | defaultMessage: 'en', 10 | }, 11 | fr: { 12 | id: 'app.components.LocaleToggle.fr', 13 | defaultMessage: 'fr', 14 | }, 15 | }; 16 | 17 | const actual = getLocaleMessages(['en', 'fr']); 18 | 19 | assert.deepEqual(expected, actual); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /app/containers/NotFoundPage/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * NotFoundPage 3 | * 4 | * This is the page we show when the user visits a url that doesn't have a route 5 | */ 6 | 7 | import React from 'react'; 8 | import { connect } from 'react-redux'; 9 | import { push } from 'react-router-redux'; 10 | 11 | import messages from './messages'; 12 | import { FormattedMessage } from 'react-intl'; 13 | import Button from 'components/Button'; 14 | import H1 from 'components/H1'; 15 | 16 | export function NotFound(props) { 17 | return ( 18 |
19 |

20 | 21 |

22 | 29 |
30 | ); 31 | } 32 | 33 | NotFound.propTypes = { 34 | changeRoute: React.PropTypes.func, 35 | }; 36 | 37 | // react-redux stuff 38 | function mapDispatchToProps(dispatch) { 39 | return { 40 | changeRoute: (url) => dispatch(push(url)), 41 | }; 42 | } 43 | 44 | // Wrap the component to inject dispatch and state into it 45 | export default connect(null, mapDispatchToProps)(NotFound); 46 | -------------------------------------------------------------------------------- /app/containers/NotFoundPage/messages.js: -------------------------------------------------------------------------------- 1 | /* 2 | * NotFoundPage Messages 3 | * 4 | * This contains all the text for the NotFoundPage component. 5 | */ 6 | import { defineMessages } from 'react-intl'; 7 | 8 | export default defineMessages({ 9 | header: { 10 | id: 'boilerplate.containers.NotFoundPage.header', 11 | defaultMessage: 'Page not found.', 12 | }, 13 | homeButton: { 14 | id: 'boilerplate.containers.NotFoundPage.home', 15 | defaultMessage: 'Home', 16 | }, 17 | }); 18 | -------------------------------------------------------------------------------- /app/containers/NotFoundPage/tests/index.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Testing the NotFoundPage 3 | */ 4 | 5 | import expect from 'expect'; 6 | import { shallow, mount } from 'enzyme'; 7 | import React from 'react'; 8 | 9 | import { IntlProvider, FormattedMessage } from 'react-intl'; 10 | import { NotFound } from '../index'; 11 | import H1 from 'components/H1'; 12 | import Button from 'components/Button'; 13 | 14 | describe('', () => { 15 | it('should render the Page Not Found text', () => { 16 | const renderedComponent = shallow( 17 | 18 | ); 19 | expect(renderedComponent.contains( 20 |

21 | 25 |

)).toEqual(true); 26 | }); 27 | 28 | it('should render a button', () => { 29 | const renderedComponent = shallow( 30 | 31 | ); 32 | const renderedButton = renderedComponent.find(Button); 33 | expect(renderedButton.length).toEqual(1); 34 | }); 35 | 36 | it('should link to "/"', () => { 37 | const changeRouteSpy = expect.createSpy(); 38 | const onChangeRoute = (dest) => { 39 | if (dest === '/') { 40 | changeRouteSpy(); 41 | } 42 | }; 43 | 44 | const renderedComponent = mount( 45 | 46 | 47 | 48 | ); 49 | const button = renderedComponent.find('button'); 50 | button.simulate('click'); 51 | expect(changeRouteSpy).toHaveBeenCalled(); 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /app/containers/RepoListItem/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * RepoListItem 3 | * 4 | * Lists the name and the issue count of a repository 5 | */ 6 | 7 | import React from 'react'; 8 | import { connect } from 'react-redux'; 9 | import { createSelector } from 'reselect'; 10 | 11 | import { FormattedNumber } from 'react-intl'; 12 | import { selectCurrentUser } from 'containers/App/selectors'; 13 | import ListItem from 'components/ListItem'; 14 | import IssueIcon from 'components/IssueIcon'; 15 | import A from 'components/A'; 16 | 17 | import styles from './styles.css'; 18 | 19 | export class RepoListItem extends React.Component { // eslint-disable-line react/prefer-stateless-function 20 | render() { 21 | const item = this.props.item; 22 | let nameprefix = ''; 23 | 24 | // If the repository is owned by a different person than we got the data for 25 | // it's a fork and we should show the name of the owner 26 | if (item.owner.login !== this.props.currentUser) { 27 | nameprefix = `${item.owner.login}/`; 28 | } 29 | 30 | // Put together the content of the repository 31 | const content = ( 32 | 49 | ); 50 | 51 | // Render the content into a list item 52 | return ( 53 | 54 | ); 55 | } 56 | } 57 | 58 | RepoListItem.propTypes = { 59 | item: React.PropTypes.object, 60 | currentUser: React.PropTypes.string, 61 | }; 62 | 63 | export default connect(createSelector( 64 | selectCurrentUser(), 65 | (currentUser) => ({ currentUser }) 66 | ))(RepoListItem); 67 | -------------------------------------------------------------------------------- /app/containers/RepoListItem/styles.css: -------------------------------------------------------------------------------- 1 | .linkWrapper { 2 | width: 100%; 3 | height: 100%; 4 | display: flex; 5 | align-items: space-between; 6 | } 7 | 8 | .linkRepo { 9 | height: 100%; 10 | color: black; 11 | display: flex; 12 | align-items: center; 13 | width: 100%; 14 | } 15 | 16 | .linkIssues { 17 | color: black; 18 | height: 100%; 19 | display: flex; 20 | align-items: center; 21 | justify-content: center; 22 | } 23 | 24 | .issueIcon { 25 | fill: #CCC; 26 | margin-right: 0.25em; 27 | } 28 | -------------------------------------------------------------------------------- /app/containers/RepoListItem/tests/index.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Test the repo list item 3 | */ 4 | 5 | import expect from 'expect'; 6 | import { shallow, mount } from 'enzyme'; 7 | import React from 'react'; 8 | 9 | import { IntlProvider } from 'react-intl'; 10 | import { RepoListItem } from '../index'; 11 | import ListItem from 'components/ListItem'; 12 | 13 | describe('', () => { 14 | let item; 15 | 16 | // Before each test reset the item data for safety 17 | beforeEach(() => { 18 | item = { 19 | owner: { 20 | login: 'mxstbr', 21 | }, 22 | html_url: 'https://github.com/mxstbr/react-boilerplate', 23 | name: 'react-boilerplate', 24 | open_issues_count: 20, 25 | full_name: 'mxstbr/react-boilerplate', 26 | }; 27 | }); 28 | 29 | it('should render a ListItem', () => { 30 | const renderedComponent = shallow( 31 | 32 | ); 33 | expect(renderedComponent.find(ListItem).length).toEqual(1); 34 | }); 35 | 36 | it('should not render the current username', () => { 37 | const renderedComponent = mount( 38 | 39 | 40 | 41 | ); 42 | expect(renderedComponent.text().indexOf(item.owner.login)).toBeLessThan(0); 43 | }); 44 | 45 | it('should render usernames that are not the current one', () => { 46 | const renderedComponent = mount( 47 | 48 | 49 | 50 | ); 51 | expect(renderedComponent.text().indexOf(item.owner.login)).toBeGreaterThan(-1); 52 | }); 53 | 54 | it('should render the repo name', () => { 55 | const renderedComponent = mount( 56 | 57 | 58 | 59 | ); 60 | expect(renderedComponent.text().indexOf(item.name)).toBeGreaterThan(-1); 61 | }); 62 | 63 | it('should render the issue count', () => { 64 | const renderedComponent = mount( 65 | 66 | 67 | 68 | ); 69 | expect(renderedComponent.text().indexOf(item.open_issues_count)).toBeGreaterThan(1); 70 | }); 71 | 72 | it('should render the IssueIcon', () => { 73 | const renderedComponent = mount( 74 | 75 | 76 | 77 | ); 78 | expect(renderedComponent.find('svg').length).toEqual(1); 79 | }); 80 | }); 81 | -------------------------------------------------------------------------------- /app/contracts/SimpleStore.sol: -------------------------------------------------------------------------------- 1 | 2 | 3 | contract SimpleStore { 4 | uint public store; 5 | 6 | function set(uint _store) { 7 | store = _store; 8 | } 9 | 10 | function get() constant public returns (uint storeValue) { 11 | return store; 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /app/contracts/SimpleStoreFactory.sol: -------------------------------------------------------------------------------- 1 | import "SimpleStore.sol"; 2 | import "SimpleStoreRegistry.sol"; 3 | 4 | 5 | contract SimpleStoreFactory { 6 | function SimpleStoreFactory(address _registry){ 7 | registry = SimpleStoreRegistry(_registry); 8 | } 9 | 10 | function createSimpleStore() returns (address simpleStore) { 11 | simpleStore = address(new SimpleStore()); 12 | registry.register(simpleStore); 13 | return simpleStore; 14 | } 15 | 16 | SimpleStoreRegistry registry; 17 | } 18 | -------------------------------------------------------------------------------- /app/contracts/SimpleStoreRegistry.sol: -------------------------------------------------------------------------------- 1 | 2 | 3 | contract SimpleStoreRegistry { 4 | 5 | function register(address simpleStore) public returns (uint serviceId) { 6 | serviceId = services.length++; 7 | ids[simpleStore] = serviceId; 8 | services[serviceId] = simpleStore; 9 | } 10 | 11 | function getService(uint serviceId) returns (address serviceAddres) { 12 | return services[serviceId]; 13 | } 14 | 15 | address[] public services; 16 | mapping(address => uint) public ids; 17 | } 18 | -------------------------------------------------------------------------------- /app/contracts/tests/test.SimpleStore.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /* globals describe, it, before */ 3 | 4 | const contracts = require('../build/classes.json'); 5 | const chaithereum = require('chaithereum'); 6 | 7 | before(() => chaithereum.promise); 8 | 9 | describe('SimpleStore', () => { 10 | let simpleStore; 11 | 12 | it('successfully instantiates with blank params', () => { 13 | return chaithereum.web3.eth.contract(JSON.parse(contracts.SimpleStore.interface)).new.q({ data: contracts.SimpleStore.bytecode }).should.eventually.be.contract.then((_simpleStore) => { 14 | simpleStore = _simpleStore; 15 | }).should.eventually.be.fulfilled; 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /app/contracts/tests/test.SimpleStore.sol: -------------------------------------------------------------------------------- 1 | import "dapple/test.sol"; 2 | import "SimpleStore.sol"; 3 | 4 | 5 | contract SimpleStoreTest is Test { 6 | SimpleStore target; 7 | 8 | function refreshTargetInstance() { 9 | target = new SimpleStore(); 10 | } 11 | 12 | function setUp() { 13 | refreshTargetInstance(); 14 | } 15 | 16 | function testSetMethod() { 17 | uint256 testValue = 45; 18 | target.set(testValue); 19 | assertEq(target.get(), testValue); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/contracts/tests/test.SimpleStoreFactory.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /* globals describe, it, before */ 3 | 4 | const contracts = require('../build/classes.json'); 5 | const chaithereum = require('chaithereum'); 6 | 7 | before(() => chaithereum.promise); 8 | 9 | describe('SimpleStoreFactory', () => { 10 | let simpleStoreFactory; 11 | 12 | it('successfully instantiates with blank params', () => { 13 | return chaithereum.web3.eth.contract(JSON.parse(contracts.SimpleStoreFactory.interface)).new.q({ data: contracts.SimpleStoreFactory.bytecode }).should.eventually.be.contract.then((_simpleStoreFactory) => { 14 | simpleStoreFactory = _simpleStoreFactory; 15 | }).should.eventually.be.fulfilled; 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /app/contracts/tests/test.SimpleStoreFactory.sol: -------------------------------------------------------------------------------- 1 | import "dapple/test.sol"; 2 | import "SimpleStoreFactory.sol"; 3 | 4 | 5 | contract SimpleStoreFactoryTest is Test { 6 | SimpleStoreFactory target; 7 | SimpleStoreRegistry registry; 8 | 9 | function refreshTarget() { 10 | registry = new SimpleStoreRegistry(); 11 | target = new SimpleStoreFactory(address(registry)); 12 | } 13 | 14 | function testFactory() { 15 | address simpleStore = target.createSimpleStore(); 16 | assertEq(registry.getService(0), simpleStore); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /app/contracts/tests/test.SimpleStoreRegistry.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /* globals describe, it, before */ 3 | 4 | const contracts = require('../build/classes.json'); 5 | const chaithereum = require('chaithereum'); 6 | 7 | before(() => chaithereum.promise); 8 | 9 | describe('SimpleStoreRegistry', () => { 10 | let simpleStoreRegistry; 11 | 12 | it('successfully instantiates with blank params', () => { 13 | return chaithereum.web3.eth.contract(JSON.parse(contracts.SimpleStoreRegistry.interface)).new.q({ data: contracts.SimpleStoreRegistry.bytecode }).should.eventually.be.contract.then((_simpleStoreRegistry) => { 14 | simpleStoreRegistry = _simpleStoreRegistry; 15 | }).should.eventually.be.fulfilled; 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /app/contracts/tests/test.SimpleStoreRegistry.sol: -------------------------------------------------------------------------------- 1 | import "dapple/test.sol"; 2 | import "SimpleStoreRegistry.sol"; 3 | 4 | 5 | contract SimpleStoreRegistryTest is Test { 6 | SimpleStoreRegistry target; 7 | 8 | function refreshTarget() { 9 | target = new SimpleStoreRegistry(); 10 | } 11 | 12 | function testRegistry() { 13 | address someAddr = address(target); 14 | target.register(someAddr); 15 | assertEq(target.getService(0), someAddr); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /app/dappfile: -------------------------------------------------------------------------------- 1 | version: 1.0.0 2 | tags: [] 3 | layout: 4 | sol_sources: ./contracts/ 5 | build_dir: ./contracts/build/ 6 | dependencies: {} 7 | ignore: [] 8 | name: react-dapp-boilerplate 9 | -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SilentCicero/react-dapp-boilerplate/23404782642847d99b751240f62fde31f10e218b/app/favicon.ico -------------------------------------------------------------------------------- /app/i18n.js: -------------------------------------------------------------------------------- 1 | /** 2 | * i18n.js 3 | * 4 | * This will setup the i18n language files and locale data for your app. 5 | * 6 | */ 7 | import { addLocaleData } from 'react-intl'; 8 | 9 | import enLocaleData from 'react-intl/locale-data/en'; 10 | import deLocaleData from 'react-intl/locale-data/de'; 11 | 12 | addLocaleData(enLocaleData); 13 | addLocaleData(deLocaleData); 14 | 15 | export const appLocales = [ 16 | 'en', 17 | 'de', 18 | ]; 19 | 20 | import enTranslationMessages from './translations/en.json'; 21 | import deTranslationMessages from './translations/de.json'; 22 | 23 | export const formatTranslationMessages = (messages) => { 24 | const formattedMessages = {}; 25 | for (const message of messages) { 26 | formattedMessages[message.id] = message.message || message.defaultMessage; 27 | } 28 | 29 | return formattedMessages; 30 | }; 31 | 32 | export const translationMessages = { 33 | en: formatTranslationMessages(enTranslationMessages), 34 | de: formatTranslationMessages(deTranslationMessages), 35 | }; 36 | -------------------------------------------------------------------------------- /app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | React.js Boilerplate 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /app/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "React Boilerplate", 3 | "icons": [ 4 | { 5 | "src": "favicon.png", 6 | "sizes": "48x48", 7 | "type": "image/png", 8 | "density": 1.0 9 | }, 10 | { 11 | "src": "favicon.png", 12 | "sizes": "96x96", 13 | "type": "image/png", 14 | "density": 2.0 15 | }, 16 | { 17 | "src": "favicon.png", 18 | "sizes": "144x144", 19 | "type": "image/png", 20 | "density": 3.0 21 | }, 22 | { 23 | "src": "favicon.png", 24 | "sizes": "192x192", 25 | "type": "image/png", 26 | "density": 4.0 27 | } 28 | ], 29 | "start_url": "index.html", 30 | "display": "standalone", 31 | "orientation": "portrait", 32 | "background_color": "#FFFFFF" 33 | } -------------------------------------------------------------------------------- /app/reducers.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Combine all reducers in this file and export the combined reducers. 3 | * If we were to do this in store.js, reducers wouldn't be hot reloadable. 4 | */ 5 | 6 | import { fromJS } from 'immutable'; 7 | import { combineReducers } from 'redux-immutable'; 8 | import { LOCATION_CHANGE } from 'react-router-redux'; 9 | 10 | import globalReducer from 'containers/App/reducer'; 11 | import languageProviderReducer from 'containers/LanguageProvider/reducer'; 12 | 13 | /* 14 | * routeReducer 15 | * 16 | * The reducer merges route location changes into our immutable state. 17 | * The change is necessitated by moving to react-router-redux@4 18 | * 19 | */ 20 | 21 | // Initial routing state 22 | const routeInitialState = fromJS({ 23 | locationBeforeTransitions: null, 24 | }); 25 | 26 | /** 27 | * Merge route into the global application state 28 | */ 29 | function routeReducer(state = routeInitialState, action) { 30 | switch (action.type) { 31 | /* istanbul ignore next */ 32 | case LOCATION_CHANGE: 33 | return state.merge({ 34 | locationBeforeTransitions: action.payload, 35 | }); 36 | default: 37 | return state; 38 | } 39 | } 40 | 41 | /** 42 | * Creates the main reducer with the asynchronously loaded ones 43 | */ 44 | export default function createReducer(asyncReducers) { 45 | return combineReducers({ 46 | route: routeReducer, 47 | global: globalReducer, 48 | language: languageProviderReducer, 49 | ...asyncReducers, 50 | }); 51 | } 52 | -------------------------------------------------------------------------------- /app/routes.js: -------------------------------------------------------------------------------- 1 | // These are the pages you can go to. 2 | // They are all wrapped in the App component, which should contain the navbar etc 3 | // See http://blog.mxstbr.com/2016/01/react-apps-with-pages for more information 4 | // about the code splitting business 5 | import { getAsyncInjectors } from './utils/asyncInjectors'; 6 | 7 | const errorLoading = (err) => { 8 | console.error('Dynamic page loading failed', err); // eslint-disable-line no-console 9 | }; 10 | 11 | const loadModule = (cb) => (componentModule) => { 12 | cb(null, componentModule.default); 13 | }; 14 | 15 | export default function createRoutes(store) { 16 | // create reusable async injectors using getAsyncInjectors factory 17 | const { injectReducer, injectSagas } = getAsyncInjectors(store); 18 | 19 | return [ 20 | { 21 | path: '/', 22 | name: 'home', 23 | getComponent(nextState, cb) { 24 | const importModules = Promise.all([ 25 | System.import('containers/HomePage/reducer'), 26 | System.import('containers/HomePage/sagas'), 27 | System.import('containers/HomePage'), 28 | ]); 29 | 30 | const renderRoute = loadModule(cb); 31 | 32 | importModules.then(([reducer, sagas, component]) => { 33 | injectReducer('home', reducer.default); 34 | injectSagas(sagas.default); 35 | 36 | renderRoute(component); 37 | }); 38 | 39 | importModules.catch(errorLoading); 40 | }, 41 | }, { 42 | path: '/features', 43 | name: 'features', 44 | getComponent(nextState, cb) { 45 | System.import('containers/FeaturePage') 46 | .then(loadModule(cb)) 47 | .catch(errorLoading); 48 | }, 49 | }, { 50 | path: '*', 51 | name: 'notfound', 52 | getComponent(nextState, cb) { 53 | System.import('containers/NotFoundPage') 54 | .then(loadModule(cb)) 55 | .catch(errorLoading); 56 | }, 57 | }, 58 | ]; 59 | } 60 | -------------------------------------------------------------------------------- /app/store.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Create the store with asynchronously loaded reducers 3 | */ 4 | 5 | import { createStore, applyMiddleware, compose } from 'redux'; 6 | import { fromJS } from 'immutable'; 7 | import { routerMiddleware } from 'react-router-redux'; 8 | import createSagaMiddleware from 'redux-saga'; 9 | import createReducer from './reducers'; 10 | 11 | const sagaMiddleware = createSagaMiddleware(); 12 | const devtools = window.devToolsExtension || (() => noop => noop); 13 | 14 | export default function configureStore(initialState = {}, history) { 15 | // Create the store with two middlewares 16 | // 1. sagaMiddleware: Makes redux-sagas work 17 | // 2. routerMiddleware: Syncs the location/URL path to the state 18 | const middlewares = [ 19 | sagaMiddleware, 20 | routerMiddleware(history), 21 | ]; 22 | 23 | const enhancers = [ 24 | applyMiddleware(...middlewares), 25 | devtools(), 26 | ]; 27 | 28 | const store = createStore( 29 | createReducer(), 30 | fromJS(initialState), 31 | compose(...enhancers) 32 | ); 33 | 34 | // Extensions 35 | store.runSaga = sagaMiddleware.run; 36 | store.asyncReducers = {}; // Async reducer registry 37 | 38 | // Make reducers hot reloadable, see http://mxs.is/googmo 39 | /* istanbul ignore next */ 40 | if (module.hot) { 41 | module.hot.accept('./reducers', () => { 42 | System.import('./reducers').then((reducerModule) => { 43 | const createReducers = reducerModule.default; 44 | const nextReducers = createReducers(store.asyncReducers); 45 | 46 | store.replaceReducer(nextReducers); 47 | }); 48 | }); 49 | } 50 | 51 | return store; 52 | } 53 | -------------------------------------------------------------------------------- /app/tests/store.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Test store addons 3 | */ 4 | 5 | import expect from 'expect'; 6 | import configureStore from '../store'; 7 | import { browserHistory } from 'react-router'; 8 | 9 | describe('configureStore', () => { 10 | let store; 11 | 12 | before(() => { 13 | store = configureStore({}, browserHistory); 14 | }); 15 | 16 | describe('asyncReducers', () => { 17 | it('should contain an object for async reducers', () => { 18 | expect(typeof store.asyncReducers).toEqual('object'); 19 | }); 20 | }); 21 | 22 | describe('runSaga', () => { 23 | it('should contain a hook for `sagaMiddleware.run`', () => { 24 | expect(typeof store.runSaga).toEqual('function'); 25 | }); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /app/utils/asyncInjectors.js: -------------------------------------------------------------------------------- 1 | import { conformsTo, isEmpty, isFunction, isObject, isString } from 'lodash'; 2 | import invariant from 'invariant'; 3 | import warning from 'warning'; 4 | import createReducer from '../reducers'; 5 | 6 | /** 7 | * Validate the shape of redux store 8 | */ 9 | export function checkStore(store) { 10 | const shape = { 11 | dispatch: isFunction, 12 | subscribe: isFunction, 13 | getState: isFunction, 14 | replaceReducer: isFunction, 15 | runSaga: isFunction, 16 | asyncReducers: isObject, 17 | }; 18 | invariant( 19 | conformsTo(store, shape), 20 | '(app/utils...) asyncInjectors: Expected a valid redux store' 21 | ); 22 | } 23 | 24 | /** 25 | * Inject an asynchronously loaded reducer 26 | */ 27 | export function injectAsyncReducer(store, isValid) { 28 | return function injectReducer(name, asyncReducer) { 29 | if (!isValid) checkStore(store); 30 | 31 | invariant( 32 | isString(name) && !isEmpty(name) && isFunction(asyncReducer), 33 | '(app/utils...) injectAsyncReducer: Expected `asyncReducer` to be a reducer function' 34 | ); 35 | 36 | store.asyncReducers[name] = asyncReducer; // eslint-disable-line no-param-reassign 37 | store.replaceReducer(createReducer(store.asyncReducers)); 38 | }; 39 | } 40 | 41 | /** 42 | * Inject an asynchronously loaded saga 43 | */ 44 | export function injectAsyncSagas(store, isValid) { 45 | return function injectSagas(sagas) { 46 | if (!isValid) checkStore(store); 47 | 48 | invariant( 49 | Array.isArray(sagas), 50 | '(app/utils...) injectAsyncSagas: Expected `sagas` to be an array of generator functions' 51 | ); 52 | 53 | warning( 54 | !isEmpty(sagas), 55 | '(app/utils...) injectAsyncSagas: Received an empty `sagas` array' 56 | ); 57 | 58 | sagas.map(store.runSaga); 59 | }; 60 | } 61 | 62 | /** 63 | * Helper for creating injectors 64 | */ 65 | export function getAsyncInjectors(store) { 66 | checkStore(store); 67 | 68 | return { 69 | injectReducer: injectAsyncReducer(store, true), 70 | injectSagas: injectAsyncSagas(store, true), 71 | }; 72 | } 73 | -------------------------------------------------------------------------------- /app/utils/request.js: -------------------------------------------------------------------------------- 1 | import 'whatwg-fetch'; 2 | 3 | /** 4 | * Parses the JSON returned by a network request 5 | * 6 | * @param {object} response A response from a network request 7 | * 8 | * @return {object} The parsed JSON from the request 9 | */ 10 | function parseJSON(response) { 11 | return response.json(); 12 | } 13 | 14 | /** 15 | * Checks if a network request came back fine, and throws an error if not 16 | * 17 | * @param {object} response A response from a network request 18 | * 19 | * @return {object|undefined} Returns either the response, or throws an error 20 | */ 21 | function checkStatus(response) { 22 | if (response.status >= 200 && response.status < 300) { 23 | return response; 24 | } 25 | 26 | const error = new Error(response.statusText); 27 | error.response = response; 28 | throw error; 29 | } 30 | 31 | /** 32 | * Requests a URL, returning a promise 33 | * 34 | * @param {string} url The URL we want to request 35 | * @param {object} [options] The options we want to pass to "fetch" 36 | * 37 | * @return {object} An object containing either "data" or "err" 38 | */ 39 | export default function request(url, options) { 40 | return fetch(url, options) 41 | .then(checkStatus) 42 | .then(parseJSON) 43 | .then((data) => ({ data })) 44 | .catch((err) => ({ err })); 45 | } 46 | -------------------------------------------------------------------------------- /app/utils/tests/request.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Test the request function 3 | */ 4 | 5 | import request from '../request'; 6 | import sinon from 'sinon'; 7 | import expect from 'expect'; 8 | 9 | describe('request', () => { 10 | // Before each test, stub the fetch function 11 | beforeEach(() => { 12 | sinon.stub(window, 'fetch'); 13 | }); 14 | 15 | // After each test, restore the fetch function 16 | afterEach(() => { 17 | window.fetch.restore(); 18 | }); 19 | 20 | describe('stubbing successful response', () => { 21 | // Before each test, pretend we got a successful response 22 | beforeEach(() => { 23 | const res = new Response('{"hello":"world"}', { 24 | status: 200, 25 | headers: { 26 | 'Content-type': 'application/json', 27 | }, 28 | }); 29 | 30 | window.fetch.returns(Promise.resolve(res)); 31 | }); 32 | 33 | it('should format the response correctly', (done) => { 34 | request('/thisurliscorrect') 35 | .catch(done) 36 | .then((json) => { 37 | expect(json.data.hello).toEqual('world'); 38 | done(); 39 | }); 40 | }); 41 | }); 42 | 43 | describe('stubbing error response', () => { 44 | // Before each test, pretend we got an unsuccessful response 45 | beforeEach(() => { 46 | const res = new Response('', { 47 | status: 404, 48 | statusText: 'Not Found', 49 | headers: { 50 | 'Content-type': 'application/json', 51 | }, 52 | }); 53 | 54 | window.fetch.returns(Promise.resolve(res)); 55 | }); 56 | 57 | it('should catch errors', (done) => { 58 | request('/thisdoesntexist') 59 | .then((json) => { 60 | expect(json.err.response.status).toEqual(404); 61 | expect(json.err.response.statusText).toEqual('Not Found'); 62 | done(); 63 | }); 64 | }); 65 | }); 66 | }); 67 | -------------------------------------------------------------------------------- /app/web3.js: -------------------------------------------------------------------------------- 1 | import Web3 from 'web3'; 2 | 3 | // instantiate new web3 instance 4 | const web3 = new Web3(); 5 | 6 | // providers 7 | export const providers = { 8 | livenet: web3.setProvider(new web3.providers.HttpProvider('https://livenet.infura.io/')), 9 | testnet: web3.setProvider(new web3.providers.HttpProvider('https://morden.infura.io/')), 10 | testrpc: web3.setProvider(new web3.providers.HttpProvider('http://localhost:8545')), 11 | }; 12 | 13 | // if window provider exists 14 | if (typeof window.web3 !== 'undefined' && typeof window.web3.currentProvider !== 'undefined') { 15 | providers.window = web3.setProvider(window.web3.currentProvider); 16 | } 17 | 18 | // get current provider 19 | export function getCurrentProvider() { 20 | return web3.currentProvider; 21 | } 22 | 23 | // set provider abstraction 24 | export function setProvider(provider) { 25 | if (typeof provider === 'string') { 26 | web3.setProvider(providers[provider]); 27 | } else { 28 | web3.setProvider(provider); 29 | } 30 | } 31 | 32 | // Abstraction: 33 | // The web3 object may change in the future 34 | // it is best to abstract the critical methods 35 | // so we dont get hung up on object design that may change in the future 36 | 37 | // abstract the getBalance object 38 | export function getBalance() { 39 | return web3.eth.getBalance.apply(web3.eth, arguments); // eslint-disable-line 40 | } 41 | 42 | // abstract the contract object 43 | export function contract() { 44 | return web3.eth.contract.apply(web3.eth, arguments); // eslint-disable-line 45 | } 46 | 47 | // export web3 object instance 48 | export default web3; 49 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | # http://www.appveyor.com/docs/appveyor-yml 2 | 3 | # Set build version format here instead of in the admin panel 4 | version: "{build}" 5 | 6 | # Do not build on gh tags 7 | skip_tags: true 8 | 9 | # Test against these versions of Node.js 10 | environment: 11 | 12 | matrix: 13 | # Node versions to run 14 | - nodejs_version: "5.0" 15 | 16 | # Fix line endings in Windows. (runs before repo cloning) 17 | init: 18 | - git config --global core.autocrlf input 19 | 20 | # Install scripts--runs after repo cloning 21 | install: 22 | # Install chrome 23 | - choco install -y googlechrome 24 | # Install the latest stable version of Node 25 | - ps: Install-Product node $env:nodejs_version 26 | - npm -g install npm 27 | - set PATH=%APPDATA%\npm;%PATH% 28 | - npm install 29 | 30 | # Disable automatic builds 31 | build: off 32 | 33 | # Post-install test scripts 34 | test_script: 35 | # Output debugging info 36 | - node --version 37 | - npm --version 38 | # run build and run tests 39 | - npm run build 40 | 41 | # remove, as appveyor doesn't support secure variables on pr builds 42 | # so `COVERALLS_REPO_TOKEN` cannot be set, without hard-coding in this file 43 | #on_success: 44 | #- npm run coveralls 45 | -------------------------------------------------------------------------------- /docs/css/README.md: -------------------------------------------------------------------------------- 1 | # CSS 2 | 3 | This boilerplate uses PostCSS as a CSS preprocessor with a few utility plugins 4 | to make it "batteries included". 5 | 6 | CSS Modules lets us embrace component encapsulation while sanitize.css gives us 7 | data-driven cross-browser normalisation. 8 | 9 | Learn more: 10 | 11 | - [PostCSS](postcss.md) 12 | - [CSS Modules](css-modules.md) 13 | - [sanitize.css](sanitize.md) 14 | - [stylelint.css](stylelint.md) 15 | - [Using Sass](sass.md) 16 | -------------------------------------------------------------------------------- /docs/css/postcss.md: -------------------------------------------------------------------------------- 1 | # PostCSS 2 | 3 | PostCSS is a modular CSS preprocessor based on JavaScript. It comes pre- 4 | configured with the plugins listed below. 5 | 6 | See the [official documentation](https://github.com/postcss/postcss) for more 7 | information! 8 | 9 | ## Plugins 10 | 11 | This boilerplate bundles a few of the most useful PostCSS plugins by default: 12 | 13 | - [`postcss-focus`](https://github.com/postcss/postcss-focus): Adds a `:focus` 14 | selector to every `:hover` selector for keyboard accessibility. 15 | - [`autoprefixer`](https://github.com/postcss/autoprefixer): Prefixes your CSS 16 | automatically for the last two versions of all major browsers and IE10+. 17 | - [`cssnext`](https://github.com/moox/postcss-cssnext): Use tomorrow's CSS 18 | features today. Transpiles CSS4 features down to CSS3. 19 | - [`cssnano`](https://github.com/ben-eb/cssnano): Optimizes your CSS file. For a 20 | full list of optimizations check [the offical website](http://cssnano.co/optimisations/). 21 | 22 | For more awesome features that the PostCSS ecosystem offers, check out the 23 | comprehensive, fully-searchable catalog of available plugins at [postcss.parts](http://postcss.parts). 24 | 25 | ## Adding a new PostCSS plugin 26 | 27 | 1. Add the plugin to your project (e.g. `npm install --save-dev postcss-super-plugin`). 28 | 2. Modify `internals/webpack/webpack.dev.babel.js`: 29 | - Add `const postcssSuperPlugin = require('postcss-super-plugin');` 30 | to `// PostCSS plugins` section. 31 | - Find `postcss: () => [/* ... current set of plugins ... */]` and add 32 | the new plugin to the list: `postcssPlugins: [/* ... */, postcssSuperPlugin()]`. 33 | 3. Restart the server (`CTRL+C`, `npm start`) for the new plugin to become available 34 | (webpack does not pick config changes while running). 35 | 36 | Before installing a new plugin, make sure that you are not trying to add a feature 37 | that is already available. It is likely that what you are looking for 38 | [is supported by `cssnext`](http://cssnext.io/features/), which is a part of the boilerplate. 39 | 40 | --- 41 | 42 | _Don't like this feature? [Click here](remove.md)_ 43 | -------------------------------------------------------------------------------- /docs/css/remove.md: -------------------------------------------------------------------------------- 1 | ## Removing CSS modules 2 | 3 | To remove this feature from your setup, stop importing `.css` files in your 4 | components and delete the `modules` option from the `css-loader` declaration in 5 | [`webpack.prod.babel.js`](/internals/webpack/webpack.prod.babel.js) and 6 | [`webpack.base.babel.js`](/internals/webpack/webpack.base.babel.js)! 7 | 8 | ## Removing PostCSS 9 | 10 | To remove PostCSS, delete the `postcssPlugins` option and remove all occurences 11 | of the `postcss-loader` from 12 | 13 | - [`webpack.dev.babel.js`](/internals/webpack/webpack.dev.babel.js) 14 | - [`webpack.prod.babel.js`](/internals/webpack/webpack.prod.babel.js) 15 | - [`webpack.base.babel.js`](/internals/webpack/webpack.base.babel.js) 16 | 17 | When that is done - and you've verified that everything is still working - remove 18 | all related dependencies from [`package.json`](/package.json)! 19 | 20 | ## Removing `sanitize.css` 21 | 22 | Delete [lines 44 and 45 in `app.js`](../../app/app.js#L44-L45) and remove it 23 | from the `dependencies` in [`package.json`](../../package.json)! 24 | -------------------------------------------------------------------------------- /docs/css/sanitize.md: -------------------------------------------------------------------------------- 1 | # `sanitize.css` 2 | 3 | Sanitize.css makes browsers render elements more in 4 | line with developer expectations (e.g. having the box model set to a cascading 5 | `box-sizing: border-box`) and preferences (its defaults can be individually 6 | overridden). 7 | 8 | It was selected over older projects like `normalize.css` and `reset.css` due 9 | to its greater flexibility and better alignment with CSSNext features like CSS 10 | variables. 11 | 12 | See the [official documentation](https://github.com/10up/sanitize.css) for more 13 | information. 14 | 15 | --- 16 | 17 | _Don't like this feature? [Click here](remove.md)_ 18 | -------------------------------------------------------------------------------- /docs/css/sass.md: -------------------------------------------------------------------------------- 1 | # Can I use Sass with this boilerplate? 2 | 3 | Yes, although we advise against it and **do not support this**. We selected 4 | PostCSS over Sass because its approach is more powerful: instead of trying to 5 | give a styling language programmatic abilities, it pulls logic and configuration 6 | out into JS where we believe those features belong. 7 | 8 | As an alternative, consider installing a PostCSS plugin called [`PreCSS`](https://github.com/jonathantneal/precss): 9 | it lets you use familiar syntax - $variables, nesting, mixins, etc. - but retain 10 | the advantages (speed, memory efficiency, extensibility, etc) of PostCSS. 11 | 12 | If you _really_ still want (or need) to use Sass then... 13 | 14 | 1. Change `internals/webpack/webpack.base.babel.js` so that line 22 reads 15 | ```JavaScript 16 | test: /\.s?css$/, 17 | ``` 18 | 19 | This means that both `.scss` and `.css` will be picked up by the compiler 20 | 21 | 1. Update each of 22 | 23 | - `internals/webpack/webpack.dev.babel.js` 24 | - `internals/webpack/webpack.prod.babel.js` 25 | 26 | changing the config option for `cssLoaders` to 27 | 28 | ```JavaScript 29 | cssLoaders: 'style-loader!css-loader?modules&importLoaders=1&sourceMap!postcss-loader!sass-loader', 30 | ``` 31 | 32 | Then run `npm i -D sass-loader node-sass` 33 | 34 | ...and you should be good to go! 35 | -------------------------------------------------------------------------------- /docs/css/stylelint.md: -------------------------------------------------------------------------------- 1 | # stylelint 2 | 3 | stylelint catches bugs and helps keep you and your team on consistent with the 4 | standards and conventions you define. 5 | 6 | We've pre-configured it to extend [stylelint-config-standard](https://github.com/stylelint/stylelint-config-standard) 7 | but you can (and should!) adapt it to your house style. 8 | 9 | See the [official documentation](http://stylelint.io/) for more information! 10 | -------------------------------------------------------------------------------- /docs/general/deployment.md: -------------------------------------------------------------------------------- 1 | # Deployment 2 | 3 | ## Heroku 4 | 5 | ### Easy 5-Step Deployment Process 6 | 7 | *Step 1:* Create a Procfile with the following line: `web: npm run start:prod`. We are doing this because heroku runs `npm run start` by default, so we need this setting to override the default run command. 8 | 9 | *Step 2:* Install heroku's buildpack on your heroku app by running the following command: `heroku buildpacks:set https://github.com/heroku/heroku-buildpack-nodejs#v90 -a [your app name]`. Make sure to replace `#v90` with whatever the latest buildpack is which you can [find here](https://github.com/heroku/heroku-buildpack-nodejs/releases). 10 | 11 | *Step 3:* Add this line to your Package.json file in the scripts area: `"postinstall": "npm run build:clean",`. This is because Heroku runs this as part of their build process (more of which you can [read about here](https://devcenter.heroku.com/articles/nodejs-support#build-behavior)). 12 | 13 | *Step 4:* Run `heroku config:set NPM_CONFIG_PRODUCTION=false` so that Heroku can compile the NPM Modules included in your devDependencies (since many of these packages are required for the build process). 14 | 15 | *Step 5:* Follow the standard Heroku deploy process at this point: 16 | 17 | 1. `git add .` 18 | 2. `git commit -m 'Made some epic changes as per usual'` 19 | 3. `git push heroku master` 20 | -------------------------------------------------------------------------------- /docs/general/files.md: -------------------------------------------------------------------------------- 1 | # Configuration: A Glossary 2 | 3 | A guide to the configuration files for this project: where they live and what 4 | they do. 5 | 6 | ## The root folder 7 | 8 | * `.editorconfig`: Sets the default configuration for certain files across editors. (e.g. indentation) 9 | 10 | * `.gitattributes`: Normalizes how `git`, the version control system this boilerplate uses, handles certain files. 11 | 12 | * `.gitignore`: Tells `git` to ignore certain files and folders which don't need to be version controlled, like the build folder. 13 | 14 | * `.travis.yml` and `appveyor.yml`: Continuous Integration configuration
15 | This boilerplate uses [Travis CI](https://travis-ci.com) for Linux environments 16 | and [AppVeyor](https://www.appveyor.com/) for Windows platforms, but feel free 17 | to swap either out for your own choice of CI. 18 | 19 | * `package.json`: Our `npm` configuration file has three functions: 20 | 21 | 1. It's where Babel, ESLint and stylelint are configured 22 | 1. It's the API for the project: a consistent interface for all its controls 23 | 1. It lists the project's package dependencies 24 | 25 | Baking the config in is a slightly unusual set-up, but it allows us to keep 26 | the project root as uncluttered and grokkable-at-a-glance as possible. 27 | 28 | ## The `./internals` folder 29 | 30 | This is where the bulk of the tooling configuration lives, broken out into 31 | recognisable units of work. 32 | 33 | Feel free to change anything you like but don't be afraid to [ask upfront](https://gitter.im/mxstbr/react-boilerplate) 34 | whether you should: build systems are easy to break! 35 | -------------------------------------------------------------------------------- /docs/general/gotchas.md: -------------------------------------------------------------------------------- 1 | # Gotchas 2 | 3 | These are some things to be aware of when using this boilerplate. 4 | 5 | ## Special images in HTML files 6 | 7 | If you specify your images in the `.html` files using the `` tag, everything 8 | will work fine. The problem comes up if you try to include images using anything 9 | except that tag, like meta tags: 10 | 11 | ```HTML 12 | 13 | ``` 14 | 15 | The webpack `html-loader` does not recognise this as an image file and will not 16 | transfer the image to the build folder. To get webpack to transfer them, you 17 | have to import them with the file loader in your JavaScript somewhere, e.g.: 18 | 19 | ```JavaScript 20 | import 'file?name=[name].[ext]!../img/yourimg.png'; 21 | ``` 22 | 23 | Then webpack will correctly transfer the image to the build folder. 24 | -------------------------------------------------------------------------------- /docs/general/remove.md: -------------------------------------------------------------------------------- 1 | ### Removing offline access 2 | 3 | **Careful** about removing this, as there is no real downside to having your 4 | application available when the users network connection isn't perfect. 5 | 6 | To remove offline capability, delete the `offline-plugin` from the 7 | [`package.json`](../../package.json), remove the import of the plugin in 8 | [`app.js`](../../app/app.js) and remove the plugin from the 9 | [`webpack.prod.babel.js`](../../internals/webpack/webpack.prod.babel.js). 10 | 11 | ### Removing add to homescreen functionality 12 | 13 | Delete [`manifest.json`](../../app/manifest.json) and remove the 14 | `` tag from the 15 | [`index.html`](../../app/index.html). 16 | 17 | ### Removing performant web font loading 18 | 19 | **Careful** about removing this, as perceived performance might be highly impacted. 20 | 21 | To remove `FontFaceObserver`, don't import it in [`app.js`](../../app/app.js) and 22 | remove it from the [`package.json`](../../package.json). 23 | 24 | ### Removing image optimization 25 | 26 | To remove image optimization, delete the `image-webpack-loader` from the 27 | [`package.json`](../../package.json), and remove the `image-loader` from [`webpack.base.babel.js`](../../internals/webpack/webpack.base.babel.js): 28 | ``` 29 | … 30 | { 31 | test: /\.(jpg|png|gif)$/, 32 | loaders: [ 33 | 'file-loader', 34 | 'image-webpack?{progressive:true, optimizationLevel: 7, interlaced: false, pngquant:{quality: "65-90", speed: 4}}', 35 | ], 36 | } 37 | … 38 | ``` 39 | 40 | Then replace it with classic `file-loader`: 41 | 42 | ``` 43 | … 44 | { 45 | test: /\.(jpg|png|gif)$/, 46 | loader: 'file-loader', 47 | } 48 | … 49 | ``` 50 | -------------------------------------------------------------------------------- /docs/general/server-configs.md: -------------------------------------------------------------------------------- 1 | # Server Configurations 2 | 3 | ## Apache 4 | 5 | This boilerplate includes a `.htaccess` file that does two things: 6 | 7 | 1. Redirect all traffic to HTTPS because ServiceWorker only works for encrypted 8 | traffic. 9 | 1. Rewrite all pages (e.g. `yourdomain.com/subpage`) to `yourdomain.com/index.html` 10 | to let `react-router` take care of presenting the correct page. 11 | 12 | > Note: For performance reasons you should probably adapt it to run as a static 13 | `.conf` file (typically under `/etc/apache2/sites-enabled` or similar) so that 14 | your server doesn't have to apply its rules dynamically per request) 15 | 16 | ## Nginx 17 | 18 | Also it includes a `.nginx.conf` file that does the same on Nginx server. 19 | -------------------------------------------------------------------------------- /docs/general/webstorm-debug.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SilentCicero/react-dapp-boilerplate/23404782642847d99b751240f62fde31f10e218b/docs/general/webstorm-debug.png -------------------------------------------------------------------------------- /docs/general/webstorm-eslint.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SilentCicero/react-dapp-boilerplate/23404782642847d99b751240f62fde31f10e218b/docs/general/webstorm-eslint.png -------------------------------------------------------------------------------- /docs/js/README.md: -------------------------------------------------------------------------------- 1 | # JavaScript 2 | 3 | ## State management 4 | 5 | This boilerplate manages application state using [Redux](redux.md), makes it 6 | immutable with [`ImmutableJS`](immutablejs.md) and keeps access performant 7 | via [`reselect`](reselect.md). 8 | 9 | For managing asynchronous flows (e.g. logging in) we use [`redux-saga`](redux-saga.md). 10 | 11 | For routing, we use [`react-router` in combination with `react-router-redux`](routing.md). 12 | 13 | We include a generator for components, containers, sagas, routes and selectors. 14 | Run `npm run generate` to choose from the available generators, and automatically 15 | add new parts of your application! 16 | 17 | > Note: If you want to skip the generator selection process, 18 | `npm run generate ` also works. (e.g. `npm run generate route`) 19 | 20 | ### Learn more 21 | 22 | - [Redux](redux.md) 23 | - [ImmutableJS](immutablejs.md) 24 | - [reselect](reselect.md) 25 | - [redux-saga](redux-saga.md) 26 | - [react-intl](i18n.md) 27 | - [routing](routing.md) 28 | 29 | ## Architecture: `components` and `containers` 30 | 31 | We adopted a split between stateless, reusable components called (wait for it...) 32 | `components` and stateful parent components called `containers`. 33 | 34 | ### Learn more 35 | 36 | See [this article](https://medium.com/@dan_abramov/smart-and-dumb-components-7ca2f9a7c7d0) 37 | by Dan Abramov for a great introduction to this approach. 38 | -------------------------------------------------------------------------------- /docs/js/immutablejs.md: -------------------------------------------------------------------------------- 1 | # ImmutableJS 2 | 3 | Immutable data structures can be deeply compared in no time. This allows us to 4 | efficiently determine if our components need to rerender since we know if the 5 | `props` changed or not! 6 | 7 | Check out the [official documentation](https://facebook.github.io/immutable-js/) 8 | for a good explanation of the more intricate benefits it has. 9 | 10 | ## Usage 11 | 12 | In our reducers, we make the initial state an immutable data structure with the 13 | `fromJS` function. We pass it an object or an array, and it takes care of 14 | converting it to a compatible one. (Note: the conversion is performed deeply so 15 | that even arbitrarily nested arrays/objects are immutable stuctures too!) 16 | 17 | ```JS 18 | import { fromJS } from 'immutable'; 19 | 20 | const initialState = fromJS({ 21 | myData: 'Hello World!', 22 | }); 23 | ``` 24 | 25 | To react to an incoming actions our reducers can use the `.set` and the `.setIn` 26 | functions. 27 | 28 | ```JS 29 | import { SOME_ACTION } from './actions'; 30 | 31 | // […] 32 | 33 | function myReducer(state = initialState, action) { 34 | switch (action.type) { 35 | case SOME_ACTION: 36 | return state.set('myData', action.payload); 37 | default: 38 | return state; 39 | } 40 | } 41 | ``` 42 | 43 | We use [`reselect`](./reselect.md) to efficiently cache our computed application 44 | state. Since that state is now immutable, we need to use the `.get` and `.getIn` 45 | functions to select the part we want. 46 | 47 | ```JS 48 | const myDataSelector = (state) => state.get('myData'); 49 | 50 | export default myDataSelector; 51 | ``` 52 | 53 | To learn more, check out [`reselect.md`](reselect.md)! 54 | 55 | ## Advanced Usage 56 | 57 | ImmutableJS provide many immutable structures like `Map`, `Set` and `List`. But the downside to using ImmutableJS data structures is that they are not normal JavaScript data structures. 58 | 59 | That means you must use getters to access properties : for instance you'll do `map.get("property")` instead of `map.property`, and `array.get(0)` instead of `array[0]`. It's not natural and your code becoming bigger, you finish by not knowing anymore if you are working with a JavaScript object or an Immutable one. While it's possible to be clear where you are using immutable objects, you still pass them through the system into places where it's not clear. This makes reasoning about functions harder. 60 | 61 | The `Record` structure tries to get rid of this drawback. `Record` is like a `Map` whose shape is fixed : you can't later add a new property after the record is created. The benefit of `Record` is that you can now, along with others .get, .set and .merge methods, use the dot notation to access properties, which is a good point to write simpler code. 62 | 63 | The creation of a record is less simple. You got to first create the `Record` shape. With the example above, to create your initial state, you'll write : 64 | 65 | ```JS 66 | //the shape 67 | const StateRecord = Record({ 68 | myData: 'Hello World!', 69 | }); 70 | 71 | const initialState = new StateRecord({}); // initialState is now a new StateRecord instance 72 | // initialized with myData set by default as 'Hello World!' 73 | ``` 74 | 75 | Now, if you want to access `myData`, you can just write `state.myData` in your reducer code. 76 | 77 | -------------------------------------------------------------------------------- /docs/js/redux-saga.md: -------------------------------------------------------------------------------- 1 | # `redux-saga` 2 | 3 | `redux-saga` is a library to manage side effects in your application. It works 4 | beautifully for data fetching, concurrent computations and a lot more. 5 | [Sebastien Lorber](https://twitter.com/sebastienlorber) put it best: 6 | 7 | > Imagine there is widget1 and widget2. When some button on widget1 is clicked, 8 | then it should have an effect on widget2. Instead of coupling the 2 widgets 9 | together (ie widget1 dispatch an action that targets widget2), widget1 only 10 | dispatch that its button was clicked. Then the saga listen for this button 11 | click and then update widget2 by dispaching a new event that widget2 is aware of. 12 | > 13 | > This adds a level of indirection that is unnecessary for simple apps, but make 14 | it more easy to scale complex applications. You can now publish widget1 and 15 | widget2 to different npm repositories so that they never have to know about 16 | each others, without having them to share a global registry of actions. The 2 17 | widgets are now bounded contexts that can live separately. They do not need 18 | each others to be consistent and can be reused in other apps as well. **The saga 19 | is the coupling point between the two widgets that coordinate them in a 20 | meaningful way for your business.** 21 | 22 | _Note: It is well worth reading the [source](https://stackoverflow.com/questions/34570758/why-do-we-need-middleware-for-async-flow-in-redux/34623840#34623840) 23 | of this quote in its entirety!_ 24 | 25 | To learn more about this amazing way to handle concurrent flows, start with the 26 | [official documentation](https://github.com/yelouafi/redux-saga) and explore 27 | some examples! (read [this comparison](https://stackoverflow.com/questions/34930735/pros-cons-of-using-redux-saga-with-es6-generators-vs-redux-thunk-with-es7-async/34933395) if you're used to `redux-thunk`) 28 | 29 | ## Usage 30 | 31 | Sagas are associated with a container, just like actions, constants, selectors 32 | and reducers. If your container already has a `sagas.js` file, simply add your 33 | saga to that. If your container does not yet have a `sagas.js` file, add one with 34 | this boilerplate structure: 35 | 36 | ```JS 37 | import { take, call, put, select } from 'redux-saga/effects'; 38 | 39 | // Your sagas for this container 40 | export default [ 41 | sagaName, 42 | ]; 43 | 44 | // Individual exports for testing 45 | export function* sagaName() { 46 | 47 | } 48 | ``` 49 | 50 | Then, in your `routes.js`, add injection for the newly added saga: 51 | 52 | ```JS 53 | getComponent(nextState, cb) { 54 | const importModules = Promise.all([ 55 | System.import('containers/YourComponent/reducer'), 56 | System.import('containers/YourComponent/sagas'), 57 | System.import('containers/YourComponent'), 58 | ]); 59 | 60 | const renderRoute = loadModule(cb); 61 | 62 | importModules.then(([reducer, sagas, component]) => { 63 | injectReducer('home', reducer.default); 64 | injectSagas(sagas.default); // Inject the saga 65 | 66 | renderRoute(component); 67 | }); 68 | 69 | importModules.catch(errorLoading); 70 | }, 71 | ``` 72 | 73 | Now add as many sagas to your `sagas.js` file as you want! 74 | 75 | --- 76 | 77 | _Don't like this feature? [Click here](remove.md)_ 78 | -------------------------------------------------------------------------------- /docs/js/redux.md: -------------------------------------------------------------------------------- 1 | # Redux 2 | 3 | If you haven't worked with Redux, it's highly recommended (possibly indispensable!) 4 | to read through the (amazing) [official documentation](http://redux.js.org) 5 | and/or watch this [free video tutorial series](https://egghead.io/series/getting-started-with-redux). 6 | 7 | ## Usage 8 | 9 | See above! As minimal as Redux is, the challenge it addresses - app state 10 | management - is a complex topic that is too involved to properly discuss here. 11 | 12 | ## Removing redux 13 | 14 | There are a few reasons why we chose to bundle redux with React Boilerplate, the 15 | biggest being that it is widely regarded as the current best Flux implementation 16 | in terms of architecture, support and documentation. 17 | 18 | You may feel differently! This is completely OK :) 19 | 20 | Below are a few reasons you might want to remove it: 21 | 22 | ### I'm just getting started and Flux is hard 23 | 24 | You're under no obligation to use Redux or any other Flux library! The complexity 25 | of your application will determine the point at which you need to introduce it. 26 | 27 | Here are a couple of great resources for taking a minimal approach: 28 | 29 | - [Misconceptions of Tooling in JavaScript](http://javascriptplayground.com/blog/2016/02/the-react-webpack-tooling-problem) 30 | - [Learn Raw React — no JSX, no Flux, no ES6, no Webpack…](http://jamesknelson.com/learn-raw-react-no-jsx-flux-es6-webpack/) 31 | 32 | ### It's overkill for my project! 33 | 34 | See above. 35 | 36 | ### I prefer `(Alt|MobX|SomethingElse)`! 37 | 38 | React Boilerplate is a baseline for _your_ app: go for it! 39 | 40 | If you feel that we should take a closer look at supporting your preference 41 | out of the box, please let us know. 42 | -------------------------------------------------------------------------------- /docs/js/remove.md: -------------------------------------------------------------------------------- 1 | ## Removing `redux-saga` 2 | 3 | **We don't recommend removing `redux-saga`**, as we strongly feel that it's the 4 | way to go for most redux based applications. 5 | 6 | If you really want to get rid of it, delete the `sagas/` folder, remove the 7 | `import` and the `sagaMiddleware` from the `store.js` and finally remove it from 8 | the `package.json`. Then you should be good to go with whatever side-effect 9 | management library you want to use! 10 | 11 | ## Removing `reselect` 12 | 13 | To remove `reselect`, delete the `app/selectors` folder, remove it from your 14 | dependencies in `package.json` and then write your `mapStateToProps` functions 15 | like you normally would! 16 | 17 | You'll also need to hook up the history directly to the store. Change the const 18 | `history` in `app/app.js` to the following: 19 | 20 | ```js 21 | const history = syncHistoryWithStore(browserHistory, store, { 22 | selectLocationState: (state) => state.get('route').toJS(), 23 | }); 24 | ``` 25 | -------------------------------------------------------------------------------- /docs/js/reselect.md: -------------------------------------------------------------------------------- 1 | # `reselect` 2 | 3 | reselect memoizes ("caches") previous state trees and calculations based on said 4 | tree. This means repeated changes and calculations are fast and efficient, 5 | providing us with a performance boost over standard `mapStateToProps` 6 | implementations. 7 | 8 | The [official documentation](https://github.com/reactjs/reselect) 9 | offers a good starting point! 10 | 11 | ## Usage 12 | 13 | There are two different kinds of selectors, simple and complex ones. 14 | 15 | ### Simple selectors 16 | 17 | Simple selectors are just that: they take the application state and select a 18 | part of it. 19 | 20 | ```javascript 21 | const mySelector = (state) => state.get('someState'); 22 | 23 | export { 24 | mySelector, 25 | }; 26 | ``` 27 | 28 | ### Complex selectors 29 | 30 | If we need to, we can combine simple selectors to build more complex ones which 31 | get nested state parts with reselects `createSelector` function. We import other 32 | selectors and pass them to the `createSelector` call: 33 | 34 | ```javascript 35 | import { createSelector } from 'reselect'; 36 | import mySelector from 'mySelector'; 37 | 38 | const myComplexSelector = createSelector( 39 | mySelector, 40 | (myState) => myState.get('someNestedState') 41 | ); 42 | 43 | export { 44 | myComplexSelector, 45 | }; 46 | ``` 47 | 48 | These selectors can then either be used directly in our containers as 49 | `mapStateToProps` functions or be nested with `createSelector` once again: 50 | 51 | ```javascript 52 | export default connect(createSelector( 53 | myComplexSelector, 54 | (myNestedState) => ({ data: myNestedState }) 55 | ))(SomeComponent); 56 | ``` 57 | 58 | ### Adding a new selector 59 | 60 | If you have a `selectors.js` file next to the reducer which's part of the state 61 | you want to select, add your selector to said file. If you don't have one yet, 62 | add a new one into your container folder and fill it with this boilerplate code: 63 | 64 | ```JS 65 | import { createSelector } from 'reselect'; 66 | 67 | const selectMyState = () => createSelector( 68 | 69 | ); 70 | 71 | export { 72 | selectMyState, 73 | }; 74 | ``` 75 | 76 | --- 77 | 78 | _Don't like this feature? [Click here](remove.md)_ 79 | -------------------------------------------------------------------------------- /docs/testing/README.md: -------------------------------------------------------------------------------- 1 | # Testing 2 | 3 | - [Unit Testing](unit-testing.md) 4 | - [Component Testing](component-testing.md) 5 | - [Remote Testing](remote-testing.md) 6 | 7 | Testing your application is a vital part of serious development. There are a few 8 | things you should test. If you've never done this before start with [unit testing](unit-testing.md). 9 | Move on to [component testing](component-testing.md) when you feel like you 10 | understand that! 11 | 12 | We also support [remote testing](remote-testing.md) your local application, 13 | which is quite awesome, so definitely check that out! 14 | 15 | ## Usage with this boilerplate 16 | 17 | To test your application started with this boilerplate do the following: 18 | 19 | 1. Sprinkle `.test.js` files directly next to the parts of your application you 20 | want to test. (Or in `test/` subdirectories, it doesn't really matter as long 21 | as they are directly next to those parts and end in `.test.js`) 22 | 23 | 1. Write your unit and component tests in those files. 24 | 25 | 1. Run `npm run test` in your terminal and see all the tests pass! (hopefully) 26 | 27 | There are a few more commands related to testing, checkout the [commands documentation](../general/commands.md#testing) 28 | for the full list! 29 | -------------------------------------------------------------------------------- /docs/testing/remote-testing.md: -------------------------------------------------------------------------------- 1 | # Remote testing 2 | 3 | ```Shell 4 | npm run start:tunnel 5 | ``` 6 | 7 | This command will start a server and tunnel it with `ngrok`. You'll get a URL 8 | that looks a bit like this: `http://abcdef.ngrok.com` 9 | 10 | This URL will show the version of your application that's in the `build` folder, 11 | and it's accessible from the entire world! This is great for testing on different 12 | devices and from different locations! 13 | -------------------------------------------------------------------------------- /internals/config.js: -------------------------------------------------------------------------------- 1 | const resolve = require('path').resolve; 2 | const pullAll = require('lodash/pullAll'); 3 | const uniq = require('lodash/uniq'); 4 | 5 | const ReactBoilerplate = { 6 | // This refers to the react-boilerplate version this project is based on. 7 | version: '3.1.0', 8 | 9 | /** 10 | * The DLL Plugin provides a dramatic speed increase to webpack build and hot module reloading 11 | * by caching the module metadata for all of our npm dependencies. We enable it by default 12 | * in development. 13 | * 14 | * 15 | * To disable the DLL Plugin, set this value to false. 16 | */ 17 | dllPlugin: { 18 | defaults: { 19 | /** 20 | * we need to exclude dependencies which are not intended for the browser 21 | * by listing them here. 22 | */ 23 | exclude: [ 24 | 'chalk', 25 | 'compression', 26 | 'cross-env', 27 | 'express', 28 | 'ip', 29 | 'minimist', 30 | 'sanitize.css', 31 | ], 32 | 33 | /** 34 | * Specify any additional dependencies here. We include core-js and lodash 35 | * since a lot of our dependencies depend on them and they get picked up by webpack. 36 | */ 37 | include: ['core-js', 'eventsource-polyfill', 'babel-polyfill', 'lodash'], 38 | 39 | // The path where the DLL manifest and bundle will get built 40 | path: resolve('../node_modules/react-boilerplate-dlls'), 41 | }, 42 | 43 | entry(pkg) { 44 | const dependencyNames = Object.keys(pkg.dependencies); 45 | const exclude = pkg.dllPlugin.exclude || ReactBoilerplate.dllPlugin.defaults.exclude; 46 | const include = pkg.dllPlugin.include || ReactBoilerplate.dllPlugin.defaults.include; 47 | const includeDependencies = uniq(dependencyNames.concat(include)); 48 | 49 | return { 50 | reactBoilerplateDeps: pullAll(includeDependencies, exclude), 51 | }; 52 | }, 53 | }, 54 | }; 55 | 56 | module.exports = ReactBoilerplate; 57 | -------------------------------------------------------------------------------- /internals/ethdeploy/ethdeploy.base.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | const contracts = require('../../app/contracts/build/classes.json'); 3 | const environments = require('../../app/contracts/build/environments.json'); 4 | 5 | // new entry point 6 | var entry = { 7 | testrpc: contracts, 8 | testnet: contracts, 9 | livenet: contracts, 10 | }; 11 | 12 | // use environments 13 | if (typeof environments !== 'object') { 14 | if (Object.keys(environments).length !== 0) { 15 | entry = environments; 16 | } 17 | } 18 | 19 | // main module export 20 | module.exports = { 21 | entry: entry, 22 | config: { 23 | defaultAccount: 0, 24 | defaultGas: 3000000, 25 | environments: { 26 | testrpc: { 27 | provider: { 28 | type: 'http', 29 | host: 'http://localhost', 30 | port: 8545, 31 | }, 32 | }, 33 | testnet: { 34 | provider: { 35 | type: 'zero-client', 36 | getAccounts: function(cb) { 37 | cb(null, ['0x2233eD250Ea774146B0fBbC1da0Ffa6a81514cCC']); 38 | }, 39 | signTransaction: function(rawTx, cb) { 40 | const privateKey = new Buffer('', 'hex'); 41 | 42 | const tx = new Tx(rawTx); 43 | tx.sign(privateKey); 44 | 45 | cb(null, ethUtil.bufferToHex(tx.serialize())); 46 | }, 47 | host: 'https://morden.infura.io', 48 | port: 8545, 49 | } 50 | }, 51 | livenet: { 52 | provider: { 53 | type: 'zero-client', 54 | getAccounts: function(cb) { 55 | cb(null, ['0x2233eD250Ea774146B0fBbC1da0Ffa6a81514cCC']); 56 | }, 57 | signTransaction: function(rawTx, cb) { 58 | const privateKey = new Buffer('', 'hex'); 59 | 60 | const tx = new Tx(rawTx); 61 | tx.sign(privateKey); 62 | 63 | cb(null, ethUtil.bufferToHex(tx.serialize())); 64 | }, 65 | host: 'https://livenet.infura.io', 66 | port: 8545, 67 | } 68 | }, 69 | }, 70 | }, 71 | }; 72 | -------------------------------------------------------------------------------- /internals/ethdeploy/ethdeploy.livenet.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | const base = require('./ethdeploy.base.js'); 3 | 4 | // main module export 5 | module.exports = Object.assign(base, { 6 | output: { 7 | environment: 'livenet', 8 | }, 9 | module: function(deploy, contracts){ 10 | deploy(contracts.SimpleStoreRegistry).then(function(simpleStoreRegistry){ 11 | deploy(contracts.SimpleStoreFactory, simpleStoreRegistry.address); 12 | }); 13 | }, 14 | }); 15 | -------------------------------------------------------------------------------- /internals/ethdeploy/ethdeploy.testnet.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | const base = require('./ethdeploy.base.js'); 3 | 4 | // main module export 5 | module.exports = Object.assign(base, { 6 | output: { 7 | environment: 'testnet', 8 | }, 9 | module: function(deploy, contracts){ 10 | deploy(contracts.SimpleStoreRegistry).then(function(simpleStoreRegistry){ 11 | deploy(contracts.SimpleStoreFactory, simpleStoreRegistry.address); 12 | }); 13 | }, 14 | }); 15 | -------------------------------------------------------------------------------- /internals/ethdeploy/ethdeploy.testrpc.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | const base = require('./ethdeploy.base.js'); 3 | 4 | // main module export 5 | module.exports = Object.assign(base, { 6 | output: { 7 | environment: 'testrpc', 8 | }, 9 | module: function(deploy, contracts){ 10 | deploy(contracts.SimpleStoreRegistry).then(function(simpleStoreRegistry){ 11 | deploy(contracts.SimpleStoreFactory, simpleStoreRegistry.address); 12 | }); 13 | }, 14 | }); 15 | -------------------------------------------------------------------------------- /internals/generators/component/es6.js.hbs: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * {{ properCase name }} 4 | * 5 | */ 6 | 7 | import React from 'react'; 8 | 9 | {{#if wantMessages}} 10 | import { FormattedMessage } from 'react-intl'; 11 | import messages from './messages'; 12 | {{/if}} 13 | {{#if wantCSS}} 14 | import styles from './styles.css'; 15 | {{/if}} 16 | 17 | class {{ properCase name }} extends React.Component { // eslint-disable-line react/prefer-stateless-function 18 | render() { 19 | return ( 20 | {{#if wantCSS}} 21 |
22 | {{else}} 23 |
24 | {{/if}} 25 | {{#if wantMessages}} 26 | 27 | {{/if}} 28 |
29 | ); 30 | } 31 | } 32 | 33 | export default {{ properCase name }}; 34 | -------------------------------------------------------------------------------- /internals/generators/component/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Component Generator 3 | */ 4 | 5 | const componentExists = require('../utils/componentExists'); 6 | 7 | module.exports = { 8 | description: 'Add an unconnected component', 9 | prompts: [{ 10 | type: 'list', 11 | name: 'type', 12 | message: 'Select the type of component', 13 | default: 'Stateless Function', 14 | choices: () => ['ES6 Class', 'Stateless Function'], 15 | }, { 16 | type: 'input', 17 | name: 'name', 18 | message: 'What should it be called?', 19 | default: 'Button', 20 | validate: value => { 21 | if ((/.+/).test(value)) { 22 | return componentExists(value) ? 'A component or container with this name already exists' : true; 23 | } 24 | 25 | return 'The name is required'; 26 | }, 27 | }, { 28 | type: 'confirm', 29 | name: 'wantCSS', 30 | default: true, 31 | message: 'Does it have styling?', 32 | }, { 33 | type: 'confirm', 34 | name: 'wantMessages', 35 | default: true, 36 | message: 'Do you want i18n messages (i.e. will this component use text)?', 37 | }], 38 | actions: data => { 39 | // Generate index.js and index.test.js 40 | const actions = [{ 41 | type: 'add', 42 | path: '../../app/components/{{properCase name}}/index.js', 43 | templateFile: data.type === 'ES6 Class' ? './component/es6.js.hbs' : './component/stateless.js.hbs', 44 | abortOnFail: true, 45 | }, { 46 | type: 'add', 47 | path: '../../app/components/{{properCase name}}/tests/index.test.js', 48 | templateFile: './component/test.js.hbs', 49 | abortOnFail: true, 50 | }]; 51 | 52 | // If they want a CSS file, add styles.css 53 | if (data.wantCSS) { 54 | actions.push({ 55 | type: 'add', 56 | path: '../../app/components/{{properCase name}}/styles.css', 57 | templateFile: './component/styles.css.hbs', 58 | abortOnFail: true, 59 | }); 60 | } 61 | 62 | // If they want a i18n messages file 63 | if (data.wantMessages) { 64 | actions.push({ 65 | type: 'add', 66 | path: '../../app/components/{{properCase name}}/messages.js', 67 | templateFile: './component/messages.js.hbs', 68 | abortOnFail: true, 69 | }); 70 | } 71 | 72 | return actions; 73 | }, 74 | }; 75 | -------------------------------------------------------------------------------- /internals/generators/component/messages.js.hbs: -------------------------------------------------------------------------------- 1 | /* 2 | * {{ properCase name }} Messages 3 | * 4 | * This contains all the text for the {{ properCase name }} component. 5 | */ 6 | import { defineMessages } from 'react-intl'; 7 | 8 | export default defineMessages({ 9 | header: { 10 | id: 'app.components.{{ properCase name }}.header', 11 | defaultMessage: 'This is the {{ properCase name}} component !', 12 | }, 13 | }); 14 | -------------------------------------------------------------------------------- /internals/generators/component/stateless.js.hbs: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * {{ properCase name }} 4 | * 5 | */ 6 | 7 | import React from 'react'; 8 | 9 | {{#if wantMessages}} 10 | import { FormattedMessage } from 'react-intl'; 11 | import messages from './messages'; 12 | {{/if}} 13 | 14 | {{#if wantCSS}} 15 | import styles from './styles.css'; 16 | {{/if}} 17 | 18 | function {{ properCase name }}() { 19 | return ( 20 | {{#if wantCSS}} 21 |
22 | {{else}} 23 |
24 | {{/if}} 25 | {{#if wantMessages}} 26 | 27 | {{/if}} 28 |
29 | ); 30 | } 31 | 32 | export default {{ properCase name }}; 33 | -------------------------------------------------------------------------------- /internals/generators/component/styles.css.hbs: -------------------------------------------------------------------------------- 1 | .{{ camelCase name }} { /* stylelint-disable */ 2 | 3 | } 4 | -------------------------------------------------------------------------------- /internals/generators/component/test.js.hbs: -------------------------------------------------------------------------------- 1 | // import {{ properCase name }} from '../index'; 2 | 3 | import expect from 'expect'; 4 | // import { shallow } from 'enzyme'; 5 | // import React from 'react'; 6 | 7 | describe('<{{ properCase name }} />', () => { 8 | it('Expect to have unit tests specified', () => { 9 | expect(true).toEqual(false); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /internals/generators/container/actions.js.hbs: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * {{ properCase name }} actions 4 | * 5 | */ 6 | 7 | import { 8 | DEFAULT_ACTION, 9 | } from './constants'; 10 | 11 | export function defaultAction() { 12 | return { 13 | type: DEFAULT_ACTION, 14 | }; 15 | } 16 | -------------------------------------------------------------------------------- /internals/generators/container/actions.test.js.hbs: -------------------------------------------------------------------------------- 1 | import expect from 'expect'; 2 | import { 3 | defaultAction, 4 | } from '../actions'; 5 | import { 6 | DEFAULT_ACTION, 7 | } from '../constants'; 8 | 9 | describe('{{ properCase name }} actions', () => { 10 | describe('Default Action', () => { 11 | it('has a type of DEFAULT_ACTION', () => { 12 | const expected = { 13 | type: DEFAULT_ACTION, 14 | }; 15 | expect(defaultAction()).toEqual(expected); 16 | }); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /internals/generators/container/constants.js.hbs: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * {{ properCase name }} constants 4 | * 5 | */ 6 | 7 | export const DEFAULT_ACTION = 'app/{{ properCase name }}/DEFAULT_ACTION'; 8 | -------------------------------------------------------------------------------- /internals/generators/container/index.js.hbs: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * {{properCase name }} 4 | * 5 | */ 6 | 7 | import React from 'react'; 8 | import { connect } from 'react-redux'; 9 | {{#if wantHeaders}} 10 | import Helmet from 'react-helmet'; 11 | {{/if}} 12 | {{#if wantActionsAndReducer}} 13 | import select{{properCase name}} from './selectors'; 14 | {{/if}} 15 | {{#if wantMessages}} 16 | import { FormattedMessage } from 'react-intl'; 17 | import messages from './messages'; 18 | {{/if}} 19 | {{#if wantCSS}} 20 | import styles from './styles.css'; 21 | {{/if}} 22 | 23 | export class {{ properCase name }} extends React.Component { // eslint-disable-line react/prefer-stateless-function 24 | render() { 25 | return ( 26 | {{#if wantCSS}} 27 |
28 | {{else}} 29 |
30 | {{/if}} 31 | {{#if wantHeaders}} 32 | 38 | {{/if}} 39 | {{#if wantMessages}} 40 | 41 | {{/if}} 42 |
43 | ); 44 | } 45 | } 46 | 47 | {{#if wantActionsAndReducer}} 48 | const mapStateToProps = select{{properCase name}}(); 49 | {{/if}} 50 | 51 | function mapDispatchToProps(dispatch) { 52 | return { 53 | dispatch, 54 | }; 55 | } 56 | 57 | {{#if wantActionsAndReducer}} 58 | export default connect(mapStateToProps, mapDispatchToProps)({{ properCase name }}); 59 | {{else}} 60 | export default connect(mapDispatchToProps)({{ properCase name }}); 61 | {{/if}} 62 | -------------------------------------------------------------------------------- /internals/generators/container/messages.js.hbs: -------------------------------------------------------------------------------- 1 | /* 2 | * {{properCase name }} Messages 3 | * 4 | * This contains all the text for the {{properCase name }} component. 5 | */ 6 | import { defineMessages } from 'react-intl'; 7 | 8 | export default defineMessages({ 9 | header: { 10 | id: 'app.containers.{{properCase name }}.header', 11 | defaultMessage: 'This is {{properCase name}} container !', 12 | }, 13 | }); 14 | -------------------------------------------------------------------------------- /internals/generators/container/reducer.js.hbs: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * {{ properCase name }} reducer 4 | * 5 | */ 6 | 7 | import { fromJS } from 'immutable'; 8 | import { 9 | DEFAULT_ACTION, 10 | } from './constants'; 11 | 12 | const initialState = fromJS({}); 13 | 14 | function {{ camelCase name }}Reducer(state = initialState, action) { 15 | switch (action.type) { 16 | case DEFAULT_ACTION: 17 | return state; 18 | default: 19 | return state; 20 | } 21 | } 22 | 23 | export default {{ camelCase name }}Reducer; 24 | -------------------------------------------------------------------------------- /internals/generators/container/reducer.test.js.hbs: -------------------------------------------------------------------------------- 1 | import expect from 'expect'; 2 | import {{ camelCase name }}Reducer from '../reducer'; 3 | import { fromJS } from 'immutable'; 4 | 5 | describe('{{ camelCase name }}Reducer', () => { 6 | it('returns the initial state', () => { 7 | expect({{ camelCase name }}Reducer(undefined, {})).toEqual(fromJS({})); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /internals/generators/container/sagas.js.hbs: -------------------------------------------------------------------------------- 1 | // import { take, call, put, select } from 'redux-saga/effects'; 2 | 3 | // Individual exports for testing 4 | export function* defaultSaga() { 5 | return; 6 | } 7 | 8 | // All sagas to be loaded 9 | export default [ 10 | defaultSaga, 11 | ]; 12 | -------------------------------------------------------------------------------- /internals/generators/container/sagas.test.js.hbs: -------------------------------------------------------------------------------- 1 | /** 2 | * Test sagas 3 | */ 4 | 5 | import expect from 'expect'; 6 | // import { take, call, put, select } from 'redux-saga/effects'; 7 | // import { defaultSaga } from '../sagas'; 8 | 9 | // const generator = defaultSaga(); 10 | 11 | describe('defaultSaga Saga', () => { 12 | it('Expect to have unit tests specified', () => { 13 | expect(true).toEqual(false); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /internals/generators/container/selectors.js.hbs: -------------------------------------------------------------------------------- 1 | import { createSelector } from 'reselect'; 2 | 3 | /** 4 | * Direct selector to the {{ camelCase name }} state domain 5 | */ 6 | const select{{ properCase name }}Domain = () => state => state.get('{{ camelCase name }}'); 7 | 8 | /** 9 | * Other specific selectors 10 | */ 11 | 12 | 13 | /** 14 | * Default selector used by {{ properCase name }} 15 | */ 16 | 17 | const select{{ properCase name }} = () => createSelector( 18 | select{{ properCase name }}Domain(), 19 | (substate) => substate.toJS() 20 | ); 21 | 22 | export default select{{ properCase name }}; 23 | export { 24 | select{{ properCase name }}Domain, 25 | }; 26 | -------------------------------------------------------------------------------- /internals/generators/container/selectors.test.js.hbs: -------------------------------------------------------------------------------- 1 | // import { select{{ properCase name }}Domain } from '../selectors'; 2 | // import { fromJS } from 'immutable'; 3 | import expect from 'expect'; 4 | 5 | // const selector = select{{ properCase name}}Domain(); 6 | 7 | describe('select{{ properCase name }}Domain', () => { 8 | it('Expect to have unit tests specified', () => { 9 | expect('Test case').toEqual(false); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /internals/generators/container/styles.css.hbs: -------------------------------------------------------------------------------- 1 | .{{ camelCase name }} { /* stylelint-disable */ 2 | 3 | } 4 | -------------------------------------------------------------------------------- /internals/generators/container/test.js.hbs: -------------------------------------------------------------------------------- 1 | // import {{ properCase name }} from '../index'; 2 | 3 | import expect from 'expect'; 4 | // import { shallow } from 'enzyme'; 5 | // import React from 'react'; 6 | 7 | describe('<{{ properCase name }} />', () => { 8 | it('Expect to have unit tests specified', () => { 9 | expect(true).toEqual(false); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /internals/generators/contract/contract.sol.hbs: -------------------------------------------------------------------------------- 1 | 2 | 3 | contract {{ properCase name }} { 4 | function someMethod() returns (bool) { 5 | return true; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /internals/generators/contract/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Component Generator 3 | */ 4 | 5 | const componentExists = require('../utils/componentExists'); 6 | 7 | module.exports = { 8 | description: 'Add a Ethereum Solidity contract', 9 | prompts: [{ 10 | type: 'list', 11 | name: 'type', 12 | message: 'Select the type of contract', 13 | default: 'Stateless Function', 14 | choices: () => ['Contract', 'Library'], 15 | }, { 16 | type: 'input', 17 | name: 'name', 18 | message: 'What should it be called?', 19 | default: 'SimpleStore', 20 | validate: value => { 21 | if ((/.+/).test(value)) { 22 | return componentExists(value) ? 'A contract or library already exists with that name' : true; 23 | } 24 | 25 | return 'The name is required'; 26 | }, 27 | }], 28 | actions: data => { 29 | // Generate index.js and index.test.js 30 | const actions = [{ 31 | type: 'add', 32 | path: '../../app/contracts/{{properCase name}}.sol', 33 | templateFile: data.type === 'Contract' ? './contract/contract.sol.hbs' : './contract/library.sol.hbs', 34 | abortOnFail: true, 35 | }, { 36 | type: 'add', 37 | path: '../../app/contracts/tests/test.{{properCase name}}.sol', 38 | templateFile: './contract/test.contract.sol.hbs', 39 | abortOnFail: true, 40 | }, { 41 | type: 'add', 42 | path: '../../app/contracts/tests/test.{{properCase name}}.js', 43 | templateFile: './contract/test.contract.js.hbs', 44 | abortOnFail: true, 45 | }]; 46 | 47 | return actions; 48 | }, 49 | }; 50 | -------------------------------------------------------------------------------- /internals/generators/contract/library.sol.hbs: -------------------------------------------------------------------------------- 1 | 2 | 3 | library {{ properCase name }} { 4 | function someMethod() returns (bool) { 5 | return true; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /internals/generators/contract/test.contract.js.hbs: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /* globals describe, it, before */ 3 | 4 | import expect from 'expect'; 5 | const contracts = require('../build/classes.json'); 6 | const chaithereum = require('chaithereum'); 7 | 8 | before(() => chaithereum.promise); 9 | 10 | describe('{{ properCase name }}', () => { 11 | let {{ properCase name }}; 12 | 13 | it('successfully instantiates with blank params', () => { 14 | return chaithereum.web3.eth.contract(JSON.parse(contracts.{{ properCase name }}.interface)).new.q({ data: contracts.{{ properCase name }}.bytecode }).should.eventually.be.contract.then((_{{ properCase name }}) => { 15 | {{ properCase name }} = _{{ properCase name }}; 16 | }).should.eventually.be.fulfilled; 17 | }); 18 | 19 | it('successfully instantiates with blank params', () => { 20 | expect(true).toEqual(false); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /internals/generators/contract/test.contract.sol.hbs: -------------------------------------------------------------------------------- 1 | import "dapple/test.sol"; 2 | import "{{ properCase name }}.sol"; 3 | 4 | contract {{ properCase name }}Test is Test { 5 | {{ properCase name }} target; 6 | 7 | functon test{{ properCase name }}Method() { 8 | assertEq(1, 0); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /internals/generators/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * generator/index.js 3 | * 4 | * Exports the generators so plop knows them 5 | */ 6 | 7 | const fs = require('fs'); 8 | const componentGenerator = require('./component/index.js'); 9 | const containerGenerator = require('./container/index.js'); 10 | const routeGenerator = require('./route/index.js'); 11 | const languageGenerator = require('./language/index.js'); 12 | const contractGenerator = require('./contract/index.js'); 13 | 14 | module.exports = (plop) => { 15 | plop.setGenerator('component', componentGenerator); 16 | plop.setGenerator('container', containerGenerator); 17 | plop.setGenerator('route', routeGenerator); 18 | plop.setGenerator('language', languageGenerator); 19 | plop.setGenerator('contract', contractGenerator); 20 | plop.addHelper('directory', (comp) => { 21 | try { 22 | fs.accessSync(`app/containers/${comp}`, fs.F_OK); 23 | return `containers/${comp}`; 24 | } catch (e) { 25 | return `components/${comp}`; 26 | } 27 | }); 28 | plop.addHelper('curly', (object, open) => (open ? '{' : '}')); 29 | }; 30 | -------------------------------------------------------------------------------- /internals/generators/language/add-locale-data.hbs: -------------------------------------------------------------------------------- 1 | $1addLocaleData({{language}}LocaleData); 2 | -------------------------------------------------------------------------------- /internals/generators/language/app-locale.hbs: -------------------------------------------------------------------------------- 1 | $1 2 | '{{language}}', 3 | -------------------------------------------------------------------------------- /internals/generators/language/format-translation-messages.hbs: -------------------------------------------------------------------------------- 1 | $1 {{language}}: formatTranslationMessages({{language}}TranslationMessages), 2 | -------------------------------------------------------------------------------- /internals/generators/language/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Language Generator 3 | */ 4 | const exec = require('child_process').exec; 5 | 6 | module.exports = { 7 | description: 'Add a langauge', 8 | prompts: [{ 9 | type: 'input', 10 | name: 'language', 11 | message: 'What is the language you want to add i18n support for (e.g. "fr", "de")?', 12 | default: 'fr', 13 | validate: value => { 14 | if ((/.+/).test(value) && value.length === 2) { 15 | return true; 16 | } 17 | 18 | return '2 character language specifier is required'; 19 | }, 20 | }], 21 | 22 | actions: () => { 23 | const actions = []; 24 | actions.push({ 25 | type: 'modify', 26 | path: '../../app/i18n.js', 27 | pattern: /('react-intl\/locale-data\/[a-z]+';\n)(?!.*'react-intl\/locale-data\/[a-z]+';)/g, 28 | templateFile: './language/intl-locale-data.hbs', 29 | }); 30 | actions.push({ 31 | type: 'modify', 32 | path: '../../app/i18n.js', 33 | pattern: /([\n\s'[a-z]+',)(?!.*[\n\s'[a-z]+',)/g, 34 | templateFile: './language/app-locale.hbs', 35 | }); 36 | actions.push({ 37 | type: 'modify', 38 | path: '../../app/i18n.js', 39 | pattern: /(from\s'.\/translations\/[a-z]+.json';\n)(?!.*from\s'.\/translations\/[a-z]+.json';)/g, 40 | templateFile: './language/translation-messages.hbs', 41 | }); 42 | actions.push({ 43 | type: 'modify', 44 | path: '../../app/i18n.js', 45 | pattern: /(addLocaleData\([a-z]+LocaleData\);\n)(?!.*addLocaleData\([a-z]+LocaleData\);)/g, 46 | templateFile: './language/add-locale-data.hbs', 47 | }); 48 | actions.push({ 49 | type: 'modify', 50 | path: '../../app/i18n.js', 51 | pattern: /([a-z]+:\sformatTranslationMessages\([a-z]+TranslationMessages\),\n)(?!.*[a-z]+:\sformatTranslationMessages\([a-z]+TranslationMessages\),)/g, 52 | templateFile: './language/format-translation-messages.hbs', 53 | }); 54 | actions.push({ 55 | type: 'add', 56 | path: '../../app/translations/{{language}}.json', 57 | templateFile: './language/translations-json.hbs', 58 | abortOnFail: true, 59 | }); 60 | actions.push({ 61 | type: 'modify', 62 | path: '../../app/app.js', 63 | pattern: /(System\.import\('intl\/locale-data\/jsonp\/[a-z]+\.js'\),\n)(?!.*System\.import\('intl\/locale-data\/jsonp\/[a-z]+\.js'\),)/g, 64 | templateFile: './language/polyfill-intl-locale.hbs', 65 | }); 66 | actions.push( 67 | () => { 68 | const cmd = 'npm run extract-intl'; 69 | exec(cmd, (err, result, stderr) => { 70 | if (err || stderr) { 71 | throw err || stderr; 72 | } 73 | process.stdout.write(result); 74 | }); 75 | } 76 | ); 77 | 78 | return actions; 79 | }, 80 | }; 81 | -------------------------------------------------------------------------------- /internals/generators/language/intl-locale-data.hbs: -------------------------------------------------------------------------------- 1 | $1import {{language}}LocaleData from 'react-intl/locale-data/{{language}}'; 2 | -------------------------------------------------------------------------------- /internals/generators/language/polyfill-intl-locale.hbs: -------------------------------------------------------------------------------- 1 | $1 System.import('intl/locale-data/jsonp/{{language}}.js'), 2 | -------------------------------------------------------------------------------- /internals/generators/language/translation-messages.hbs: -------------------------------------------------------------------------------- 1 | $1import {{language}}TranslationMessages from './translations/{{language}}.json'; 2 | -------------------------------------------------------------------------------- /internals/generators/language/translations-json.hbs: -------------------------------------------------------------------------------- 1 | [] 2 | -------------------------------------------------------------------------------- /internals/generators/route/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Route Generator 3 | */ 4 | const fs = require('fs'); 5 | const componentExists = require('../utils/componentExists'); 6 | 7 | function reducerExists(comp) { 8 | try { 9 | fs.accessSync(`app/containers/${comp}/reducer.js`, fs.F_OK); 10 | return true; 11 | } catch (e) { 12 | return false; 13 | } 14 | } 15 | 16 | function sagasExists(comp) { 17 | try { 18 | fs.accessSync(`app/containers/${comp}/sagas.js`, fs.F_OK); 19 | return true; 20 | } catch (e) { 21 | return false; 22 | } 23 | } 24 | 25 | function trimTemplateFile(template) { 26 | // Loads the template file and trims the whitespace and then returns the content as a string. 27 | return fs.readFileSync(`internals/generators/route/${template}`, 'utf8').replace(/\s*$/, ''); 28 | } 29 | 30 | module.exports = { 31 | description: 'Add a route', 32 | prompts: [{ 33 | type: 'input', 34 | name: 'component', 35 | message: 'Which component should the route show?', 36 | validate: value => { 37 | if ((/.+/).test(value)) { 38 | return componentExists(value) ? true : `"${value}" doesn't exist.`; 39 | } 40 | 41 | return 'The path is required'; 42 | }, 43 | }, { 44 | type: 'input', 45 | name: 'path', 46 | message: 'Enter the path of the route.', 47 | default: '/about', 48 | validate: value => { 49 | if ((/.+/).test(value)) { 50 | return true; 51 | } 52 | 53 | return 'path is required'; 54 | }, 55 | }], 56 | 57 | // Add the route to the routes.js file above the error route 58 | // TODO smarter route adding 59 | actions: data => { 60 | const actions = []; 61 | if (reducerExists(data.component)) { 62 | data.useSagas = sagasExists(data.component); // eslint-disable-line no-param-reassign 63 | actions.push({ 64 | type: 'modify', 65 | path: '../../app/routes.js', 66 | pattern: /(\s{\n\s{0,}path: '\*',)/g, 67 | template: trimTemplateFile('routeWithReducer.hbs'), 68 | }); 69 | } else { 70 | actions.push({ 71 | type: 'modify', 72 | path: '../../app/routes.js', 73 | pattern: /(\s{\n\s{0,}path: '\*',)/g, 74 | template: trimTemplateFile('route.hbs'), 75 | }); 76 | } 77 | 78 | return actions; 79 | }, 80 | }; 81 | -------------------------------------------------------------------------------- /internals/generators/route/route.hbs: -------------------------------------------------------------------------------- 1 | { 2 | path: '{{ path }}', 3 | name: '{{ camelCase component }}', 4 | getComponent(location, cb) { 5 | System.import('{{{directory (properCase component)}}}') 6 | .then(loadModule(cb)) 7 | .catch(errorLoading); 8 | }, 9 | },$1 10 | -------------------------------------------------------------------------------- /internals/generators/route/routeWithReducer.hbs: -------------------------------------------------------------------------------- 1 | { 2 | path: '{{ path }}', 3 | name: '{{ camelCase component }}', 4 | getComponent(nextState, cb) { 5 | const importModules = Promise.all([ 6 | System.import('containers/{{ properCase component }}/reducer'), 7 | {{#if useSagas}} 8 | System.import('containers/{{ properCase component }}/sagas'), 9 | {{/if}} 10 | System.import('containers/{{ properCase component }}'), 11 | ]); 12 | 13 | const renderRoute = loadModule(cb); 14 | 15 | importModules.then(([reducer,{{#if useSagas}} sagas,{{/if}} component]) => { 16 | injectReducer('{{ camelCase component }}', reducer.default); 17 | {{#if useSagas}} 18 | injectSagas(sagas.default); 19 | {{/if}} 20 | renderRoute(component); 21 | }); 22 | 23 | importModules.catch(errorLoading); 24 | }, 25 | },$1 26 | -------------------------------------------------------------------------------- /internals/generators/utils/componentExists.js: -------------------------------------------------------------------------------- 1 | /** 2 | * componentExists 3 | * 4 | * Check whether the given component exist in either the components or containers directory 5 | */ 6 | 7 | const fs = require('fs'); 8 | const pageComponents = fs.readdirSync('app/components'); 9 | const pageContainers = fs.readdirSync('app/containers'); 10 | const components = pageComponents.concat(pageContainers); 11 | 12 | function componentExists(comp) { 13 | return components.indexOf(comp) >= 0; 14 | } 15 | 16 | module.exports = componentExists; 17 | -------------------------------------------------------------------------------- /internals/scripts/analyze.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var shelljs = require('shelljs'); 4 | var animateProgress = require('./helpers/progress'); 5 | var chalk = require('chalk'); 6 | var addCheckMark = require('./helpers/checkmark'); 7 | 8 | var progress = animateProgress('Generating stats'); 9 | 10 | // Generate stats.json file with webpack 11 | shelljs.exec( 12 | 'webpack --config internals/webpack/webpack.prod.babel.js --profile --json > stats.json', 13 | addCheckMark.bind(null, callback) // Output a checkmark on completion 14 | ); 15 | 16 | // Called after webpack has finished generating the stats.json file 17 | function callback() { 18 | clearInterval(progress); 19 | process.stdout.write( 20 | '\n\nOpen ' + chalk.magenta('http://webpack.github.io/analyse/') + ' in your browser and upload the stats.json file!' + 21 | chalk.blue('\n(Tip: ' + chalk.italic('CMD + double-click') + ' the link!)\n\n') 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /internals/scripts/dependencies.js: -------------------------------------------------------------------------------- 1 | /*eslint-disable*/ 2 | 3 | // No need to build the DLL in production 4 | if (process.env.NODE_ENV === 'production') { 5 | process.exit(0) 6 | } 7 | 8 | require('shelljs/global') 9 | 10 | const path = require('path') 11 | const fs = require('fs') 12 | const exists = fs.existsSync 13 | const writeFile = fs.writeFileSync 14 | 15 | const defaults = require('lodash/defaultsDeep') 16 | const pkg = require(path.join(process.cwd(), 'package.json')) 17 | const config = require('../config') 18 | const dllConfig = defaults(pkg.dllPlugin, config.dllPlugin.defaults) 19 | const outputPath = path.join(process.cwd(), dllConfig.path) 20 | const dllManifestPath = path.join(outputPath, 'package.json') 21 | 22 | /** 23 | * I use node_modules/react-boilerplate-dlls by default just because 24 | * it isn't going to be version controlled and babel wont try to parse it. 25 | */ 26 | mkdir('-p', outputPath) 27 | 28 | echo('Building the Webpack DLL...') 29 | 30 | /** 31 | * Create a manifest so npm install doesnt warn us 32 | */ 33 | if (!exists(dllManifestPath)) { 34 | writeFile( 35 | dllManifestPath, 36 | JSON.stringify(defaults({ 37 | name: 'react-boilerplate-dlls', 38 | private: true, 39 | author: pkg.author, 40 | repository: pkg.repository, 41 | version: pkg.version 42 | }), null, 2), 43 | 44 | 'utf8' 45 | ) 46 | } 47 | 48 | // the BUILDING_DLL env var is set to avoid confusing the development environment 49 | exec('cross-env BUILDING_DLL=true webpack --display-chunks --color --config internals/webpack/webpack.dll.babel.js') 50 | -------------------------------------------------------------------------------- /internals/scripts/deploy.js: -------------------------------------------------------------------------------- 1 | 2 | require('shelljs/global'); 3 | const deploy = require('ethdeploy'); 4 | const base = require('../ethdeploy/ethdeploy.base.js'); 5 | const testnet = require('../ethdeploy/ethdeploy.testnet.js'); 6 | const livenet = require('../ethdeploy/ethdeploy.livenet.js'); 7 | const testrpc = require('../ethdeploy/ethdeploy.testrpc.js'); 8 | const fs = require('fs'); 9 | 10 | // deployment modules 11 | const deploymentModules = { 12 | testnet: testnet, 13 | livenet: livenet, 14 | testrpc: testrpc, 15 | }; 16 | 17 | // environments file path 18 | const environmentsFilePath = './app/contracts/build/environments.json'; 19 | 20 | // wait for testrpc to load, this is a hack until something better is discovered 21 | // testrpc should wait, or deployment staged in sequence somehow 22 | setTimeout(function(){ 23 | deploy(deploymentModules[process.env['ENVIRONMENT']], function(deployError, deployResult){ 24 | if (deployError) throw err; 25 | 26 | fs.writeFile(environmentsFilePath, JSON.stringify(deployResult, null, 2), 'utf8', (err) => { 27 | if (err) throw err; 28 | console.log('Contracts deployed!'); 29 | 30 | exit(0); 31 | }); 32 | }); 33 | }, 4000); 34 | -------------------------------------------------------------------------------- /internals/scripts/helpers/checkmark.js: -------------------------------------------------------------------------------- 1 | var chalk = require('chalk'); 2 | 3 | /** 4 | * Adds mark check symbol 5 | */ 6 | function addCheckMark(callback) { 7 | process.stdout.write(chalk.green(' ✓')); 8 | callback(); 9 | } 10 | 11 | module.exports = addCheckMark; 12 | -------------------------------------------------------------------------------- /internals/scripts/helpers/progress.js: -------------------------------------------------------------------------------- 1 | var readline = require('readline'); 2 | 3 | /** 4 | * Adds an animated progress indicator 5 | * 6 | * @param {string} message The message to write next to the indicator 7 | * @param {number} amountOfDots The amount of dots you want to animate 8 | */ 9 | function animateProgress(message, amountOfDots) { 10 | if (typeof amountOfDots !== 'number') { 11 | amountOfDots = 3; 12 | } 13 | 14 | var i = 0; 15 | return setInterval(function () { 16 | readline.cursorTo(process.stdout, 0); 17 | i = (i + 1) % (amountOfDots + 1); 18 | var dots = new Array(i + 1).join('.'); 19 | process.stdout.write(message + dots); 20 | }, 500); 21 | } 22 | 23 | module.exports = animateProgress; 24 | -------------------------------------------------------------------------------- /internals/scripts/npmcheckversion.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | var exec = require('child_process').exec; 3 | exec('npm -v', function (err, stdout, stderr) { 4 | if (err) throw err; 5 | if (parseFloat(stdout) < 3) { 6 | throw new Error('[ERROR: React Boilerplate] You need npm version @>=3'); 7 | process.exit(1); 8 | } 9 | }); 10 | -------------------------------------------------------------------------------- /internals/scripts/pagespeed.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | process.stdin.resume(); 4 | process.stdin.setEncoding('utf8'); 5 | 6 | var ngrok = require('ngrok'); 7 | var psi = require('psi'); 8 | var chalk = require('chalk'); 9 | 10 | log('\nStarting ngrok tunnel'); 11 | 12 | startTunnel(runPsi); 13 | 14 | function runPsi(url) { 15 | log('\nStarting PageSpeed Insights'); 16 | psi.output(url).then(function (err) { 17 | process.exit(0); 18 | }); 19 | } 20 | 21 | function startTunnel(cb) { 22 | ngrok.connect(3000, function (err, url) { 23 | if (err) { 24 | log(chalk.red('\nERROR\n' + err)); 25 | process.exit(0); 26 | } 27 | 28 | log('\nServing tunnel from: ' + chalk.magenta(url)); 29 | cb(url); 30 | }); 31 | } 32 | 33 | function log(string) { 34 | process.stdout.write(string); 35 | } 36 | -------------------------------------------------------------------------------- /internals/scripts/setup.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var shell = require('shelljs'); 4 | var exec = require('child_process').exec; 5 | var path = require('path'); 6 | var fs = require('fs'); 7 | var animateProgress = require('./helpers/progress'); 8 | var addCheckMark = require('./helpers/checkmark'); 9 | var readline = require('readline'); 10 | 11 | process.stdin.resume(); 12 | process.stdin.setEncoding('utf8'); 13 | 14 | process.stdout.write('\n'); 15 | var interval = animateProgress('Cleaning old repository'); 16 | process.stdout.write('Cleaning old repository'); 17 | 18 | cleanRepo(function () { 19 | clearInterval(interval); 20 | process.stdout.write('\nInstalling dependencies... (This might take a while)'); 21 | setTimeout(function () { 22 | readline.cursorTo(process.stdout, 0); 23 | interval = animateProgress('Installing dependencies'); 24 | }, 500); 25 | 26 | process.stdout.write('Installing dependencies'); 27 | installDeps(function (error) { 28 | clearInterval(interval); 29 | if (error) { 30 | process.stdout.write(error); 31 | } 32 | 33 | deleteFileInCurrentDir('setup.js', function () { 34 | process.stdout.write('\n'); 35 | interval = animateProgress('Initialising new repository'); 36 | process.stdout.write('Initialising new repository'); 37 | initGit(function () { 38 | clearInterval(interval); 39 | process.stdout.write('\nDone!'); 40 | process.exit(0); 41 | }); 42 | }); 43 | }); 44 | }); 45 | 46 | /** 47 | * Deletes the .git folder in dir 48 | */ 49 | function cleanRepo(callback) { 50 | shell.rm('-rf', '.git/'); 51 | addCheckMark(callback); 52 | } 53 | 54 | /** 55 | * Initializes git again 56 | */ 57 | function initGit(callback) { 58 | exec('git init && git add . && git commit -m "Initial commit"', addCheckMark.bind(null, callback)); 59 | } 60 | 61 | /** 62 | * Deletes a file in the current directory 63 | */ 64 | function deleteFileInCurrentDir(file, callback) { 65 | fs.unlink(path.join(__dirname, file), callback); 66 | } 67 | 68 | /** 69 | * Installs dependencies 70 | */ 71 | function installDeps(callback) { 72 | exec('npm install', addCheckMark.bind(null, callback)); 73 | } 74 | -------------------------------------------------------------------------------- /internals/scripts/solium.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | require('shelljs/global'); 3 | 4 | // Commit the changes 5 | if (exec('node ./node_modules/solium/bin/solium.js test').code !== 0) { 6 | echo('\nError: Solium solidity linting has failed..'); 7 | exit(1); 8 | } else { 9 | exit(0); 10 | } 11 | -------------------------------------------------------------------------------- /internals/templates/appContainer.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * App.react.js 4 | * 5 | * This component is the skeleton around the actual pages, and should only 6 | * contain code that should be seen on all pages. (e.g. navigation bar) 7 | * 8 | * NOTE: while this component should technically be a stateless functional 9 | * component (SFC), hot reloading does not currently support SFCs. If hot 10 | * reloading is not a neccessity for you then you can refactor it and remove 11 | * the linting exception. 12 | */ 13 | 14 | import React from 'react'; 15 | 16 | import styles from './styles.css'; 17 | 18 | export default class App extends React.Component { // eslint-disable-line react/prefer-stateless-function 19 | 20 | static propTypes = { 21 | children: React.PropTypes.node, 22 | }; 23 | 24 | render() { 25 | return ( 26 |
27 | {React.Children.toArray(this.props.children)} 28 |
29 | ); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /internals/templates/asyncInjectors.js: -------------------------------------------------------------------------------- 1 | import { conformsTo, isEmpty, isFunction, isObject, isString } from 'lodash'; 2 | import invariant from 'invariant'; 3 | import warning from 'warning'; 4 | import createReducer from 'reducers'; 5 | 6 | /** 7 | * Validate the shape of redux store 8 | */ 9 | export function checkStore(store) { 10 | const shape = { 11 | dispatch: isFunction, 12 | subscribe: isFunction, 13 | getState: isFunction, 14 | replaceReducer: isFunction, 15 | runSaga: isFunction, 16 | asyncReducers: isObject, 17 | }; 18 | invariant( 19 | conformsTo(store, shape), 20 | '(app/utils...) asyncInjectors: Expected a valid redux store' 21 | ); 22 | } 23 | 24 | /** 25 | * Inject an asynchronously loaded reducer 26 | */ 27 | export function injectAsyncReducer(store, isValid) { 28 | return function injectReducer(name, asyncReducer) { 29 | if (!isValid) checkStore(store); 30 | 31 | invariant( 32 | isString(name) && !isEmpty(name) && isFunction(asyncReducer), 33 | '(app/utils...) injectAsyncReducer: Expected `asyncReducer` to be a reducer function' 34 | ); 35 | 36 | store.asyncReducers[name] = asyncReducer; // eslint-disable-line no-param-reassign 37 | store.replaceReducer(createReducer(store.asyncReducers)); 38 | }; 39 | } 40 | 41 | /** 42 | * Inject an asynchronously loaded saga 43 | */ 44 | export function injectAsyncSagas(store, isValid) { 45 | return function injectSagas(sagas) { 46 | if (!isValid) checkStore(store); 47 | 48 | invariant( 49 | Array.isArray(sagas), 50 | '(app/utils...) injectAsyncSagas: Expected `sagas` to be an array of generator functions' 51 | ); 52 | 53 | warning( 54 | !isEmpty(sagas), 55 | '(app/utils...) injectAsyncSagas: Received an empty `sagas` array' 56 | ); 57 | 58 | sagas.map(store.runSaga); 59 | }; 60 | } 61 | 62 | /** 63 | * Helper for creating injectors 64 | */ 65 | export function getAsyncInjectors(store) { 66 | checkStore(store); 67 | 68 | return { 69 | injectReducer: injectAsyncReducer(store, true), 70 | injectSagas: injectAsyncSagas(store, true), 71 | }; 72 | } 73 | -------------------------------------------------------------------------------- /internals/templates/homePage/homePage.js: -------------------------------------------------------------------------------- 1 | /* 2 | * HomePage 3 | * 4 | * This is the first thing users see of our App, at the '/' route 5 | * 6 | * NOTE: while this component should technically be a stateless functional 7 | * component (SFC), hot reloading does not currently support SFCs. If hot 8 | * reloading is not a neccessity for you then you can refactor it and remove 9 | * the linting exception. 10 | */ 11 | 12 | import React from 'react'; 13 | import { FormattedMessage } from 'react-intl'; 14 | import messages from './messages'; 15 | 16 | export default class HomePage extends React.Component { // eslint-disable-line react/prefer-stateless-function 17 | 18 | render() { 19 | return ( 20 |

21 | 22 |

23 | ); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /internals/templates/homePage/messages.js: -------------------------------------------------------------------------------- 1 | /* 2 | * HomePage Messages 3 | * 4 | * This contains all the text for the HomePage component. 5 | */ 6 | import { defineMessages } from 'react-intl'; 7 | 8 | export default defineMessages({ 9 | header: { 10 | id: 'app.components.HomePage.header', 11 | defaultMessage: 'This is HomePage components !', 12 | }, 13 | }); 14 | -------------------------------------------------------------------------------- /internals/templates/i18n.js: -------------------------------------------------------------------------------- 1 | /** 2 | * i18n.js 3 | * 4 | * This will setup the i18n language files and locale data for your app. 5 | * 6 | */ 7 | import { addLocaleData } from 'react-intl'; 8 | 9 | import enLocaleData from 'react-intl/locale-data/en'; 10 | 11 | export const appLocales = [ 12 | 'en', 13 | ]; 14 | 15 | import enTranslationMessages from './translations/en.json'; 16 | 17 | addLocaleData(enLocaleData); 18 | 19 | const formatTranslationMessages = (messages) => { 20 | const formattedMessages = {}; 21 | for (const message of messages) { 22 | formattedMessages[message.id] = message.message || message.defaultMessage; 23 | } 24 | 25 | return formattedMessages; 26 | }; 27 | 28 | export const translationMessages = { 29 | en: formatTranslationMessages(enTranslationMessages), 30 | }; 31 | -------------------------------------------------------------------------------- /internals/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | React.js Boilerplate 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /internals/templates/languageProvider/actions.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * LanguageProvider actions 4 | * 5 | */ 6 | 7 | import { 8 | CHANGE_LOCALE, 9 | } from './constants'; 10 | 11 | export function changeLocale(languageLocale) { 12 | return { 13 | type: CHANGE_LOCALE, 14 | locale: languageLocale, 15 | }; 16 | } 17 | -------------------------------------------------------------------------------- /internals/templates/languageProvider/constants.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * LanguageProvider constants 4 | * 5 | */ 6 | 7 | export const CHANGE_LOCALE = 'app/LanguageToggle/CHANGE_LOCALE'; 8 | -------------------------------------------------------------------------------- /internals/templates/languageProvider/languageProvider.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * LanguageProvider 4 | * 5 | * this component connects the redux state language locale to the 6 | * IntlProvider component and i18n messages (loaded from `app/translations`) 7 | */ 8 | 9 | import React from 'react'; 10 | import { connect } from 'react-redux'; 11 | import { createSelector } from 'reselect'; 12 | import { IntlProvider } from 'react-intl'; 13 | import { selectLocale } from './selectors'; 14 | 15 | export class LanguageProvider extends React.Component { // eslint-disable-line react/prefer-stateless-function 16 | render() { 17 | return ( 18 | 19 | {React.Children.only(this.props.children)} 20 | 21 | ); 22 | } 23 | } 24 | 25 | LanguageProvider.propTypes = { 26 | locale: React.PropTypes.string, 27 | messages: React.PropTypes.object, 28 | children: React.PropTypes.element.isRequired, 29 | }; 30 | 31 | 32 | const mapStateToProps = createSelector( 33 | selectLocale(), 34 | (locale) => ({ locale }) 35 | ); 36 | 37 | function mapDispatchToProps(dispatch) { 38 | return { 39 | dispatch, 40 | }; 41 | } 42 | 43 | export default connect(mapStateToProps, mapDispatchToProps)(LanguageProvider); 44 | -------------------------------------------------------------------------------- /internals/templates/languageProvider/reducer.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * LanguageProvider reducer 4 | * 5 | */ 6 | 7 | import { fromJS } from 'immutable'; 8 | import { 9 | CHANGE_LOCALE, 10 | } from './constants'; 11 | 12 | const initialState = fromJS({ 13 | locale: 'en', 14 | }); 15 | 16 | function languageProviderReducer(state = initialState, action) { 17 | switch (action.type) { 18 | case CHANGE_LOCALE: 19 | return state 20 | .set('locale', action.locale); 21 | default: 22 | return state; 23 | } 24 | } 25 | 26 | export default languageProviderReducer; 27 | -------------------------------------------------------------------------------- /internals/templates/languageProvider/selectors.js: -------------------------------------------------------------------------------- 1 | import { createSelector } from 'reselect'; 2 | 3 | /** 4 | * Direct selector to the languageToggle state domain 5 | */ 6 | const selectLanguage = () => state => state.get('language'); 7 | 8 | /** 9 | * Select the language locale 10 | */ 11 | 12 | const selectLocale = () => createSelector( 13 | selectLanguage(), 14 | (languageState) => languageState.get('locale') 15 | ); 16 | 17 | export { 18 | selectLanguage, 19 | selectLocale, 20 | }; 21 | -------------------------------------------------------------------------------- /internals/templates/notFoundPage/messages.js: -------------------------------------------------------------------------------- 1 | /* 2 | * NotFoundPage Messages 3 | * 4 | * This contains all the text for the NotFoundPage component. 5 | */ 6 | import { defineMessages } from 'react-intl'; 7 | 8 | export default defineMessages({ 9 | header: { 10 | id: 'app.components.NotFoundPage.header', 11 | defaultMessage: 'This is NotFoundPage component !', 12 | }, 13 | }); 14 | -------------------------------------------------------------------------------- /internals/templates/notFoundPage/notFoundPage.js: -------------------------------------------------------------------------------- 1 | /** 2 | * NotFoundPage 3 | * 4 | * This is the page we show when the user visits a url that doesn't have a route 5 | * 6 | * NOTE: while this component should technically be a stateless functional 7 | * component (SFC), hot reloading does not currently support SFCs. If hot 8 | * reloading is not a neccessity for you then you can refactor it and remove 9 | * the linting exception. 10 | */ 11 | 12 | import React from 'react'; 13 | import { FormattedMessage } from 'react-intl'; 14 | import messages from './messages'; 15 | 16 | export default class NotFound extends React.Component { // eslint-disable-line react/prefer-stateless-function 17 | 18 | render() { 19 | return ( 20 |

21 | 22 |

23 | ); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /internals/templates/reducers.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Combine all reducers in this file and export the combined reducers. 3 | * If we were to do this in store.js, reducers wouldn't be hot reloadable. 4 | */ 5 | 6 | import { combineReducers } from 'redux-immutable'; 7 | import { fromJS } from 'immutable'; 8 | import { LOCATION_CHANGE } from 'react-router-redux'; 9 | import languageProviderReducer from 'containers/LanguageProvider/reducer'; 10 | 11 | /* 12 | * routeReducer 13 | * 14 | * The reducer merges route location changes into our immutable state. 15 | * The change is necessitated by moving to react-router-redux@4 16 | * 17 | */ 18 | 19 | // Initial routing state 20 | const routeInitialState = fromJS({ 21 | locationBeforeTransitions: null, 22 | }); 23 | 24 | /** 25 | * Merge route into the global application state 26 | */ 27 | function routeReducer(state = routeInitialState, action) { 28 | switch (action.type) { 29 | /* istanbul ignore next */ 30 | case LOCATION_CHANGE: 31 | return state.merge({ 32 | locationBeforeTransitions: action.payload, 33 | }); 34 | default: 35 | return state; 36 | } 37 | } 38 | 39 | /** 40 | * Creates the main reducer with the asynchronously loaded ones 41 | */ 42 | export default function createReducer(asyncReducers) { 43 | return combineReducers({ 44 | route: routeReducer, 45 | language: languageProviderReducer, 46 | ...asyncReducers, 47 | }); 48 | } 49 | -------------------------------------------------------------------------------- /internals/templates/routes.js: -------------------------------------------------------------------------------- 1 | // These are the pages you can go to. 2 | // They are all wrapped in the App component, which should contain the navbar etc 3 | // See http://blog.mxstbr.com/2016/01/react-apps-with-pages for more information 4 | // about the code splitting business 5 | import { getAsyncInjectors } from 'utils/asyncInjectors'; 6 | 7 | const errorLoading = (err) => { 8 | console.error('Dynamic page loading failed', err); // eslint-disable-line no-console 9 | }; 10 | 11 | const loadModule = (cb) => (componentModule) => { 12 | cb(null, componentModule.default); 13 | }; 14 | 15 | export default function createRoutes(store) { 16 | // Create reusable async injectors using getAsyncInjectors factory 17 | const { injectReducer, injectSagas } = getAsyncInjectors(store); // eslint-disable-line no-unused-vars 18 | 19 | return [ 20 | { 21 | path: '/', 22 | name: 'home', 23 | getComponent(nextState, cb) { 24 | const importModules = Promise.all([ 25 | System.import('containers/HomePage'), 26 | ]); 27 | 28 | const renderRoute = loadModule(cb); 29 | 30 | importModules.then(([component]) => { 31 | renderRoute(component); 32 | }); 33 | 34 | importModules.catch(errorLoading); 35 | }, 36 | }, { 37 | path: '*', 38 | name: 'notfound', 39 | getComponent(nextState, cb) { 40 | System.import('containers/NotFoundPage') 41 | .then(loadModule(cb)) 42 | .catch(errorLoading); 43 | }, 44 | }, 45 | ]; 46 | } 47 | -------------------------------------------------------------------------------- /internals/templates/selectors.js: -------------------------------------------------------------------------------- 1 | // selectLocationState expects a plain JS object for the routing state 2 | const selectLocationState = () => { 3 | let prevRoutingState; 4 | let prevRoutingStateJS; 5 | 6 | return (state) => { 7 | const routingState = state.get('route'); // or state.route 8 | 9 | if (!routingState.equals(prevRoutingState)) { 10 | prevRoutingState = routingState; 11 | prevRoutingStateJS = routingState.toJS(); 12 | } 13 | 14 | return prevRoutingStateJS; 15 | }; 16 | }; 17 | 18 | export { 19 | selectLocationState, 20 | }; 21 | -------------------------------------------------------------------------------- /internals/templates/selectors.test.js: -------------------------------------------------------------------------------- 1 | import { fromJS } from 'immutable'; 2 | import expect from 'expect'; 3 | 4 | import { selectLocationState } from 'containers/App/selectors'; 5 | 6 | describe('selectLocationState', () => { 7 | it('should select the route as a plain JS object', () => { 8 | const route = fromJS({ 9 | locationBeforeTransitions: null, 10 | }); 11 | const mockedState = fromJS({ 12 | route, 13 | }); 14 | expect(selectLocationState()(mockedState)).toEqual(route.toJS()); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /internals/templates/store.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Create the store with asynchronously loaded reducers 3 | */ 4 | 5 | import { createStore, applyMiddleware, compose } from 'redux'; 6 | import { fromJS } from 'immutable'; 7 | import { routerMiddleware } from 'react-router-redux'; 8 | import createSagaMiddleware from 'redux-saga'; 9 | import createReducer from './reducers'; 10 | 11 | const sagaMiddleware = createSagaMiddleware(); 12 | const devtools = window.devToolsExtension || (() => noop => noop); 13 | 14 | export default function configureStore(initialState = {}, history) { 15 | // Create the store with two middlewares 16 | // 1. sagaMiddleware: Makes redux-sagas work 17 | // 2. routerMiddleware: Syncs the location/URL path to the state 18 | const middlewares = [ 19 | sagaMiddleware, 20 | routerMiddleware(history), 21 | ]; 22 | 23 | const enhancers = [ 24 | applyMiddleware(...middlewares), 25 | devtools(), 26 | ]; 27 | 28 | const store = createStore( 29 | createReducer(), 30 | fromJS(initialState), 31 | compose(...enhancers) 32 | ); 33 | 34 | // Create hook for async sagas 35 | store.runSaga = sagaMiddleware.run; 36 | 37 | // Make reducers hot reloadable, see http://mxs.is/googmo 38 | /* istanbul ignore next */ 39 | if (module.hot) { 40 | System.import('./reducers').then((reducerModule) => { 41 | const createReducers = reducerModule.default; 42 | const nextReducers = createReducers(store.asyncReducers); 43 | 44 | store.replaceReducer(nextReducers); 45 | }); 46 | } 47 | 48 | // Initialize it with no other reducers 49 | store.asyncReducers = {}; 50 | return store; 51 | } 52 | -------------------------------------------------------------------------------- /internals/templates/store.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Test store addons 3 | */ 4 | 5 | import expect from 'expect'; 6 | import configureStore from '../store'; // eslint-disable-line 7 | import { browserHistory } from 'react-router'; 8 | 9 | describe('configureStore', () => { 10 | let store; 11 | 12 | before(() => { 13 | store = configureStore({}, browserHistory); 14 | }); 15 | 16 | describe('asyncReducers', () => { 17 | it('should contain an object for async reducers', () => { 18 | expect(typeof store.asyncReducers).toEqual('object'); 19 | }); 20 | }); 21 | 22 | describe('runSaga', () => { 23 | it('should contain a hook for `sagaMiddleware.run`', () => { 24 | expect(typeof store.runSaga).toEqual('function'); 25 | }); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /internals/templates/styles.css: -------------------------------------------------------------------------------- 1 | /** 2 | * styles.css 3 | * 4 | * App container styles 5 | */ 6 | 7 | .container { 8 | display: block; 9 | } 10 | -------------------------------------------------------------------------------- /internals/templates/translations/en.json: -------------------------------------------------------------------------------- 1 | [] 2 | -------------------------------------------------------------------------------- /internals/testing/dapple.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | require('shelljs/global'); 3 | 4 | const chalk = require('chalk'); 5 | 6 | echo(` 7 | 8 | ${chalk.underline('DAPPLE TESTING STARTED:')} 9 | 10 | `); 11 | 12 | // Commit the changes 13 | if (exec('cd ./app && node ../node_modules/dapple/cmd/main.js test').code !== 0) { 14 | echo('\nError: Dapple test failed'); 15 | exit(1); 16 | } else { 17 | exit(0); 18 | } 19 | -------------------------------------------------------------------------------- /internals/testing/karma.conf.js: -------------------------------------------------------------------------------- 1 | const webpackConfig = require('../webpack/webpack.test.babel'); 2 | const argv = require('minimist')(process.argv.slice(2)); 3 | const path = require('path'); 4 | 5 | module.exports = (config) => { 6 | config.set({ 7 | frameworks: ['mocha'], 8 | reporters: ['coverage', 'mocha'], 9 | browsers: process.env.TRAVIS // eslint-disable-line no-nested-ternary 10 | ? ['ChromeTravis'] 11 | : process.env.APPVEYOR 12 | ? ['IE'] : ['Chrome'], 13 | 14 | autoWatch: false, 15 | singleRun: true, 16 | 17 | client: { 18 | mocha: { 19 | grep: argv.grep, 20 | }, 21 | }, 22 | 23 | files: [ 24 | { 25 | pattern: './test-bundler.js', 26 | watched: false, 27 | served: true, 28 | included: true, 29 | }, 30 | ], 31 | 32 | preprocessors: { 33 | ['./test-bundler.js']: ['webpack', 'sourcemap'], // eslint-disable-line no-useless-computed-key 34 | }, 35 | 36 | webpack: webpackConfig, 37 | 38 | // make Webpack bundle generation quiet 39 | webpackMiddleware: { 40 | noInfo: true, 41 | stats: 'errors-only', 42 | }, 43 | 44 | customLaunchers: { 45 | ChromeTravis: { 46 | base: 'Chrome', 47 | flags: ['--no-sandbox'], 48 | }, 49 | }, 50 | 51 | coverageReporter: { 52 | dir: path.join(process.cwd(), 'coverage'), 53 | reporters: [ 54 | { type: 'lcov', subdir: 'lcov' }, 55 | { type: 'html', subdir: 'html' }, 56 | { type: 'text-summary' }, 57 | ], 58 | }, 59 | 60 | }); 61 | }; 62 | -------------------------------------------------------------------------------- /internals/testing/test-bundler.js: -------------------------------------------------------------------------------- 1 | // needed for regenerator-runtime 2 | // (ES7 generator support is required by redux-saga) 3 | import 'babel-polyfill'; 4 | 5 | // If we need to use Chai, we'll have already chaiEnzyme loaded 6 | import chai from 'chai'; 7 | import chaiEnzyme from 'chai-enzyme'; 8 | chai.use(chaiEnzyme()); 9 | 10 | // Include all .js files under `app`, except app.js, reducers.js, and routes.js. 11 | // This is for isparta code coverage 12 | const context = require.context('../../app', true, /^^((?!(app|reducers|routes|contracts)).)*\.js$/); 13 | context.keys().forEach(context); 14 | -------------------------------------------------------------------------------- /internals/webpack/webpack.dll.babel.js: -------------------------------------------------------------------------------- 1 | /** 2 | * WEBPACK DLL GENERATOR 3 | * 4 | * This profile is used to cache webpack's module 5 | * contexts for external library and framework type 6 | * dependencies which will usually not change often enough 7 | * to warrant building them from scratch every time we use 8 | * the webpack process. 9 | */ 10 | 11 | const { join } = require('path'); 12 | const defaults = require('lodash/defaultsDeep'); 13 | const webpack = require('webpack'); 14 | const pkg = require(join(process.cwd(), 'package.json')); 15 | const dllPlugin = require('../config').dllPlugin; 16 | 17 | if (!pkg.dllPlugin) { process.exit(0); } 18 | 19 | const dllConfig = defaults(pkg.dllPlugin, dllPlugin.defaults); 20 | const outputPath = join(process.cwd(), dllConfig.path); 21 | 22 | module.exports = { 23 | context: process.cwd(), 24 | entry: dllConfig.dlls ? dllConfig.dlls : dllPlugin.entry(pkg), 25 | devtool: 'eval', 26 | output: { 27 | filename: '[name].dll.js', 28 | path: outputPath, 29 | library: '[name]', 30 | }, 31 | module: { 32 | loaders: [{ 33 | test: /\.json$/, 34 | loader: 'json-loader', 35 | }], 36 | }, 37 | plugins: [ 38 | new webpack.DllPlugin({ name: '[name]', path: join(outputPath, '[name].json') }), // eslint-disable-line no-new 39 | ], 40 | }; 41 | -------------------------------------------------------------------------------- /internals/webpack/webpack.prod.babel.js: -------------------------------------------------------------------------------- 1 | // Important modules this config uses 2 | const path = require('path'); 3 | const webpack = require('webpack'); 4 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 5 | const ExtractTextPlugin = require('extract-text-webpack-plugin'); 6 | const OfflinePlugin = require('offline-plugin'); 7 | 8 | module.exports = require('./webpack.base.babel')({ 9 | // In production, we skip all hot-reloading stuff 10 | entry: [ 11 | path.join(process.cwd(), 'app/app.js'), 12 | ], 13 | 14 | // Utilize long-term caching by adding content hashes (not compilation hashes) to compiled assets 15 | output: { 16 | filename: '[name].[chunkhash].js', 17 | chunkFilename: '[name].[chunkhash].chunk.js', 18 | }, 19 | 20 | // We use ExtractTextPlugin so we get a seperate CSS file instead 21 | // of the CSS being in the JS and injected as a style tag 22 | cssLoaders: ExtractTextPlugin.extract( 23 | 'style-loader', 24 | 'css-loader?modules&-autoprefixer&importLoaders=1!postcss-loader' 25 | ), 26 | 27 | plugins: [ 28 | new webpack.optimize.CommonsChunkPlugin({ 29 | name: 'vendor', 30 | children: true, 31 | minChunks: 2, 32 | async: true, 33 | }), 34 | 35 | // OccurrenceOrderPlugin is needed for long-term caching to work properly. 36 | // See http://mxs.is/googmv 37 | new webpack.optimize.OccurrenceOrderPlugin(true), 38 | 39 | // Merge all duplicate modules 40 | new webpack.optimize.DedupePlugin(), 41 | 42 | // Minify and optimize the JavaScript 43 | new webpack.optimize.UglifyJsPlugin({ 44 | compress: { 45 | warnings: false, // ...but do not show warnings in the console (there is a lot of them) 46 | }, 47 | }), 48 | 49 | // Minify and optimize the index.html 50 | new HtmlWebpackPlugin({ 51 | template: 'app/index.html', 52 | minify: { 53 | removeComments: true, 54 | collapseWhitespace: true, 55 | removeRedundantAttributes: true, 56 | useShortDoctype: true, 57 | removeEmptyAttributes: true, 58 | removeStyleLinkTypeAttributes: true, 59 | keepClosingSlash: true, 60 | minifyJS: true, 61 | minifyCSS: true, 62 | minifyURLs: true, 63 | }, 64 | inject: true, 65 | }), 66 | 67 | // Extract the CSS into a seperate file 68 | new ExtractTextPlugin('[name].[contenthash].css'), 69 | 70 | // Put it in the end to capture all the HtmlWebpackPlugin's 71 | // assets manipulations and do leak its manipulations to HtmlWebpackPlugin 72 | new OfflinePlugin({ 73 | relativePaths: false, 74 | publicPath: '/', 75 | 76 | // No need to cache .htaccess. See http://mxs.is/googmp, 77 | // this is applied before any match in `caches` section 78 | excludes: ['.htaccess'], 79 | 80 | caches: { 81 | main: [':rest:'], 82 | 83 | // All chunks marked as `additional`, loaded after main section 84 | // and do not prevent SW to install. Change to `optional` if 85 | // do not want them to be preloaded at all (cached only when first loaded) 86 | additional: ['*.chunk.js'], 87 | }, 88 | 89 | // Removes warning for about `additional` section usage 90 | safeToUseOptionalCaches: true, 91 | 92 | AppCache: false, 93 | }), 94 | ], 95 | }); 96 | -------------------------------------------------------------------------------- /internals/webpack/webpack.test.babel.js: -------------------------------------------------------------------------------- 1 | /** 2 | * TEST WEBPACK CONFIGURATION 3 | */ 4 | 5 | const path = require('path'); 6 | const webpack = require('webpack'); 7 | const modules = [ 8 | 'app', 9 | 'node_modules', 10 | ]; 11 | 12 | module.exports = { 13 | devtool: 'inline-source-map', 14 | isparta: { 15 | babel: { 16 | presets: ['es2015', 'react', 'stage-0'], 17 | }, 18 | }, 19 | module: { 20 | // Some libraries don't like being run through babel. 21 | // If they gripe, put them here. 22 | noParse: [ 23 | /node_modules(\\|\/)sinon/, 24 | /node_modules(\\|\/)acorn/, 25 | ], 26 | preLoaders: [ 27 | { test: /\.js$/, 28 | loader: 'isparta', 29 | include: path.resolve('app/'), 30 | }, 31 | ], 32 | loaders: [ 33 | { test: /\.json$/, loader: 'json-loader' }, 34 | { test: /\.css$/, loader: 'null-loader' }, 35 | 36 | // sinon.js--aliased for enzyme--expects/requires global vars. 37 | // imports-loader allows for global vars to be injected into the module. 38 | // See https://github.com/webpack/webpack/issues/304 39 | { test: /sinon(\\|\/)pkg(\\|\/)sinon\.js/, 40 | loader: 'imports?define=>false,require=>false', 41 | }, 42 | { test: /\.js$/, 43 | loader: 'babel', 44 | exclude: [/node_modules/], 45 | }, 46 | { test: /\.jpe?g$|\.gif$|\.png$|\.svg$/i, 47 | loader: 'null-loader', 48 | }, 49 | ], 50 | }, 51 | 52 | plugins: [ 53 | 54 | // Always expose NODE_ENV to webpack, in order to use `process.env.NODE_ENV` 55 | // inside your code for any environment checks; UglifyJS will automatically 56 | // drop any unreachable code. 57 | new webpack.DefinePlugin({ 58 | 'process.env': { 59 | NODE_ENV: JSON.stringify(process.env.NODE_ENV), 60 | }, 61 | })], 62 | 63 | // Some node_modules pull in Node-specific dependencies. 64 | // Since we're running in a browser we have to stub them out. See: 65 | // https://webpack.github.io/docs/configuration.html#node 66 | // https://github.com/webpack/node-libs-browser/tree/master/mock 67 | // https://github.com/webpack/jade-loader/issues/8#issuecomment-55568520 68 | node: { 69 | fs: 'empty', 70 | child_process: 'empty', 71 | net: 'empty', 72 | tls: 'empty', 73 | }, 74 | 75 | // required for enzyme to work properly 76 | externals: { 77 | jsdom: 'window', 78 | 'react/addons': true, 79 | 'react/lib/ExecutionEnvironment': true, 80 | 'react/lib/ReactContext': 'window', 81 | }, 82 | resolve: { 83 | modulesDirectories: modules, 84 | modules, 85 | alias: { 86 | // required for enzyme to work properly 87 | sinon: 'sinon/pkg/sinon', 88 | }, 89 | }, 90 | }; 91 | -------------------------------------------------------------------------------- /server/index.js: -------------------------------------------------------------------------------- 1 | /* eslint consistent-return:0 */ 2 | 3 | const express = require('express'); 4 | const logger = require('./logger'); 5 | 6 | const argv = require('minimist')(process.argv.slice(2)); 7 | const setup = require('./middlewares/frontendMiddleware'); 8 | const isDev = process.env.NODE_ENV !== 'production'; 9 | const ngrok = (isDev && process.env.ENABLE_TUNNEL) || argv.tunnel ? require('ngrok') : false; 10 | const resolve = require('path').resolve; 11 | const app = express(); 12 | 13 | // If you need a backend, e.g. an API, add your custom backend-specific middleware here 14 | // app.use('/api', myApi); 15 | 16 | // In production we need to pass these values in instead of relying on webpack 17 | setup(app, { 18 | outputPath: resolve(process.cwd(), 'build'), 19 | publicPath: '/', 20 | }); 21 | 22 | // get the intended port number, use port 3000 if not provided 23 | const port = argv.port || process.env.PORT || 3000; 24 | 25 | // Start your app. 26 | app.listen(port, (err) => { 27 | if (err) { 28 | return logger.error(err.message); 29 | } 30 | 31 | // Connect to ngrok in dev mode 32 | if (ngrok) { 33 | ngrok.connect(port, (innerErr, url) => { 34 | if (innerErr) { 35 | return logger.error(innerErr); 36 | } 37 | 38 | logger.appStarted(port, url); 39 | }); 40 | } else { 41 | logger.appStarted(port); 42 | } 43 | }); 44 | -------------------------------------------------------------------------------- /server/logger.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | 3 | const chalk = require('chalk'); 4 | const ip = require('ip'); 5 | 6 | const divider = chalk.gray('\n-----------------------------------'); 7 | 8 | /** 9 | * Logger middleware, you can customize it to make messages more personal 10 | */ 11 | const logger = { 12 | 13 | // Called whenever there's an error on the server we want to print 14 | error: err => { 15 | console.error(chalk.red(err)); 16 | }, 17 | 18 | // Called when express.js app starts on given port w/o errors 19 | appStarted: (port, tunnelStarted) => { 20 | console.log(`Server started ${chalk.green('✓')}`); 21 | 22 | // If the tunnel started, log that and the URL it's available at 23 | if (tunnelStarted) { 24 | console.log(`Tunnel initialised ${chalk.green('✓')}`); 25 | } 26 | 27 | console.log(` 28 | ${chalk.bold('Access URLs:')}${divider} 29 | Localhost: ${chalk.magenta(`http://localhost:${port}`)} 30 | LAN: ${chalk.magenta(`http://${ip.address()}:${port}`) + 31 | (tunnelStarted ? `\n Proxy: ${chalk.magenta(tunnelStarted)}` : '')}${divider} 32 | ${chalk.blue(`Press ${chalk.italic('CTRL-C')} to stop`)} 33 | `); 34 | }, 35 | }; 36 | 37 | module.exports = logger; 38 | -------------------------------------------------------------------------------- /server/middlewares/frontendMiddleware.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable global-require */ 2 | const express = require('express'); 3 | const path = require('path'); 4 | const compression = require('compression'); 5 | const pkg = require(path.resolve(process.cwd(), 'package.json')); 6 | 7 | // Dev middleware 8 | const addDevMiddlewares = (app, webpackConfig) => { 9 | const webpack = require('webpack'); 10 | const webpackDevMiddleware = require('webpack-dev-middleware'); 11 | const webpackHotMiddleware = require('webpack-hot-middleware'); 12 | const compiler = webpack(webpackConfig); 13 | const middleware = webpackDevMiddleware(compiler, { 14 | noInfo: true, 15 | publicPath: webpackConfig.output.publicPath, 16 | silent: true, 17 | stats: 'errors-only', 18 | }); 19 | 20 | app.use(middleware); 21 | app.use(webpackHotMiddleware(compiler)); 22 | 23 | // Since webpackDevMiddleware uses memory-fs internally to store build 24 | // artifacts, we use it instead 25 | const fs = middleware.fileSystem; 26 | 27 | if (pkg.dllPlugin) { 28 | app.get(/\.dll\.js$/, (req, res) => { 29 | const filename = req.path.replace(/^\//, ''); 30 | res.sendFile(path.join(process.cwd(), pkg.dllPlugin.path, filename)); 31 | }); 32 | } 33 | 34 | app.get('*', (req, res) => { 35 | fs.readFile(path.join(compiler.outputPath, 'index.html'), (err, file) => { 36 | if (err) { 37 | res.sendStatus(404); 38 | } else { 39 | res.send(file.toString()); 40 | } 41 | }); 42 | }); 43 | }; 44 | 45 | // Production middlewares 46 | const addProdMiddlewares = (app, options) => { 47 | const publicPath = options.publicPath || '/'; 48 | const outputPath = options.outputPath || path.resolve(process.cwd(), 'build'); 49 | 50 | // compression middleware compresses your server responses which makes them 51 | // smaller (applies also to assets). You can read more about that technique 52 | // and other good practices on official Express.js docs http://mxs.is/googmy 53 | app.use(compression()); 54 | app.use(publicPath, express.static(outputPath)); 55 | 56 | app.get('*', (req, res) => res.sendFile(path.resolve(outputPath, 'index.html'))); 57 | }; 58 | 59 | /** 60 | * Front-end middleware 61 | */ 62 | module.exports = (app, options) => { 63 | const isProd = process.env.NODE_ENV === 'production'; 64 | 65 | if (isProd) { 66 | addProdMiddlewares(app, options); 67 | } else { 68 | const webpackConfig = require('../../internals/webpack/webpack.dev.babel'); 69 | addDevMiddlewares(app, webpackConfig); 70 | } 71 | 72 | return app; 73 | }; 74 | --------------------------------------------------------------------------------