diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..d4eed84
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,7 @@
+root = true
+
+[*]
+end_of_line = lf
+insert_final_newline = false
+indent_style = space
+indent_size = 2
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..a29125a
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,106 @@
+# From https://github.com/Danimoth/gitattributes/blob/master/Web.gitattributes
+
+# Handle line endings automatically for files detected as text
+# and leave all files detected as binary untouched.
+* text=auto
+
+#
+# The above will handle all files NOT found below
+#
+
+#
+## These files are text and should be normalized (Convert crlf => lf)
+#
+
+# source code
+*.php text
+*.css text
+*.sass text
+*.scss text
+*.less text
+*.styl text
+*.js text eol=lf
+*.coffee text
+*.json text
+*.htm text
+*.html text
+*.xml text
+*.svg text
+*.txt text
+*.ini text
+*.inc text
+*.pl text
+*.rb text
+*.py text
+*.scm text
+*.sql text
+*.sh text
+*.bat text
+
+# templates
+*.ejs text
+*.hbt text
+*.jade text
+*.haml text
+*.hbs text
+*.dot text
+*.tmpl text
+*.phtml text
+
+# server config
+.htaccess text
+
+# git config
+.gitattributes text
+.gitignore text
+.gitconfig text
+
+# code analysis config
+.jshintrc text
+.jscsrc text
+.jshintignore text
+.csslintrc text
+
+# misc config
+*.yaml text
+*.yml text
+.editorconfig text
+
+# build config
+*.npmignore text
+*.bowerrc text
+
+# Heroku
+Procfile text
+.slugignore text
+
+# Documentation
+*.md text
+LICENSE text
+AUTHORS text
+
+
+#
+## These files are binary and should be left untouched
+#
+
+# (binary is a macro for -text -diff)
+*.png binary
+*.jpg binary
+*.jpeg binary
+*.gif binary
+*.ico binary
+*.mov binary
+*.mp4 binary
+*.mp3 binary
+*.flv binary
+*.fla binary
+*.swf binary
+*.gz binary
+*.zip binary
+*.7z binary
+*.ttf binary
+*.eot binary
+*.woff binary
+*.pyc binary
+*.pdf binary
diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md
new file mode 100644
index 0000000..f5cc234
--- /dev/null
+++ b/.github/CONTRIBUTING.md
@@ -0,0 +1,122 @@
+# Contributing to react-boilerplate
+
+Love react-boilerplate and want to help? Thanks so much, there's something to do for everybody!
+
+Please take a moment to review this document in order to make the contribution process easy and effective for everyone involved.
+
+Following these guidelines helps to communicate that you respect the time of the developers managing and developing this open source project. In return, they should reciprocate that respect in addressing your issue or assessing patches and features.
+
+## Using the issue tracker
+
+The [issue tracker](https://github.com/mxstbr/react-boilerplate/issues) is
+the preferred channel for [bug reports](#bugs), [features requests](#features)
+and [submitting pull requests](#pull-requests).
+
+
+## Bug reports
+
+A bug is a _demonstrable problem_ that is caused by the code in the repository.
+Good bug reports are extremely helpful - thank you!
+
+Guidelines for bug reports:
+
+1. **Use the GitHub issue search** — check if the issue has already been reported.
+
+2. **Check if the issue has been fixed** — try to reproduce it using the latest `master` or development branch in the repository.
+
+3. **Isolate the problem** — ideally create a [reduced test case](https://css-tricks.com/reduced-test-cases/) and a live example.
+
+A good bug report shouldn't leave others needing to chase you up for more information. Please try to be as detailed as possible in your report. What is your environment? What steps will reproduce the issue? What browser(s) and OS
+experience the problem? What would you expect to be the outcome? All these details will help people to fix any potential bugs.
+
+Example:
+
+> Short and descriptive example bug report title
+>
+> A summary of the issue and the browser/OS environment in which it occurs. If
+> suitable, include the steps required to reproduce the bug.
+>
+> 1. This is the first step
+> 2. This is the second step
+> 3. Further steps, etc.
+>
+> `` - a link to the reduced test case
+>
+> Any other information you want to share that is relevant to the issue being
+> reported. This might include the lines of code that you have identified as
+> causing the bug, and potential solutions (and your opinions on their
+> merits).
+
+
+
+## Feature requests
+
+Feature requests are welcome. But take a moment to find out whether your idea fits with the scope and aims of the project. It's up to *you* to make a strong case to convince the project's developers of the merits of this feature. Please provide as much detail and context as possible.
+
+
+
+## Pull requests
+
+Good pull requests - patches, improvements, new features - are a fantastic
+help. They should remain focused in scope and avoid containing unrelated
+commits.
+
+**Please ask first** before embarking on any significant pull request (e.g.
+implementing features, refactoring code, porting to a different language),
+otherwise you risk spending a lot of time working on something that the
+project's developers might not want to merge into the project.
+
+Please adhere to the coding conventions used throughout a project (indentation,
+accurate comments, etc.) and any other requirements (such as test coverage).
+
+Since the `master` branch is what people actually use in production, we have a
+`dev` branch that unstable changes get merged into first. Only when we
+consider that stable we merge it into the `master` branch and release the
+changes for real.
+
+Adhering to the following process is the best way to get your work
+included in the project:
+
+1. [Fork](https://help.github.com/articles/fork-a-repo/) the project, clone your fork, and configure the remotes:
+
+ ```bash
+ # Clone your fork of the repo into the current directory
+ git clone https://github.com//react-boilerplate.git
+ # Navigate to the newly cloned directory
+ cd react-boilerplate
+ # Assign the original repo to a remote called "upstream"
+ git remote add upstream https://github.com/mxstbr/react-boilerplate.git
+ ```
+
+2. If you cloned a while ago, get the latest changes from upstream:
+
+ ```bash
+ git checkout dev
+ git pull upstream dev
+ ```
+
+3. Create a new topic branch (off the `dev` branch) to contain your feature, change, or fix:
+
+ ```bash
+ git checkout -b
+ ```
+
+4. Commit your changes in logical chunks. Please adhere to these [git commit message guidelines](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html) or your code is unlikely be merged into the main project. Use Git's [interactive rebase](https://help.github.com/articles/about-git-rebase/) feature to tidy up your commits before making them public.
+
+5. Locally merge (or rebase) the upstream dev branch into your topic branch:
+
+ ```bash
+ git pull [--rebase] upstream dev
+ ```
+
+6. Push your topic branch up to your fork:
+
+ ```bash
+ git push origin
+ ```
+
+7. [Open a Pull Request](https://help.github.com/articles/using-pull-requests/)
+ with a clear title and description.
+
+**IMPORTANT**: By submitting a patch, you agree to allow the project
+owners to license your work under the terms of the [MIT License](https://github.com/mxstbr/react-boilerplate/blob/master/LICENSE.md).
diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md
new file mode 100644
index 0000000..2d17ae4
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE.md
@@ -0,0 +1,30 @@
+# React Boilerplate
+
+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.
+
+Please direct redux-saga related questions to stack overflow:
+http://stackoverflow.com/questions/tagged/redux-saga
+
+For questions related to the boilerplate itself, you can also find answers on our gitter chat:
+https://gitter.im/mxstbr/react-boilerplate
+
+**Before opening a new issue, you may find an answer in already closed issues**:
+https://github.com/mxstbr/react-boilerplate/issues?q=is%3Aissue+is%3Aclosed
+
+## Issue Type
+
+- [ ] Bug (https://github.com/mxstbr/react-boilerplate/blob/master/.github/CONTRIBUTING.md#bug-reports)
+- [ ] Feature (https://github.com/mxstbr/react-boilerplate/blob/master/.github/CONTRIBUTING.md#feature-requests)
+
+## Description
+
+(Add images if possible)
+
+## Steps to reproduce
+
+(Add link to a demo on https://jsfiddle.net or similar if possible)
+
+# Versions
+
+- Node/NPM:
+- Browser:
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
new file mode 100644
index 0000000..95659d8
--- /dev/null
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -0,0 +1,22 @@
+## React Boilerplate
+
+Thank you for contributing! Please take a moment to review our [**contributing guidelines**](https://github.com/mxstbr/react-boilerplate/blob/master/.github/CONTRIBUTING.md)
+to make the process easy and effective for everyone involved.
+
+**Please open an issue** before embarking on any significant pull request, especially those that
+add a new library or change existing tests, otherwise you risk spending a lot of time working
+on something that might not end up being merged into the project.
+
+Before opening a pull request, please ensure:
+
+- [ ] You have followed our [**contributing guidelines**](https://github.com/mxstbr/react-boilerplate/blob/master/.github/CONTRIBUTING.md)
+- [ ] Pull request has tests (we are going for 100% coverage!)
+- [ ] Code is well-commented, linted and follows project conventions
+- [ ] Documentation is updated (if necessary)
+- [ ] Internal code generators and templates are updated (if necessary)
+- [ ] Description explains the issue/use-case resolved and auto-closes related issues
+
+Be kind to code reviewers, please try to keep pull requests as small and focused as possible :)
+
+**IMPORTANT**: By submitting a patch, you agree to allow the project
+owners to license your work under the terms of the [MIT License](https://github.com/mxstbr/react-boilerplate/blob/master/LICENSE.md).
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..5d06c05
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,10 @@
+# Don't check auto-generated stuff into git
+coverage
+build
+node_modules
+stats.json
+
+# Cruft
+.DS_Store
+npm-debug.log
+.idea
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..e522d8e
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,18 @@
+language: node_js
+sudo: true
+dist: trusty
+node_js:
+ - "5.0"
+script: npm run build
+before_install:
+ - export CHROME_BIN=/usr/bin/google-chrome
+ - export DISPLAY=:99.0
+ - sudo apt-get update
+ - sudo apt-get install -y libappindicator1 fonts-liberation
+ - wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb
+ - sudo dpkg -i google-chrome*.deb
+ - sh -e /etc/init.d/xvfb start
+notifications:
+ email:
+ on_failure: change
+after_success: 'npm run coveralls'
diff --git a/.vagrant/machines/default/docker/action_provision b/.vagrant/machines/default/docker/action_provision
new file mode 100644
index 0000000..91840cc
--- /dev/null
+++ b/.vagrant/machines/default/docker/action_provision
@@ -0,0 +1 @@
+1.5:35250a5dd976e12dc0b834ad59b40a22462dd02a2d8d020d8315d41bbaf5f663
\ No newline at end of file
diff --git a/.vagrant/machines/default/docker/creator_uid b/.vagrant/machines/default/docker/creator_uid
new file mode 100644
index 0000000..4b8433a
--- /dev/null
+++ b/.vagrant/machines/default/docker/creator_uid
@@ -0,0 +1 @@
+10208
\ No newline at end of file
diff --git a/.vagrant/machines/default/docker/id b/.vagrant/machines/default/docker/id
new file mode 100644
index 0000000..a4466b1
--- /dev/null
+++ b/.vagrant/machines/default/docker/id
@@ -0,0 +1 @@
+35250a5dd976e12dc0b834ad59b40a22462dd02a2d8d020d8315d41bbaf5f663
\ No newline at end of file
diff --git a/.vagrant/machines/default/docker/index_uuid b/.vagrant/machines/default/docker/index_uuid
new file mode 100644
index 0000000..633ca5a
--- /dev/null
+++ b/.vagrant/machines/default/docker/index_uuid
@@ -0,0 +1 @@
+38fdb046ba06412995172bb776cca87e
\ No newline at end of file
diff --git a/.vagrant/machines/default/docker/private_key b/.vagrant/machines/default/docker/private_key
new file mode 100644
index 0000000..e8501f1
--- /dev/null
+++ b/.vagrant/machines/default/docker/private_key
@@ -0,0 +1,27 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIEpQIBAAKCAQEAucV2btDL3NhCNwUzdbDwnSXQDtY5cVhnsQoH07GuwLqSj4DQ
+zd66YuuQaX0xfhdYkasdQKicZ6qLZhVvocy8PgEHpgjQdhJbM0HB95EwdrUS6ZSj
+4Zs+L7nc0QRdInSzPItw25C+8qcioOrkwu+ahL9TTyvFWWWFWHD8XOzi/kI7JEeJ
+jzrolBoJgN5lXggwaEIGQh6xanTwV6pyNASwDctNSEFHcDgqsKo8mjrxeZD6yiAh
+S0PxBcvPL8NapjVGvyHmWRdvcGU4MSijUo6wvazAIvGMqdCfv9XxRvhAbLy58ct8
+vyZcT/V+8E7F3ZCndTXKxtCGqNh+wZs1Kl/u0wIDAQABAoIBAQCTLloFtBFAOGpF
+ky5RGU2ZA8NSbfF21rbYcfz/gK/WbDr/zOwhn0wGYWG54gFbR/3Y8zwq5St9ioYE
+7AjUpROjAEfiCOu4EBUHiBq4HOTLt+xy+VvZu5hKUbQcOZvcV59F4agZnRVbxIVP
+/qods521fKvxdtlVWXSLPIEE0n2JOPBgBqgD8rMThhx2//L3dKaphC8HtXYwVCro
+/xSTrbDo3OulSL+DMptW0pa2cTCymnVaZfjCKy6BbZcF0Hxpf4GJhD0GUlBuU+0f
+YzQRzZOGQfZG3NGVepT3Aq/NP6pY/RAZWMqCWupwcBBmXZaPuO+79x10wxxmXWxQ
+Epst6HJBAoGBAN9rkceH9sba2BneAtgwVi300/fUR8Subc3jzDz0LpIPhPfeBafz
+IfS46gn6WNRImdHPX8kBQ0ttllLFD6ezbFHQ5g4mvmmCD6NaZ5riAVkuerCwhtuU
+p83u1N9JMRHQucXimFjxw9z3bWNC2pSNqOVAGt7FqON57PzTKwaDvzyzAoGBANTc
+b3NWKdfEU0lACnvhd/p7iogSZfmodRsAgxJzsp2ULwe520eM+Msk44QuxWEMkFAH
+XxtxTYhjkRrgjN3CSQcwC6mF4GSRAWNXXcMyiN5aDbS+X6pKxEHmlMuQ7YSK7P42
+xD8bInzD95CmDa46BZcP68aKB3iJwM/jWaYvc9VhAoGBANlw0L1350Yb4WwzdWNA
+j+9EMzQlBwA1nypdxP2hzN1ce1XdYHXXnDmX6jdxzhg03HelMxzmvL7hVgcSQS0+
+43IxNGWbcYAwE9Yw+1pzEUrhgIkMFQQKBtLW2ZjCnB4xnUwpP4p5Kd2ZdX3AqAki
+YblUjZI4nyldFbfuMRazDGEfAoGAFowbCh6QZBiZseKkuaaSbOf1LqC0SJO9g9S0
+DZpPyz1NFgZr4dJe8DXCG9hQdA0+pBuDyYZg7heN4Ujz4vGXhrliItzZfg2WFg3F
+Es4hjVwAo6qeu40b6Nch38ZEQovsuqjWdNDNAGZJrPrJ7DCdMvkuwmMQk4YT9HFi
+p6XTIUECgYEAhI3d0/l+kaIVN0XNBGB2JnP8a+XS8nJsQfCaundbCvoR38Nocrxk
+KP79gt33p3OI08Zu/wb/Yf8sFgTnjcBjbUXpKwBcRK8LFwImwsxZ/FekAKnvoGPo
+hX5n7ZzWJw8ak+8cEoZ7Roppj4F2os5i3qd2qvSoHKxPECVXOdD95V8=
+-----END RSA PRIVATE KEY-----
diff --git a/.vagrant/machines/default/docker/synced_folders b/.vagrant/machines/default/docker/synced_folders
new file mode 100644
index 0000000..c097d46
--- /dev/null
+++ b/.vagrant/machines/default/docker/synced_folders
@@ -0,0 +1 @@
+{"docker":{"/vagrant":{"guestpath":"/vagrant","hostpath":"/home/aron/src/ss/sonos/frontend","disabled":false,"__vagrantfile":true}}}
\ No newline at end of file
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
new file mode 100644
index 0000000..a02dddf
--- /dev/null
+++ b/CODE_OF_CONDUCT.md
@@ -0,0 +1,50 @@
+# Contributor Code of Conduct
+
+As contributors and maintainers of this project, and in the interest of
+fostering an open and welcoming community, we pledge to respect all people who
+contribute through reporting issues, posting feature requests, updating
+documentation, submitting pull requests or patches, and other activities.
+
+We are committed to making participation in this project a harassment-free
+experience for everyone, regardless of level of experience, gender, gender
+identity and expression, sexual orientation, disability, personal appearance,
+body size, race, ethnicity, age, religion, or nationality.
+
+Examples of unacceptable behavior by participants include:
+
+* The use of sexualized language or imagery
+* Personal attacks
+* Trolling or insulting/derogatory comments
+* Public or private harassment
+* Publishing other's private information, such as physical or electronic
+ addresses, without explicit permission
+* Other unethical or unprofessional conduct
+
+Project maintainers have the right and responsibility to remove, edit, or
+reject comments, commits, code, wiki edits, issues, and other contributions
+that are not aligned to this Code of Conduct, or to ban temporarily or
+permanently any contributor for other behaviors that they deem inappropriate,
+threatening, offensive, or harmful.
+
+By adopting this Code of Conduct, project maintainers commit themselves to
+fairly and consistently applying these principles to every aspect of managing
+this project. Project maintainers who do not follow or enforce the Code of
+Conduct may be permanently removed from the project team.
+
+This Code of Conduct applies both within project spaces and in public spaces
+when an individual is representing the project or its community.
+
+Instances of abusive, harassing, or otherwise unacceptable behavior may be
+reported by contacting the project maintainer at contact@mxstbr.com. All
+complaints will be reviewed and investigated and will result in a response that
+is deemed necessary and appropriate to the circumstances. Maintainers are
+obligated to maintain confidentiality with regard to the reporter of an
+incident.
+
+
+This Code of Conduct is adapted from the [Contributor Covenant][homepage],
+version 1.3.0, available at
+[http://contributor-covenant.org/version/1/3/0/][version]
+
+[homepage]: http://contributor-covenant.org
+[version]: http://contributor-covenant.org/version/1/3/0/
diff --git a/Changelog.md b/Changelog.md
new file mode 100644
index 0000000..077c426
--- /dev/null
+++ b/Changelog.md
@@ -0,0 +1,75 @@
+# Changelog
+
+## RBP v3: The "JS Fatigue Antivenin" Edition
+
+React Boilerplate (RBP) v3.0.0 is out, and it's a _complete_ rewrite! :tada:
+
+We've focused on becoming a rock-solid foundation to start your next project
+with, no matter what its scale. You get to focus on writing your app because we
+focus on making that as easy as pie.
+
+website!
+
+## Highlights
+
+- **Scaffolding**: Thanks to @somus, you can now run `npm run generate` in your
+ terminal and immediately create new components, containers, sagas, routes and
+ selectors! No more context switching, no more "Create new file, copy and paste
+ that boilerplate structure, bla bla": just `npm run generate ` and go.
+
+ Oh... and starting a project got a whole lot easier too: `npm run setup`. Done.
+
+- **Revamped architecture**: Following the incredible discussion in #27 (thanks
+ everybody for sharing your thoughts), we now have a weapons-grade, domain-driven
+ application architecture.
+
+ "Smart" containers are now isolated from stateless and/or generic components,
+ tests are now co-located with the code that they validate.
+
+- **New industry-standard JS utilities** We're now making the most of...
+ - ImmutableJS
+ - reselect
+ - react-router-redux
+ - redux-saga
+
+- **Huge CSS Improvements**
+ - _[CSS Modules](docs/css/css-modules.md)_: Finally, truly modular, reusable
+ styles!
+ - _Page-specific CSS_: smart Webpack configuration means that only the CSS
+ your components need is served
+ - _Standards rock:_ Nothing beats consistent styling so we beefed up the
+ quality checks with **[stylelint](docs/css/stylelint.md)** to help ensure
+ that you and your team stay on point.
+
+- **Performance**
+ - _Code splitting_: splitting/chunking by route means the leanest, meanest
+ payload (because the fastest code is the code you don't load!)
+ - _PageSpeed Metrics_ are built right in with `npm run pagespeed`
+
+- **Testing setup**: Thanks to @jbinto's herculean efforts, testing is now a
+ first-class citizen of this boilerplate. (the example app has _99% test coverage!_)
+ Karma and enzyme take care of unit testing, while ngrok tunnels your local
+ server for access from anywhere in the world – perfect for testing on different
+ devices in different locations.
+
+- **New server setup**: Thanks to the mighty @grabbou, we now use express.js to
+ give users a production-ready server right out of the box. Hot reloading is
+ still as available as always, but adding a custom API or a non-React page to
+ your application is now easier than ever :smile:
+
+- **Cleaner layout:** We've taken no prisoners with our approach to keeping your
+ code the star of the show: wherever possible, the new file layout keeps the
+ config in the background so that you can keep your focus where it needs to be.
+
+- **Documentation**: Thanks to @oliverturner, this boilerplate has some of the best
+ documentation going. Not just clearly explained usage guides, but easy-to-follow
+ _removal_ guides for most features too. RBP is just a launchpad: don't want to
+ use a bundled feature? Get rid of it quickly and easily without having to dig
+ through the code.
+
+- **Countless small improvements**: Everything, from linting pre-commit (thanks
+ @okonet!) to code splitting to cross-OS compatibility is now tested and ready
+ to go:
+
+ - We finally added a **[CoC](CODE_OF_CONDUCT.md)**
+ - Windows compatibility has improved massively
diff --git a/LICENSE.md b/LICENSE.md
new file mode 100644
index 0000000..39cb81e
--- /dev/null
+++ b/LICENSE.md
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2015 Maximilian Stoiber
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
\ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..90d55e0
--- /dev/null
+++ b/README.md
@@ -0,0 +1,158 @@
+
+
+
+
+
Start your next react project in seconds
+
A highly scalable, offline-first foundation with the best DX and a focus on performance and best practices
Create components, containers, routes, selectors and sagas - and their tests - right from the CLI!
+
+
Instant feedback
+
Enjoy the best DX (Developer eXperience) and code your app at the speed of thought! Your saved changes to the CSS and JS are reflected instantaneously without refreshing the page. Preserve application state even when you update something in the underlying code!
+
+
Predictable state management
+
Unidirectional data flow allows for change logging and time travel debugging.
+
+
Next generation JavaScript
+
Use template strings, object destructuring, arrow functions, JSX syntax and more, today.
+
+
Next generation CSS
+
Write composable CSS that's co-located with your components for complete modularity. Unique generated class names keep the specificity low while eliminating style clashes. Ship only the styles that are on the page for the best performance.
+
+
Industry-standard routing
+
It's natural to want to add pages (e.g. `/about`) to your application, and routing makes this possible.
+
+
Industry-standard i18n internationalization support
+
Scalable apps need to support multiple languages, easily add and support multiple languages with `react-intl`.
+
+
Offline-first
+
The next frontier in performant web apps: availability without a network connection from the instant your users load the app.
+
+
SEO
+
We support SEO (document head tags management) for search engines that support indexing of JavaScript content. (eg. Google)
+
+
+But wait... there's more!
+
+ - *The best test setup:* Automatically guarantee code quality and non-breaking
+ changes. (Seen a react app with 99% test coverage before?)
+ - *Native web app:* Your app's new home? The home screen of your users' phones.
+ - *The fastest fonts:* Say goodbye to vacant text.
+ - *Stay fast*: Profile your app's performance from the comfort of your command
+ line!
+ - *Catch problems:* AppVeyor and TravisCI setups included by default, so your
+ tests get run automatically on Windows and Unix.
+
+There’s also a fantastic video on how to structure your React.js apps with scalability in mind. It provides rationale for the majority of boilerplate's design decisions.
+
+Keywords: React.js, Redux, Hot Reloading, ESNext, Babel, PostCSS, Autoprefixer, react-router, Offline First, ServiceWorker, CSS Modules, redux-saga, FontFaceObserver, PageSpeed Insights
+
+## Quick start
+
+1. Clone this repo using `git clone --depth=1 https://github.com/mxstbr/react-boilerplate.git`
+1. Run `npm run setup` to install dependencies and clean the git repo.
+ *At this point you can run `npm start` to see the example app at `http://localhost:3000`.*
+1. Run `npm run clean` to delete the example app.
+
+Now you're ready to rumble!
+
+> Please note that this boilerplate is **not meant for beginners**! If you're just starting out with react or redux, please refer to https://github.com/petehunt/react-howto instead.
+
+## Documentation
+
+- [Intro](docs/general): What's included and why
+- [**Commands**](docs/general/commands.md): Getting the most out of this boilerplate
+- [Testing](docs/testing): How to work with the built-in test harness
+- [Styling](docs/css): How to work with the CSS tooling
+- [Your app](docs/js): Supercharging your app with Routing, Redux, simple
+ asynchronicity helpers, etc.
+
+## Supporters
+
+This project would not be possible without the support by these amazing folks. [**Become a sponsor**](https://opencollective.com/react-boilerplate) to get your company in front of thousands of engaged react developers and help us out!
+
+
+
+----
+
+
+
+
+
+
+
+
+
+
+
+
+----
+
+
+
+
+
+
+
+
+
+
+
+
+
+## License
+
+This project is licensed under the MIT license, Copyright (c) 2016 Maximilian
+Stoiber. For more information see `LICENSE.md`.
diff --git a/Vagrantfile b/Vagrantfile
new file mode 100644
index 0000000..caef2f9
--- /dev/null
+++ b/Vagrantfile
@@ -0,0 +1,13 @@
+# Load .env.bash -- backticks actually run /bin/sh so run bash inside.
+if File.exist?('.env.bash')
+ ENV.replace(eval `bash -ac "source .env.bash >/dev/null; ruby -e 'p ENV'"`)
+end
+
+Vagrant.configure("2") do |config|
+ config.vm.provider :docker do |docker, override|
+ docker.image = ENV.fetch("VAGRANT_DOCKER_IMAGE", "jesselang/debian-vagrant:jessie")
+ docker.has_ssh = true
+ end
+ config.ssh.forward_agent = true # for github in VM
+ config.vm.provision :shell, path: "provision.bash"
+end
diff --git a/app/.htaccess b/app/.htaccess
new file mode 100644
index 0000000..5204b4b
--- /dev/null
+++ b/app/.htaccess
@@ -0,0 +1,44 @@
+
+
+
+ #######################################################################
+ # GENERAL #
+ #######################################################################
+
+ # Make apache follow sym links to files
+ Options +FollowSymLinks
+ # If somebody opens a folder, hide all files from the resulting folder list
+ IndexIgnore */*
+
+
+ #######################################################################
+ # REWRITING #
+ #######################################################################
+
+ # Enable rewriting
+ RewriteEngine On
+
+ # If its not HTTPS
+ RewriteCond %{HTTPS} off
+
+ # Comment out the RewriteCond above, and uncomment the RewriteCond below if you're using a load balancer (e.g. CloudFlare) for SSL
+ # RewriteCond %{HTTP:X-Forwarded-Proto} !https
+
+ # Redirect to the same URL with https://, ignoring all further rules if this one is in effect
+ RewriteRule ^(.*) https://%{HTTP_HOST}/$1 [R,L]
+
+ # If we get to here, it means we are on https://
+
+ # If the file with the specified name in the browser doesn't exist
+ RewriteCond %{REQUEST_FILENAME} !-f
+
+ # and the directory with the specified name in the browser doesn't exist
+ RewriteCond %{REQUEST_FILENAME} !-d
+
+ # and we are not opening the root already (otherwise we get a redirect loop)
+ RewriteCond %{REQUEST_FILENAME} !\/$
+
+ # Rewrite all requests to the root
+ RewriteRule ^(.*) /
+
+
diff --git a/app/.nginx.conf b/app/.nginx.conf
new file mode 100644
index 0000000..cf6b6ca
--- /dev/null
+++ b/app/.nginx.conf
@@ -0,0 +1,66 @@
+##
+# Put this file in /etc/nginx/conf.d folder and make sure
+# you have line 'include /etc/nginx/conf.d/*.conf;'
+# in your main nginx configuration file
+##
+
+##
+# Redirect to the same URL with https://
+##
+
+server {
+
+ listen 80;
+
+# Type your domain name below
+ server_name example.com;
+
+ return 301 https://$server_name$request_uri;
+
+}
+
+##
+# HTTPS configurations
+##
+
+server {
+
+ listen 443;
+
+# Type your domain name below
+ server_name example.com;
+
+ ssl on;
+ ssl_certificate /path/to/certificate.crt;
+ ssl_certificate_key /path/to/server.key;
+
+# Use only TSL protocols for more secure
+ ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
+
+# Always serve index.html for any request
+ location / {
+ # Set path
+ root /var/www/;
+ try_files $uri /index.html;
+ }
+
+##
+# If you want to use Node/Rails/etc. API server
+# on the same port (443) config Nginx as a reverse proxy.
+# For security reasons use a firewall like ufw in Ubuntu
+# and deny port 3000/tcp.
+##
+
+# location /api/ {
+#
+# proxy_pass http://localhost:3000;
+# proxy_http_version 1.1;
+# proxy_set_header X-Forwarded-Proto https;
+# proxy_set_header Upgrade $http_upgrade;
+# proxy_set_header Connection 'upgrade';
+# proxy_set_header Host $host;
+# proxy_cache_bypass $http_upgrade;
+#
+# }
+
+}
diff --git a/app/app.js b/app/app.js
new file mode 100644
index 0000000..13b72ca
--- /dev/null
+++ b/app/app.js
@@ -0,0 +1,127 @@
+/**
+ * app.js
+ *
+ * This is the entry file for the application, only setup and boilerplate
+ * code.
+ */
+
+// Needed for redux-saga es6 generator support
+import 'babel-polyfill';
+
+/* eslint-disable import/no-unresolved */
+// Load the favicon, the manifest.json file and the .htaccess file
+import 'file?name=[name].[ext]!./favicon.ico';
+import '!file?name=[name].[ext]!./manifest.json';
+import 'file?name=[name].[ext]!./.htaccess';
+/* eslint-enable import/no-unresolved */
+
+// Import all the third party stuff
+import React from 'react';
+import ReactDOM from 'react-dom';
+import { Provider } from 'react-redux';
+import { applyRouterMiddleware, Router, browserHistory } from 'react-router';
+import { syncHistoryWithStore } from 'react-router-redux';
+import FontFaceObserver from 'fontfaceobserver';
+import { useScroll } from 'react-router-scroll';
+import configureStore from './store';
+
+// Import Language Provider
+import LanguageProvider from 'containers/LanguageProvider';
+
+// Observe loading of Open Sans (to remove open sans, remove the tag in
+// the index.html file and this observer)
+import styles from 'containers/App/styles.css';
+const openSansObserver = new FontFaceObserver('Open Sans', {});
+
+// When Open Sans is loaded, add a font-family using Open Sans to the body
+openSansObserver.load().then(() => {
+ document.body.classList.add(styles.fontLoaded);
+}, () => {
+ document.body.classList.remove(styles.fontLoaded);
+});
+
+// Import i18n messages
+import { translationMessages } from './i18n';
+
+// Create redux store with history
+// this uses the singleton browserHistory provided by react-router
+// Optionally, this could be changed to leverage a created history
+// e.g. `const browserHistory = useRouterHistory(createBrowserHistory)();`
+const initialState = {};
+const store = configureStore(initialState, browserHistory);
+
+// If you use Redux devTools extension, since v2.0.1, they added an
+// `updateStore`, so any enhancers that change the store object
+// could be used with the devTools' store.
+// As this boilerplate uses Redux & Redux-Saga, the `updateStore` is needed
+// if you want to `take` actions in your Sagas, dispatched from devTools.
+if (window.devToolsExtension) {
+ window.devToolsExtension.updateStore(store);
+}
+
+// Sync history and store, as the react-router-redux reducer
+// is under the non-default key ("routing"), selectLocationState
+// must be provided for resolving how to retrieve the "route" in the state
+import { selectLocationState } from 'containers/App/selectors';
+const history = syncHistoryWithStore(browserHistory, store, {
+ selectLocationState: selectLocationState(),
+});
+
+// Set up the router, wrapping all Routes in the App component
+import App from 'containers/App';
+import createRoutes from './routes';
+const rootRoute = {
+ component: App,
+ childRoutes: createRoutes(store),
+};
+
+const render = (messages) => {
+ ReactDOM.render(
+
+
+
+
+ ,
+ document.getElementById('app')
+ );
+};
+
+// Hot reloadable translation json files
+if (module.hot) {
+ // modules.hot.accept does not accept dynamic dependencies,
+ // have to be constants at compile-time
+ module.hot.accept('./i18n', () => {
+ render(translationMessages);
+ });
+}
+
+// Chunked polyfill for browsers without Intl support
+if (!window.Intl) {
+ (new Promise((resolve) => {
+ resolve(System.import('intl'));
+ }))
+ .then(() => Promise.all([
+ System.import('intl/locale-data/jsonp/en.js'),
+ System.import('intl/locale-data/jsonp/de.js'),
+ ]))
+ .then(() => render(translationMessages))
+ .catch((err) => {
+ throw err;
+ });
+} else {
+ render(translationMessages);
+}
+
+// Install ServiceWorker and AppCache in the end since
+// it's not most important operation and if main code fails,
+// we do not want it installed
+import { install } from 'offline-plugin/runtime';
+install();
diff --git a/app/components/A/index.js b/app/components/A/index.js
new file mode 100644
index 0000000..62634e9
--- /dev/null
+++ b/app/components/A/index.js
@@ -0,0 +1,30 @@
+/**
+ * A link to a certain page, an anchor tag
+ */
+
+import React, { PropTypes } from 'react';
+
+import styles from './styles.css';
+
+function A({ ...props }) {
+ const { className, children, href, target, ...rest } = props;
+ return (
+
+ { children }
+
+ );
+}
+
+A.propTypes = {
+ className: PropTypes.string,
+ href: PropTypes.string.isRequired,
+ target: PropTypes.string,
+ children: PropTypes.node.isRequired,
+};
+
+export default A;
diff --git a/app/components/A/styles.css b/app/components/A/styles.css
new file mode 100644
index 0000000..f69b7bc
--- /dev/null
+++ b/app/components/A/styles.css
@@ -0,0 +1,7 @@
+.link {
+ color: #41ADDD;
+}
+
+.link:hover {
+ color: #6CC0E5;
+}
diff --git a/app/components/A/tests/index.test.js b/app/components/A/tests/index.test.js
new file mode 100644
index 0000000..e8161e0
--- /dev/null
+++ b/app/components/A/tests/index.test.js
@@ -0,0 +1,52 @@
+/**
+ * Testing our link component
+ */
+
+import A from '../index';
+
+import expect from 'expect';
+import { shallow } from 'enzyme';
+import React from 'react';
+
+const href = 'http://mxstbr.com/';
+const children = (
+ );
+ }
+}
+
+FeaturePage.propTypes = {
+ dispatch: React.PropTypes.func,
+};
+
+export default connect()(FeaturePage);
diff --git a/app/containers/FeaturePage/messages.js b/app/containers/FeaturePage/messages.js
new file mode 100644
index 0000000..96ffec9
--- /dev/null
+++ b/app/containers/FeaturePage/messages.js
@@ -0,0 +1,99 @@
+/*
+ * FeaturePage Messages
+ *
+ * This contains all the text for the FeaturePage component.
+ */
+import { defineMessages } from 'react-intl';
+
+export default defineMessages({
+ header: {
+ id: 'boilerplate.containers.FeaturePage.header',
+ defaultMessage: 'Features',
+ },
+ scaffoldingHeader: {
+ id: 'boilerplate.containers.FeaturePage.scaffolding.header',
+ defaultMessage: 'Quick scaffolding',
+ },
+ scaffoldingMessage: {
+ id: 'boilerplate.containers.FeaturePage.scaffolding.message',
+ defaultMessage: `Automate the creation of components, containers, routes, selectors
+ and sagas - and their tests - right from the CLI!`,
+ },
+ feedbackHeader: {
+ id: 'boilerplate.containers.FeaturePage.feedback.header',
+ defaultMessage: 'Instant feedback',
+ },
+ feedbackMessage: {
+ id: 'boilerplate.containers.FeaturePage.feedback.message',
+ defaultMessage: `
+ Enjoy the best DX and code your app at the speed of thought! Your
+ saved changes to the CSS and JS are reflected instantaneously
+ without refreshing the page. Preserve application state even when
+ you update something in the underlying code!
+ `,
+ },
+ stateManagementHeader: {
+ id: 'boilerplate.containers.FeaturePage.state_management.header',
+ defaultMessage: 'Predictable state management',
+ },
+ stateManagementMessages: {
+ id: 'boilerplate.containers.FeaturePage.state_management.message',
+ defaultMessage: `
+ Unidirectional data flow allows for change logging and time travel
+ debugging.
+ `,
+ },
+ javascriptHeader: {
+ id: 'boilerplate.containers.FeaturePage.javascript.header',
+ defaultMessage: 'Next generation JavaScript',
+ },
+ javascriptMessage: {
+ id: 'boilerplate.containers.FeaturePage.javascript.message',
+ defaultMessage: `Use template strings, object destructuring, arrow functions, JSX
+ syntax and more, today.`,
+ },
+ cssHeader: {
+ id: 'boilerplate.containers.FeaturePage.css.header',
+ defaultMessage: 'Features',
+ },
+ cssMessage: {
+ id: 'boilerplate.containers.FeaturePage.css.message',
+ defaultMessage: 'Next generation CSS',
+ },
+ routingHeader: {
+ id: 'boilerplate.containers.FeaturePage.routing.header',
+ defaultMessage: 'Industry-standard routing',
+ },
+ routingMessage: {
+ id: 'boilerplate.containers.FeaturePage.routing.message',
+ defaultMessage: `
+ Write composable CSS that's co-located with your components for
+ complete modularity. Unique generated class names keep the
+ specificity low while eliminating style clashes. Ship only the
+ styles that are on the page for the best performance.
+ `,
+ },
+ networkHeader: {
+ id: 'boilerplate.containers.FeaturePage.network.header',
+ defaultMessage: 'Offline-first',
+ },
+ networkMessage: {
+ id: 'boilerplate.containers.FeaturePage.network.message',
+ defaultMessage: `
+ The next frontier in performant web apps: availability without a
+ network connection from the instant your users load the app.
+ `,
+ },
+ intlHeader: {
+ id: 'boilerplate.containers.FeaturePage.internationalization.header',
+ defaultMessage: 'Complete i18n Standard Internationalization & Pluralization',
+ },
+ intlMessage: {
+ id: 'boilerplate.containers.FeaturePage.internationalization.message',
+ defaultMessage: 'Scalable apps need to support multiple languages, easily add and support multiple languages with `react-intl`.',
+ },
+ homeButton: {
+ id: 'boilerplate.containers.FeaturePage.home',
+ defaultMessage: 'Home',
+ },
+});
diff --git a/app/containers/FeaturePage/styles.css b/app/containers/FeaturePage/styles.css
new file mode 100644
index 0000000..e291798
--- /dev/null
+++ b/app/containers/FeaturePage/styles.css
@@ -0,0 +1,12 @@
+.list {
+ font-family: Georgia, Times, 'Times New Roman', serif;
+ padding-left: 1.75em;
+}
+
+.listItem {
+ margin: 1em 0;
+}
+
+.listItemTitle {
+ font-weight: bold;
+}
diff --git a/app/containers/FeaturePage/tests/index.test.js b/app/containers/FeaturePage/tests/index.test.js
new file mode 100644
index 0000000..49fca03
--- /dev/null
+++ b/app/containers/FeaturePage/tests/index.test.js
@@ -0,0 +1,36 @@
+import expect from 'expect';
+import { shallow } from 'enzyme';
+import React from 'react';
+
+import Button from 'components/Button';
+import { FormattedMessage } from 'react-intl';
+import messages from '../messages';
+import { FeaturePage } from '../index';
+import H1 from 'components/H1';
+
+describe('', () => {
+ it('should render its heading', () => {
+ const renderedComponent = shallow(
+
+ );
+ expect(renderedComponent.contains(
+
+
+
+ )).toEqual(true);
+ });
+
+ it('should link to "/"', (done) => {
+ // Spy on the openRoute method of the FeaturePage
+ const dispatch = (action) => {
+ expect(action.payload.args).toEqual('/');
+ done();
+ };
+
+ const renderedComponent = shallow(
+
+ );
+ const button = renderedComponent.find(Button);
+ button.prop('handleRoute')();
+ });
+});
diff --git a/app/containers/HomePage/actions.js b/app/containers/HomePage/actions.js
new file mode 100644
index 0000000..7706bbb
--- /dev/null
+++ b/app/containers/HomePage/actions.js
@@ -0,0 +1,34 @@
+/*
+ * Home Actions
+ *
+ * Actions change things in your application
+ * Since this boilerplate uses a uni-directional data flow, specifically redux,
+ * we have these actions which are the only way your application interacts with
+ * your application state. This guarantees that your state is up to date and nobody
+ * messes it up weirdly somewhere.
+ *
+ * To add a new Action:
+ * 1) Import your constant
+ * 2) Add a function like this:
+ * export function yourAction(var) {
+ * return { type: YOUR_ACTION_CONSTANT, var: var }
+ * }
+ */
+
+import {
+ CHANGE_USERNAME,
+} from './constants';
+
+/**
+ * Changes the input field of the form
+ *
+ * @param {name} name The new text of the input field
+ *
+ * @return {object} An action object with a type of CHANGE_USERNAME
+ */
+export function changeUsername(name) {
+ return {
+ type: CHANGE_USERNAME,
+ name,
+ };
+}
diff --git a/app/containers/HomePage/constants.js b/app/containers/HomePage/constants.js
new file mode 100644
index 0000000..640f918
--- /dev/null
+++ b/app/containers/HomePage/constants.js
@@ -0,0 +1,12 @@
+/*
+ * HomeConstants
+ * Each action has a corresponding type, which the reducer knows and picks up on.
+ * To avoid weird typos between the reducer and the actions, we save them as
+ * constants here. We prefix them with 'yourproject/YourComponent' so we avoid
+ * reducers accidentally picking up actions they shouldn't.
+ *
+ * Follow this format:
+ * export const YOUR_ACTION_CONSTANT = 'yourproject/YourContainer/YOUR_ACTION_CONSTANT';
+ */
+
+export const CHANGE_USERNAME = 'boilerplate/Home/CHANGE_USERNAME';
diff --git a/app/containers/HomePage/index.js b/app/containers/HomePage/index.js
new file mode 100644
index 0000000..835fcdf
--- /dev/null
+++ b/app/containers/HomePage/index.js
@@ -0,0 +1,167 @@
+/*
+ * HomePage
+ *
+ * This is the first thing users see of our App, at the '/' route
+ */
+
+import React from 'react';
+import { connect } from 'react-redux';
+import { push } from 'react-router-redux';
+import Helmet from 'react-helmet';
+
+import messages from './messages';
+import { createStructuredSelector } from 'reselect';
+
+import {
+ selectRepos,
+ selectLoading,
+ selectError,
+} from 'containers/App/selectors';
+
+import {
+ selectUsername,
+} from './selectors';
+
+import { changeUsername } from './actions';
+import { loadRepos } from '../App/actions';
+
+import { FormattedMessage } from 'react-intl';
+import RepoListItem from 'containers/RepoListItem';
+import Button from 'components/Button';
+import H2 from 'components/H2';
+import List from 'components/List';
+import ListItem from 'components/ListItem';
+import LoadingIndicator from 'components/LoadingIndicator';
+
+import styles from './styles.css';
+
+export class HomePage extends React.Component {
+ /**
+ * when initial state username is not null, submit the form to load repos
+ */
+ componentDidMount() {
+ if (this.props.username && this.props.username.trim().length > 0) {
+ this.props.onSubmitForm();
+ }
+ }
+ /**
+ * Changes the route
+ *
+ * @param {string} route The route we want to go to
+ */
+ openRoute = (route) => {
+ this.props.changeRoute(route);
+ };
+
+ /**
+ * Changed route to '/features'
+ */
+ openFeaturesPage = () => {
+ this.openRoute('/features');
+ };
+
+ render() {
+ let mainContent = null;
+
+ // Show a loading indicator when we're loading
+ if (this.props.loading) {
+ mainContent = ();
+
+ // Show an error if there is one
+ } else if (this.props.error !== false) {
+ const ErrorComponent = () => (
+
+ );
+ mainContent = ();
+
+ // If we're not loading, don't have an error and there are repos, show the repos
+ } else if (this.props.repos !== false) {
+ mainContent = ();
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {mainContent}
+
+
+
+
+ );
+ }
+}
+
+HomePage.propTypes = {
+ changeRoute: React.PropTypes.func,
+ loading: React.PropTypes.bool,
+ error: React.PropTypes.oneOfType([
+ React.PropTypes.object,
+ React.PropTypes.bool,
+ ]),
+ repos: React.PropTypes.oneOfType([
+ React.PropTypes.array,
+ React.PropTypes.bool,
+ ]),
+ onSubmitForm: React.PropTypes.func,
+ username: React.PropTypes.string,
+ onChangeUsername: React.PropTypes.func,
+};
+
+export function mapDispatchToProps(dispatch) {
+ return {
+ onChangeUsername: (evt) => dispatch(changeUsername(evt.target.value)),
+ changeRoute: (url) => dispatch(push(url)),
+ onSubmitForm: (evt) => {
+ if (evt !== undefined && evt.preventDefault) evt.preventDefault();
+ dispatch(loadRepos());
+ },
+
+ dispatch,
+ };
+}
+
+const mapStateToProps = createStructuredSelector({
+ repos: selectRepos(),
+ username: selectUsername(),
+ loading: selectLoading(),
+ error: selectError(),
+});
+
+// Wrap the component to inject dispatch and state into it
+export default connect(mapStateToProps, mapDispatchToProps)(HomePage);
diff --git a/app/containers/HomePage/messages.js b/app/containers/HomePage/messages.js
new file mode 100644
index 0000000..f2113a7
--- /dev/null
+++ b/app/containers/HomePage/messages.js
@@ -0,0 +1,33 @@
+/*
+ * HomePage Messages
+ *
+ * This contains all the text for the HomePage component.
+ */
+import { defineMessages } from 'react-intl';
+
+export default defineMessages({
+ startProjectHeader: {
+ id: 'boilerplate.containers.HomePage.start_project.header',
+ defaultMessage: 'Start your next react project in seconds',
+ },
+ startProjectMessage: {
+ id: 'boilerplate.containers.HomePage.start_project.message',
+ defaultMessage: 'A highly scalable, offline-first foundation with the best DX and a focus on performance and best practices',
+ },
+ trymeHeader: {
+ id: 'boilerplate.containers.HomePage.tryme.header',
+ defaultMessage: 'Try me!',
+ },
+ trymeMessage: {
+ id: 'boilerplate.containers.HomePage.tryme.message',
+ defaultMessage: 'Show Github repositories by',
+ },
+ trymeAtPrefix: {
+ id: 'boilerplate.containers.HomePage.tryme.atPrefix',
+ defaultMessage: '@',
+ },
+ featuresButton: {
+ id: 'boilerplate.containers.HomePage.features.Button',
+ defaultMessage: 'Features',
+ },
+});
diff --git a/app/containers/HomePage/reducer.js b/app/containers/HomePage/reducer.js
new file mode 100644
index 0000000..0ac28b8
--- /dev/null
+++ b/app/containers/HomePage/reducer.js
@@ -0,0 +1,35 @@
+/*
+ * HomeReducer
+ *
+ * The reducer takes care of our data. Using actions, we can change our
+ * application state.
+ * To add a new action, add it to the switch statement in the reducer function
+ *
+ * Example:
+ * case YOUR_ACTION_CONSTANT:
+ * return state.set('yourStateVariable', true);
+ */
+
+import {
+ CHANGE_USERNAME,
+} from './constants';
+import { fromJS } from 'immutable';
+
+// The initial state of the App
+const initialState = fromJS({
+ username: '',
+});
+
+function homeReducer(state = initialState, action) {
+ switch (action.type) {
+ case CHANGE_USERNAME:
+
+ // Delete prefixed '@' from the github username
+ return state
+ .set('username', action.name.replace(/@/gi, ''));
+ default:
+ return state;
+ }
+}
+
+export default homeReducer;
diff --git a/app/containers/HomePage/sagas.js b/app/containers/HomePage/sagas.js
new file mode 100644
index 0000000..6f955ac
--- /dev/null
+++ b/app/containers/HomePage/sagas.js
@@ -0,0 +1,55 @@
+/**
+ * Gets the repositories of the user from Github
+ */
+
+import { take, call, put, select, fork, cancel } from 'redux-saga/effects';
+import { LOCATION_CHANGE } from 'react-router-redux';
+import { LOAD_REPOS } from 'containers/App/constants';
+import { reposLoaded, repoLoadingError } from 'containers/App/actions';
+
+import request from 'utils/request';
+import { selectUsername } from 'containers/HomePage/selectors';
+
+/**
+ * Github repos request/response handler
+ */
+export function* getRepos() {
+ // Select username from store
+ const username = yield select(selectUsername());
+ const requestURL = `https://api.github.com/users/${username}/repos?type=all&sort=updated`;
+
+ // Call our request helper (see 'utils/request')
+ const repos = yield call(request, requestURL);
+
+ if (!repos.err) {
+ yield put(reposLoaded(repos.data, username));
+ } else {
+ yield put(repoLoadingError(repos.err));
+ }
+}
+
+/**
+ * Watches for LOAD_REPOS action and calls handler
+ */
+export function* getReposWatcher() {
+ while (yield take(LOAD_REPOS)) {
+ yield call(getRepos);
+ }
+}
+
+/**
+ * Root saga manages watcher lifecycle
+ */
+export function* githubData() {
+ // Fork watcher so we can continue execution
+ const watcher = yield fork(getReposWatcher);
+
+ // Suspend execution until location changes
+ yield take(LOCATION_CHANGE);
+ yield cancel(watcher);
+}
+
+// Bootstrap sagas
+export default [
+ githubData,
+];
diff --git a/app/containers/HomePage/selectors.js b/app/containers/HomePage/selectors.js
new file mode 100644
index 0000000..fdad1c6
--- /dev/null
+++ b/app/containers/HomePage/selectors.js
@@ -0,0 +1,17 @@
+/**
+ * Homepage selectors
+ */
+
+import { createSelector } from 'reselect';
+
+const selectHome = () => (state) => state.get('home');
+
+const selectUsername = () => createSelector(
+ selectHome(),
+ (homeState) => homeState.get('username')
+);
+
+export {
+ selectHome,
+ selectUsername,
+};
diff --git a/app/containers/HomePage/styles.css b/app/containers/HomePage/styles.css
new file mode 100644
index 0000000..258f155
--- /dev/null
+++ b/app/containers/HomePage/styles.css
@@ -0,0 +1,35 @@
+.textSection {
+ margin: 3em auto;
+}
+
+.textSection:first-child {
+ margin-top: 0;
+}
+
+.centered {
+ text-align: center;
+}
+
+p,
+label {
+ font-family: Georgia, Times, 'Times New Roman', serif;
+ line-height: 1.5em;
+}
+
+.link {
+ text-decoration: none;
+}
+
+.usernameForm {
+ margin-bottom: 1em;
+}
+
+.input {
+ outline: none;
+ border-bottom: 1px dotted #999;
+}
+
+.atPrefix {
+ color: black;
+ margin-left: 0.4em;
+}
diff --git a/app/containers/HomePage/tests/actions.test.js b/app/containers/HomePage/tests/actions.test.js
new file mode 100644
index 0000000..f04baa6
--- /dev/null
+++ b/app/containers/HomePage/tests/actions.test.js
@@ -0,0 +1,23 @@
+import expect from 'expect';
+
+import {
+ CHANGE_USERNAME,
+} from '../constants';
+
+import {
+ changeUsername,
+} from '../actions';
+
+describe('Home Actions', () => {
+ describe('changeUsername', () => {
+ it('should return the correct type and the passed name', () => {
+ const fixture = 'Max';
+ const expectedResult = {
+ type: CHANGE_USERNAME,
+ name: fixture,
+ };
+
+ expect(changeUsername(fixture)).toEqual(expectedResult);
+ });
+ });
+});
diff --git a/app/containers/HomePage/tests/index.test.js b/app/containers/HomePage/tests/index.test.js
new file mode 100644
index 0000000..c189a9d
--- /dev/null
+++ b/app/containers/HomePage/tests/index.test.js
@@ -0,0 +1,152 @@
+/**
+ * Test the HomePage
+ */
+
+import expect from 'expect';
+import { shallow, mount } from 'enzyme';
+import React from 'react';
+
+import { IntlProvider } from 'react-intl';
+import { HomePage, mapDispatchToProps } from '../index';
+import { changeUsername } from '../actions';
+import { loadRepos } from '../../App/actions';
+import { push } from 'react-router-redux';
+import RepoListItem from 'containers/RepoListItem';
+import List from 'components/List';
+import LoadingIndicator from 'components/LoadingIndicator';
+
+describe('', () => {
+ it('should render the loading indicator when its loading', () => {
+ const renderedComponent = shallow(
+
+ );
+ expect(renderedComponent.contains()).toEqual(true);
+ });
+
+ it('should render an error if loading failed', () => {
+ const renderedComponent = mount(
+
+
+
+ );
+ expect(
+ renderedComponent
+ .text()
+ .indexOf('Something went wrong, please try again!')
+ ).toBeGreaterThan(-1);
+ });
+
+ it('should render fetch the repos on mount if a username exists', () => {
+ const submitSpy = expect.createSpy();
+ mount(
+
+ {}}
+ onSubmitForm={submitSpy}
+ />
+
+ );
+ expect(submitSpy).toHaveBeenCalled();
+ });
+
+ it('should render the repositories if loading was successful', () => {
+ const repos = [{
+ owner: {
+ login: 'mxstbr',
+ },
+ html_url: 'https://github.com/mxstbr/react-boilerplate',
+ name: 'react-boilerplate',
+ open_issues_count: 20,
+ full_name: 'mxstbr/react-boilerplate',
+ }];
+ const renderedComponent = shallow(
+
+ );
+
+ expect(renderedComponent.contains()).toEqual(true);
+ });
+
+ it('should link to /features', () => {
+ const openRouteSpy = expect.createSpy();
+
+ // Spy on the openRoute method of the HomePage
+ const openRoute = (dest) => {
+ if (dest === '/features') {
+ openRouteSpy();
+ }
+ };
+
+ const renderedComponent = mount(
+
+
+
+ );
+ const button = renderedComponent.find('button');
+ button.simulate('click');
+ expect(openRouteSpy).toHaveBeenCalled();
+ });
+
+ describe('mapDispatchToProps', () => {
+ describe('onChangeUsername', () => {
+ it('should be injected', () => {
+ const dispatch = expect.createSpy();
+ const result = mapDispatchToProps(dispatch);
+ expect(result.onChangeUsername).toExist();
+ });
+
+ it('should dispatch changeUsername when called', () => {
+ const dispatch = expect.createSpy();
+ const result = mapDispatchToProps(dispatch);
+ const username = 'mxstbr';
+ result.onChangeUsername({ target: { value: username } });
+ expect(dispatch).toHaveBeenCalledWith(changeUsername(username));
+ });
+ });
+ });
+
+ describe('changeRoute', () => {
+ it('should be injected', () => {
+ const dispatch = expect.createSpy();
+ const result = mapDispatchToProps(dispatch);
+ expect(result.changeRoute).toExist();
+ });
+
+ it('should dispatch push when called', () => {
+ const dispatch = expect.createSpy();
+ const result = mapDispatchToProps(dispatch);
+ const route = '/';
+ result.changeRoute(route);
+ expect(dispatch).toHaveBeenCalledWith(push(route));
+ });
+ });
+
+ describe('onSubmitForm', () => {
+ it('should be injected', () => {
+ const dispatch = expect.createSpy();
+ const result = mapDispatchToProps(dispatch);
+ expect(result.onSubmitForm).toExist();
+ });
+
+ it('should dispatch loadRepos when called', () => {
+ const dispatch = expect.createSpy();
+ const result = mapDispatchToProps(dispatch);
+ result.onSubmitForm();
+ expect(dispatch).toHaveBeenCalledWith(loadRepos());
+ });
+
+ it('should preventDefault if called with event', () => {
+ const preventDefault = expect.createSpy();
+ const result = mapDispatchToProps(() => {});
+ const evt = { preventDefault };
+ result.onSubmitForm(evt);
+ expect(preventDefault).toHaveBeenCalledWith();
+ });
+ });
+});
diff --git a/app/containers/HomePage/tests/reducer.test.js b/app/containers/HomePage/tests/reducer.test.js
new file mode 100644
index 0000000..c0f38dd
--- /dev/null
+++ b/app/containers/HomePage/tests/reducer.test.js
@@ -0,0 +1,27 @@
+import expect from 'expect';
+import homeReducer from '../reducer';
+import {
+ changeUsername,
+} from '../actions';
+import { fromJS } from 'immutable';
+
+describe('homeReducer', () => {
+ let state;
+ beforeEach(() => {
+ state = fromJS({
+ username: '',
+ });
+ });
+
+ it('should return the initial state', () => {
+ const expectedResult = state;
+ expect(homeReducer(undefined, {})).toEqual(expectedResult);
+ });
+
+ it('should handle the changeUsername action correctly', () => {
+ const fixture = 'mxstbr';
+ const expectedResult = state.set('username', fixture);
+
+ expect(homeReducer(state, changeUsername(fixture))).toEqual(expectedResult);
+ });
+});
diff --git a/app/containers/HomePage/tests/sagas.test.js b/app/containers/HomePage/tests/sagas.test.js
new file mode 100644
index 0000000..49537ae
--- /dev/null
+++ b/app/containers/HomePage/tests/sagas.test.js
@@ -0,0 +1,92 @@
+/**
+ * Tests for HomePage sagas
+ */
+
+import expect from 'expect';
+import { take, call, put, select, fork, cancel } from 'redux-saga/effects';
+import { LOCATION_CHANGE } from 'react-router-redux';
+
+import { getRepos, getReposWatcher, githubData } from '../sagas';
+
+import { LOAD_REPOS } from 'containers/App/constants';
+import { reposLoaded, repoLoadingError } from 'containers/App/actions';
+
+import request from 'utils/request';
+import { selectUsername } from 'containers/HomePage/selectors';
+
+const username = 'mxstbr';
+
+describe('getRepos Saga', () => {
+ let getReposGenerator;
+
+ // We have to test twice, once for a successful load and once for an unsuccessful one
+ // so we do all the stuff that happens beforehand automatically in the beforeEach
+ beforeEach(() => {
+ getReposGenerator = getRepos();
+
+ const selectDescriptor = getReposGenerator.next().value;
+ expect(selectDescriptor).toEqual(select(selectUsername()));
+
+ const requestURL = `https://api.github.com/users/${username}/repos?type=all&sort=updated`;
+ const callDescriptor = getReposGenerator.next(username).value;
+ expect(callDescriptor).toEqual(call(request, requestURL));
+ });
+
+ it('should dispatch the reposLoaded action if it requests the data successfully', () => {
+ const response = {
+ data: [{
+ name: 'First repo',
+ }, {
+ name: 'Second repo',
+ }],
+ };
+ const putDescriptor = getReposGenerator.next(response).value;
+ expect(putDescriptor).toEqual(put(reposLoaded(response.data, username)));
+ });
+
+ it('should call the repoLoadingError action if the response errors', () => {
+ const response = {
+ err: 'Some error',
+ };
+ const putDescriptor = getReposGenerator.next(response).value;
+ expect(putDescriptor).toEqual(put(repoLoadingError(response.err)));
+ });
+});
+
+describe('getReposWatcher Saga', () => {
+ const getReposWatcherGenerator = getReposWatcher();
+
+ it('should watch for LOAD_REPOS action', () => {
+ const takeDescriptor = getReposWatcherGenerator.next().value;
+ expect(takeDescriptor).toEqual(take(LOAD_REPOS));
+ });
+
+ it('should invoke getRepos saga on actions', () => {
+ const callDescriptor = getReposWatcherGenerator.next(put(LOAD_REPOS)).value;
+ expect(callDescriptor).toEqual(call(getRepos));
+ });
+});
+
+describe('githubDataSaga Saga', () => {
+ const githubDataSaga = githubData();
+
+ let forkDescriptor;
+
+ it('should asyncronously fork getReposWatcher saga', () => {
+ forkDescriptor = githubDataSaga.next();
+ expect(forkDescriptor.value).toEqual(fork(getReposWatcher));
+ });
+
+ it('should yield until LOCATION_CHANGE action', () => {
+ const takeDescriptor = githubDataSaga.next();
+ expect(takeDescriptor.value).toEqual(take(LOCATION_CHANGE));
+ });
+
+ it('should finally cancel() the forked getReposWatcher saga',
+ function* githubDataSagaCancellable() {
+ // reuse open fork for more integrated approach
+ forkDescriptor = githubDataSaga.next(put(LOCATION_CHANGE));
+ expect(forkDescriptor.value).toEqual(cancel(forkDescriptor));
+ }
+ );
+});
diff --git a/app/containers/HomePage/tests/selectors.test.js b/app/containers/HomePage/tests/selectors.test.js
new file mode 100644
index 0000000..c1a9b5f
--- /dev/null
+++ b/app/containers/HomePage/tests/selectors.test.js
@@ -0,0 +1,33 @@
+import { fromJS } from 'immutable';
+import expect from 'expect';
+
+import {
+ selectHome,
+ selectUsername,
+} from '../selectors';
+
+describe('selectHome', () => {
+ const homeSelector = selectHome();
+ it('should select the home state', () => {
+ const homeState = fromJS({
+ userData: {},
+ });
+ const mockedState = fromJS({
+ home: homeState,
+ });
+ expect(homeSelector(mockedState)).toEqual(homeState);
+ });
+});
+
+describe('selectUsername', () => {
+ const usernameSelector = selectUsername();
+ it('should select the username', () => {
+ const username = 'mxstbr';
+ const mockedState = fromJS({
+ home: {
+ username,
+ },
+ });
+ expect(usernameSelector(mockedState)).toEqual(username);
+ });
+});
diff --git a/app/containers/LanguageProvider/actions.js b/app/containers/LanguageProvider/actions.js
new file mode 100644
index 0000000..779eed0
--- /dev/null
+++ b/app/containers/LanguageProvider/actions.js
@@ -0,0 +1,16 @@
+/*
+ *
+ * LanguageProvider actions
+ *
+ */
+
+import {
+ CHANGE_LOCALE,
+} from './constants';
+
+export function changeLocale(languageLocale) {
+ return {
+ type: CHANGE_LOCALE,
+ locale: languageLocale,
+ };
+}
diff --git a/app/containers/LanguageProvider/constants.js b/app/containers/LanguageProvider/constants.js
new file mode 100644
index 0000000..7d2f425
--- /dev/null
+++ b/app/containers/LanguageProvider/constants.js
@@ -0,0 +1,7 @@
+/*
+ *
+ * LanguageProvider constants
+ *
+ */
+
+export const CHANGE_LOCALE = 'app/LanguageToggle/CHANGE_LOCALE';
diff --git a/app/containers/LanguageProvider/index.js b/app/containers/LanguageProvider/index.js
new file mode 100644
index 0000000..1e2a746
--- /dev/null
+++ b/app/containers/LanguageProvider/index.js
@@ -0,0 +1,36 @@
+/*
+ *
+ * LanguageProvider
+ *
+ * this component connects the redux state language locale to the
+ * IntlProvider component and i18n messages (loaded from `app/translations`)
+ */
+
+import React from 'react';
+import { connect } from 'react-redux';
+import { createSelector } from 'reselect';
+import { IntlProvider } from 'react-intl';
+import { selectLocale } from './selectors';
+
+export class LanguageProvider extends React.Component { // eslint-disable-line react/prefer-stateless-function
+ render() {
+ return (
+
+ {React.Children.only(this.props.children)}
+
+ );
+ }
+}
+
+LanguageProvider.propTypes = {
+ locale: React.PropTypes.string,
+ messages: React.PropTypes.object,
+ children: React.PropTypes.element.isRequired,
+};
+
+const mapStateToProps = createSelector(
+ selectLocale(),
+ (locale) => ({ locale })
+);
+
+export default connect(mapStateToProps)(LanguageProvider);
diff --git a/app/containers/LanguageProvider/reducer.js b/app/containers/LanguageProvider/reducer.js
new file mode 100644
index 0000000..2908bcc
--- /dev/null
+++ b/app/containers/LanguageProvider/reducer.js
@@ -0,0 +1,26 @@
+/*
+ *
+ * LanguageProvider reducer
+ *
+ */
+
+import { fromJS } from 'immutable';
+import {
+ CHANGE_LOCALE,
+} from './constants';
+
+const initialState = fromJS({
+ locale: 'en',
+});
+
+function languageProviderReducer(state = initialState, action) {
+ switch (action.type) {
+ case CHANGE_LOCALE:
+ return state
+ .set('locale', action.locale);
+ default:
+ return state;
+ }
+}
+
+export default languageProviderReducer;
diff --git a/app/containers/LanguageProvider/selectors.js b/app/containers/LanguageProvider/selectors.js
new file mode 100644
index 0000000..c43816f
--- /dev/null
+++ b/app/containers/LanguageProvider/selectors.js
@@ -0,0 +1,20 @@
+import { createSelector } from 'reselect';
+
+/**
+ * Direct selector to the languageToggle state domain
+ */
+const selectLanguage = () => (state) => state.get('language');
+
+/**
+ * Select the language locale
+ */
+
+const selectLocale = () => createSelector(
+ selectLanguage(),
+ (languageState) => languageState.get('locale')
+);
+
+export {
+ selectLanguage,
+ selectLocale,
+};
diff --git a/app/containers/LanguageProvider/tests/actions.test.js b/app/containers/LanguageProvider/tests/actions.test.js
new file mode 100644
index 0000000..53c604f
--- /dev/null
+++ b/app/containers/LanguageProvider/tests/actions.test.js
@@ -0,0 +1,19 @@
+import expect from 'expect';
+import {
+ changeLocale,
+} from '../actions';
+import {
+ CHANGE_LOCALE,
+} from '../constants';
+
+describe('LanguageProvider actions', () => {
+ describe('Change Local Action', () => {
+ it('has a type of CHANGE_LOCALE', () => {
+ const expected = {
+ type: CHANGE_LOCALE,
+ locale: 'de',
+ };
+ expect(changeLocale('de')).toEqual(expected);
+ });
+ });
+});
diff --git a/app/containers/LanguageProvider/tests/index.test.js b/app/containers/LanguageProvider/tests/index.test.js
new file mode 100644
index 0000000..8e73bcd
--- /dev/null
+++ b/app/containers/LanguageProvider/tests/index.test.js
@@ -0,0 +1,35 @@
+import LanguageProvider from '../index';
+
+import expect from 'expect';
+import { shallow } from 'enzyme';
+import { FormattedMessage, defineMessages } from 'react-intl';
+import configureStore from '../../../store';
+import React from 'react';
+import { Provider } from 'react-redux';
+import { browserHistory } from 'react-router';
+import { translationMessages } from '../../../i18n';
+
+describe('', () => {
+ let store;
+
+ before(() => {
+ store = configureStore({}, browserHistory);
+ });
+
+ it('should render the default language messages', () => {
+ const messages = defineMessages({
+ someMessage: {
+ id: 'some.id',
+ defaultMessage: 'This is some default message',
+ },
+ });
+ const renderedComponent = shallow(
+
+
+
+
+
+ );
+ expect(renderedComponent.contains()).toEqual(true);
+ });
+});
diff --git a/app/containers/LanguageProvider/tests/reducer.test.js b/app/containers/LanguageProvider/tests/reducer.test.js
new file mode 100644
index 0000000..693052d
--- /dev/null
+++ b/app/containers/LanguageProvider/tests/reducer.test.js
@@ -0,0 +1,20 @@
+import expect from 'expect';
+import languageProviderReducer from '../reducer';
+import { fromJS } from 'immutable';
+import {
+ CHANGE_LOCALE,
+} from '../constants';
+
+describe('languageProviderReducer', () => {
+ it('returns the initial state', () => {
+ expect(languageProviderReducer(undefined, {})).toEqual(fromJS({
+ locale: 'en',
+ }));
+ });
+
+ it('changes the locale', () => {
+ expect(languageProviderReducer(undefined, { type: CHANGE_LOCALE, locale: 'de' }).toJS()).toEqual({
+ locale: 'de',
+ });
+ });
+});
diff --git a/app/containers/LanguageProvider/tests/selectors.test.js b/app/containers/LanguageProvider/tests/selectors.test.js
new file mode 100644
index 0000000..f850874
--- /dev/null
+++ b/app/containers/LanguageProvider/tests/selectors.test.js
@@ -0,0 +1,16 @@
+import {
+ selectLanguage,
+} from '../selectors';
+import { fromJS } from 'immutable';
+import expect from 'expect';
+
+describe('selectLanguage', () => {
+ const globalSelector = selectLanguage();
+ it('should select the global state', () => {
+ const globalState = fromJS({});
+ const mockedState = fromJS({
+ language: globalState,
+ });
+ expect(globalSelector(mockedState)).toEqual(globalState);
+ });
+});
diff --git a/app/containers/LocaleToggle/index.js b/app/containers/LocaleToggle/index.js
new file mode 100644
index 0000000..37241af
--- /dev/null
+++ b/app/containers/LocaleToggle/index.js
@@ -0,0 +1,43 @@
+/*
+ *
+ * LanguageToggle
+ *
+ */
+
+import React from 'react';
+import { connect } from 'react-redux';
+import { selectLocale } from '../LanguageProvider/selectors';
+import { changeLocale } from '../LanguageProvider/actions';
+import { appLocales } from '../../i18n';
+import { createSelector } from 'reselect';
+import styles from './styles.css';
+import messages from './messages';
+import Toggle from 'components/Toggle';
+
+export class LocaleToggle extends React.Component { // eslint-disable-line
+ render() {
+ return (
+
+
+
+ );
+ }
+}
+
+LocaleToggle.propTypes = {
+ onLocaleToggle: React.PropTypes.func,
+};
+
+const mapStateToProps = createSelector(
+ selectLocale(),
+ (locale) => ({ locale })
+);
+
+export function mapDispatchToProps(dispatch) {
+ return {
+ onLocaleToggle: (evt) => dispatch(changeLocale(evt.target.value)),
+ dispatch,
+ };
+}
+
+export default connect(mapStateToProps, mapDispatchToProps)(LocaleToggle);
diff --git a/app/containers/LocaleToggle/messages.js b/app/containers/LocaleToggle/messages.js
new file mode 100644
index 0000000..cefec26
--- /dev/null
+++ b/app/containers/LocaleToggle/messages.js
@@ -0,0 +1,21 @@
+/*
+ * LocaleToggle Messages
+ *
+ * This contains all the text for the LanguageToggle component.
+ */
+import { defineMessages } from 'react-intl';
+import { appLocales } from '../../i18n';
+
+export function getLocaleMessages(locales) {
+ return locales.reduce((messages, locale) =>
+ Object.assign(messages, {
+ [locale]: {
+ id: `app.components.LocaleToggle.${locale}`,
+ defaultMessage: `${locale}`,
+ },
+ }), {});
+}
+
+export default defineMessages(
+ getLocaleMessages(appLocales)
+);
diff --git a/app/containers/LocaleToggle/styles.css b/app/containers/LocaleToggle/styles.css
new file mode 100644
index 0000000..b8a5a97
--- /dev/null
+++ b/app/containers/LocaleToggle/styles.css
@@ -0,0 +1,3 @@
+.localeToggle {
+ padding: 2px;
+}
diff --git a/app/containers/LocaleToggle/tests/index.test.js b/app/containers/LocaleToggle/tests/index.test.js
new file mode 100644
index 0000000..fdc76f0
--- /dev/null
+++ b/app/containers/LocaleToggle/tests/index.test.js
@@ -0,0 +1,60 @@
+import LocaleToggle, { mapDispatchToProps } from '../index';
+import { changeLocale } from '../../LanguageProvider/actions';
+import LanguageProvider from '../../LanguageProvider';
+
+import expect from 'expect';
+import { shallow, mount } from 'enzyme';
+import configureStore from '../../../store';
+import React from 'react';
+import { Provider } from 'react-redux';
+import { browserHistory } from 'react-router';
+import { translationMessages } from '../../../i18n';
+
+describe('', () => {
+ let store;
+
+ before(() => {
+ store = configureStore({}, browserHistory);
+ });
+
+ it('should render the default language messages', () => {
+ const renderedComponent = shallow(
+
+
+
+
+
+ );
+ expect(renderedComponent.contains()).toEqual(true);
+ });
+
+ it('should present the default `en` english language option', () => {
+ const renderedComponent = mount(
+
+
+
+
+
+ );
+ expect(renderedComponent.contains()).toEqual(true);
+ });
+
+ describe('mapDispatchToProps', () => {
+ describe('onLocaleToggle', () => {
+ it('should be injected', () => {
+ const dispatch = expect.createSpy();
+ const result = mapDispatchToProps(dispatch);
+ expect(result.onLocaleToggle).toExist();
+ });
+
+ it('should dispatch changeLocale when called', () => {
+ const dispatch = expect.createSpy();
+ const result = mapDispatchToProps(dispatch);
+ const locale = 'de';
+ const evt = { target: { value: locale } };
+ result.onLocaleToggle(evt);
+ expect(dispatch).toHaveBeenCalledWith(changeLocale(locale));
+ });
+ });
+ });
+});
diff --git a/app/containers/LocaleToggle/tests/messages.test.js b/app/containers/LocaleToggle/tests/messages.test.js
new file mode 100644
index 0000000..64e7c67
--- /dev/null
+++ b/app/containers/LocaleToggle/tests/messages.test.js
@@ -0,0 +1,21 @@
+import assert from 'assert';
+import { getLocaleMessages } from '../messages';
+
+describe('getLocaleMessages', () => {
+ it('should create i18n messages for all locales', () => {
+ const expected = {
+ en: {
+ id: 'app.components.LocaleToggle.en',
+ defaultMessage: 'en',
+ },
+ fr: {
+ id: 'app.components.LocaleToggle.fr',
+ defaultMessage: 'fr',
+ },
+ };
+
+ const actual = getLocaleMessages(['en', 'fr']);
+
+ assert.deepEqual(expected, actual);
+ });
+});
diff --git a/app/containers/NotFoundPage/index.js b/app/containers/NotFoundPage/index.js
new file mode 100644
index 0000000..2aa8c0f
--- /dev/null
+++ b/app/containers/NotFoundPage/index.js
@@ -0,0 +1,38 @@
+/**
+ * NotFoundPage
+ *
+ * This is the page we show when the user visits a url that doesn't have a route
+ */
+
+import React from 'react';
+import { connect } from 'react-redux';
+import { push } from 'react-router-redux';
+
+import messages from './messages';
+import { FormattedMessage } from 'react-intl';
+import Button from 'components/Button';
+import H1 from 'components/H1';
+
+export function NotFound(props) {
+ return (
+
+
+
+
+
+
+ );
+}
+
+NotFound.propTypes = {
+ dispatch: React.PropTypes.func,
+};
+
+// Wrap the component to inject dispatch and state into it
+export default connect()(NotFound);
diff --git a/app/containers/NotFoundPage/messages.js b/app/containers/NotFoundPage/messages.js
new file mode 100644
index 0000000..28db83b
--- /dev/null
+++ b/app/containers/NotFoundPage/messages.js
@@ -0,0 +1,17 @@
+/*
+ * NotFoundPage Messages
+ *
+ * This contains all the text for the NotFoundPage component.
+ */
+import { defineMessages } from 'react-intl';
+
+export default defineMessages({
+ header: {
+ id: 'boilerplate.containers.NotFoundPage.header',
+ defaultMessage: 'Page not found.',
+ },
+ homeButton: {
+ id: 'boilerplate.containers.NotFoundPage.home',
+ defaultMessage: 'Home',
+ },
+});
diff --git a/app/containers/NotFoundPage/tests/index.test.js b/app/containers/NotFoundPage/tests/index.test.js
new file mode 100644
index 0000000..fe2e7f2
--- /dev/null
+++ b/app/containers/NotFoundPage/tests/index.test.js
@@ -0,0 +1,48 @@
+/**
+ * Testing the NotFoundPage
+ */
+
+import expect from 'expect';
+import { shallow } from 'enzyme';
+import React from 'react';
+
+import { FormattedMessage } from 'react-intl';
+import { NotFound } from '../index';
+import H1 from 'components/H1';
+import Button from 'components/Button';
+
+describe('', () => {
+ it('should render the Page Not Found text', () => {
+ const renderedComponent = shallow(
+
+ );
+ expect(renderedComponent.contains(
+
+
+
)).toEqual(true);
+ });
+
+ it('should render a button', () => {
+ const renderedComponent = shallow(
+
+ );
+ const renderedButton = renderedComponent.find(Button);
+ expect(renderedButton.length).toEqual(1);
+ });
+
+ it('should link to "/"', (done) => {
+ const dispatch = (action) => {
+ expect(action.payload.args).toEqual('/');
+ done();
+ };
+
+ const renderedComponent = shallow(
+
+ );
+ const button = renderedComponent.find(Button);
+ button.prop('handleRoute')();
+ });
+});
diff --git a/app/containers/RepoListItem/index.js b/app/containers/RepoListItem/index.js
new file mode 100644
index 0000000..818ed9f
--- /dev/null
+++ b/app/containers/RepoListItem/index.js
@@ -0,0 +1,66 @@
+/**
+ * RepoListItem
+ *
+ * Lists the name and the issue count of a repository
+ */
+
+import React from 'react';
+import { connect } from 'react-redux';
+import { createSelector } from 'reselect';
+
+import { FormattedNumber } from 'react-intl';
+import { selectCurrentUser } from 'containers/App/selectors';
+import ListItem from 'components/ListItem';
+import IssueIcon from 'components/IssueIcon';
+import A from 'components/A';
+
+import styles from './styles.css';
+
+export class RepoListItem extends React.Component { // eslint-disable-line react/prefer-stateless-function
+ render() {
+ const item = this.props.item;
+ let nameprefix = '';
+
+ // If the repository is owned by a different person than we got the data for
+ // it's a fork and we should show the name of the owner
+ if (item.owner.login !== this.props.currentUser) {
+ nameprefix = `${item.owner.login}/`;
+ }
+
+ // Put together the content of the repository
+ const content = (
+