├── .editorconfig ├── .gitattributes ├── .github ├── ISSUE_TEMPLATE.md ├── MAINTAINERS.md └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── .travis.yml ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Changelog.md ├── LICENSE.md ├── README.md ├── app ├── .htaccess ├── .nginx.conf ├── app.js ├── components │ ├── Header │ │ ├── index.js │ │ └── tests │ │ │ └── index.test.js │ ├── Item │ │ ├── index.js │ │ └── tests │ │ │ └── index.test.js │ ├── Rating │ │ ├── index.js │ │ └── tests │ │ │ └── index.test.js │ └── Sidebar │ │ ├── index.js │ │ └── tests │ │ └── index.test.js ├── configureStore.js ├── containers │ ├── App │ │ ├── constants.js │ │ ├── index.js │ │ ├── selectors.js │ │ └── tests │ │ │ ├── index.test.js │ │ │ └── selectors.test.js │ ├── Home │ │ ├── Loadable.js │ │ ├── actions.js │ │ ├── constants.js │ │ ├── index.js │ │ ├── reducer.js │ │ ├── saga.js │ │ ├── selectors.js │ │ └── tests │ │ │ ├── actions.test.js │ │ │ ├── index.test.js │ │ │ ├── reducer.test.js │ │ │ ├── saga.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 │ ├── Map │ │ ├── index.js │ │ └── tests │ │ │ └── index.test.js │ └── NotFoundPage │ │ ├── Loadable.js │ │ ├── index.js │ │ ├── messages.js │ │ └── tests │ │ └── index.test.js ├── global-styles.js ├── i18n.js ├── images │ ├── favicon.ico │ ├── icon-120x120.png │ ├── icon-128x128.png │ ├── icon-144x144.png │ ├── icon-152x152.png │ ├── icon-167x167.png │ ├── icon-180x180.png │ ├── icon-192x192.png │ ├── icon-384x384.png │ ├── icon-512x512.png │ ├── icon-72x72.png │ └── icon-96x96.png ├── index.html ├── manifest.json ├── reducers.js ├── tests │ ├── i18n.test.js │ └── store.test.js ├── translations │ └── en.json └── utils │ ├── checkStore.js │ ├── constants.js │ ├── googleApiHelpers.js │ ├── injectReducer.js │ ├── injectSaga.js │ ├── reducerInjectors.js │ ├── sagaInjectors.js │ └── tests │ ├── checkStore.test.js │ ├── injectReducer.test.js │ ├── injectSaga.test.js │ ├── reducerInjectors.test.js │ └── sagaInjectors.test.js ├── appveyor.yml ├── docs ├── README.md ├── css │ ├── README.md │ ├── remove.md │ └── sanitize.md ├── general │ ├── README.md │ ├── commands.md │ ├── components.md │ ├── debugging.md │ ├── deployment.md │ ├── faq.md │ ├── files.md │ ├── gotchas.md │ ├── introduction.md │ ├── remove.md │ ├── server-configs.md │ ├── webstorm-debug.png │ ├── webstorm-eslint.png │ └── workflow.png ├── js │ ├── README.md │ ├── async-components.md │ ├── i18n.md │ ├── immutablejs.md │ ├── redux-saga.md │ ├── redux.md │ ├── remove.md │ ├── reselect.md │ └── routing.md ├── maintenance │ └── dependency.md └── testing │ ├── README.md │ ├── component-testing.md │ ├── remote-testing.md │ └── unit-testing.md ├── internals ├── config.js ├── generators │ ├── component │ │ ├── class.js.hbs │ │ ├── index.js │ │ ├── loadable.js.hbs │ │ ├── messages.js.hbs │ │ ├── stateless.js.hbs │ │ └── test.js.hbs │ ├── container │ │ ├── actions.js.hbs │ │ ├── actions.test.js.hbs │ │ ├── class.js.hbs │ │ ├── constants.js.hbs │ │ ├── index.js │ │ ├── index.js.hbs │ │ ├── messages.js.hbs │ │ ├── reducer.js.hbs │ │ ├── reducer.test.js.hbs │ │ ├── saga.js.hbs │ │ ├── saga.test.js.hbs │ │ ├── selectors.js.hbs │ │ ├── selectors.test.js.hbs │ │ ├── stateless.js.hbs │ │ └── test.js.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 │ └── utils │ │ └── componentExists.js ├── mocks │ ├── cssModule.js │ └── image.js ├── scripts │ ├── analyze.js │ ├── clean.js │ ├── dependencies.js │ ├── extract-intl.js │ ├── generate-templates-for-linting.js │ ├── helpers │ │ ├── checkmark.js │ │ ├── progress.js │ │ └── xmark.js │ └── npmcheckversion.js ├── testing │ └── test-bundler.js └── webpack │ ├── webpack.base.babel.js │ ├── webpack.dev.babel.js │ ├── webpack.dll.babel.js │ └── webpack.prod.babel.js ├── package.json ├── server ├── argv.js ├── index.js ├── logger.js ├── middlewares │ ├── addDevMiddlewares.js │ ├── addProdMiddlewares.js │ └── frontendMiddleware.js └── port.js └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | end_of_line = lf 8 | insert_final_newline = true 9 | indent_style = space 10 | indent_size = 2 11 | trim_trailing_whitespace = true 12 | 13 | [*.md] 14 | trim_trailing_whitespace = false 15 | -------------------------------------------------------------------------------- /.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 | .nginx.conf text 53 | 54 | # git config 55 | .gitattributes text 56 | .gitignore text 57 | .gitconfig text 58 | 59 | # code analysis config 60 | .jshintrc text 61 | .jscsrc text 62 | .jshintignore text 63 | .csslintrc text 64 | 65 | # misc config 66 | *.yaml text 67 | *.yml text 68 | .editorconfig text 69 | 70 | # build config 71 | *.npmignore text 72 | *.bowerrc text 73 | 74 | # Heroku 75 | Procfile text 76 | .slugignore text 77 | 78 | # Documentation 79 | *.md text 80 | LICENSE text 81 | AUTHORS text 82 | 83 | 84 | # 85 | ## These files are binary and should be left untouched 86 | # 87 | 88 | # (binary is a macro for -text -diff) 89 | *.png binary 90 | *.jpg binary 91 | *.jpeg binary 92 | *.gif binary 93 | *.ico binary 94 | *.mov binary 95 | *.mp4 binary 96 | *.mp3 binary 97 | *.flv binary 98 | *.fla binary 99 | *.swf binary 100 | *.gz binary 101 | *.zip binary 102 | *.7z binary 103 | *.ttf binary 104 | *.eot binary 105 | *.woff binary 106 | *.pyc binary 107 | *.pdf binary 108 | -------------------------------------------------------------------------------- /.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/react-boilerplate/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/react-boilerplate/react-boilerplate/issues?q=is%3Aissue+is%3Aclosed 13 | 14 | ## Issue Type 15 | 16 | - [ ] Bug (https://github.com/react-boilerplate/react-boilerplate/blob/master/.github/CONTRIBUTING.md#bug-reports) 17 | - [ ] Feature (https://github.com/react-boilerplate/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 | - React-Boilerplate (see `package.json`): 30 | - Node/NPM: 31 | - Browser: 32 | -------------------------------------------------------------------------------- /.github/MAINTAINERS.md: -------------------------------------------------------------------------------- 1 | # Maintainers 2 | - mxstbr 3 | - oliverturner 4 | - justingreenberg 5 | - gihrig 6 | - sedubois 7 | - chaintng 8 | - samit4me 9 | - amilajack 10 | - Dattaya 11 | - jwinn 12 | - KarandikarMihir 13 | -------------------------------------------------------------------------------- /.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/react-boilerplate/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/react-boilerplate/react-boilerplate/blob/master/.github/CONTRIBUTING.md) 13 | - [ ] double-check your branch is based on `dev` and targets `dev` 14 | - [ ] Pull request has tests (we are going for 100% coverage!) 15 | - [ ] Code is well-commented, linted and follows project conventions 16 | - [ ] Documentation is updated (if necessary) 17 | - [ ] Internal code generators and templates are updated (if necessary) 18 | - [ ] Description explains the issue/use-case resolved and auto-closes related issues 19 | 20 | Be kind to code reviewers, please try to keep pull requests as small and focused as possible :) 21 | 22 | **IMPORTANT**: By submitting a patch, you agree to allow the project 23 | owners to license your work under the terms of the [MIT License](https://github.com/react-boilerplate/react-boilerplate/blob/master/LICENSE.md). 24 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - 7 5 | - 6 6 | - 5 7 | 8 | script: 9 | - node ./internals/scripts/generate-templates-for-linting 10 | - npm run test 11 | - npm run build 12 | 13 | before_install: 14 | - export CHROME_BIN=chromium-browser 15 | - export DISPLAY=:99.0 16 | - sh -e /etc/init.d/xvfb start 17 | 18 | notifications: 19 | email: 20 | on_failure: change 21 | 22 | after_success: 'npm run coveralls' 23 | 24 | cache: 25 | yarn: true 26 | directories: 27 | - node_modules 28 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to react-boilerplate 2 | 3 | Love react-boilerplate and want to help? Thanks so much, there's something to do for everybody! 4 | 5 | Please take a moment to review this document in order to make the contribution process easy and effective for everyone involved. 6 | 7 | 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. 8 | 9 | ## Using the issue tracker 10 | 11 | The [issue tracker](https://github.com/react-boilerplate/react-boilerplate/issues) is 12 | the preferred channel for [bug reports](#bugs), [features requests](#features) 13 | and [submitting pull requests](#pull-requests). 14 | 15 | 16 | ## Bug reports 17 | 18 | A bug is a _demonstrable problem_ that is caused by the code in the repository. 19 | Good bug reports are extremely helpful - thank you! 20 | 21 | Guidelines for bug reports: 22 | 23 | 1. **Use the GitHub issue search** — check if the issue has already been reported. 24 | 25 | 2. **Check if the issue has been fixed** — try to reproduce it using the latest `master` or development branch in the repository. 26 | 27 | 3. **Isolate the problem** — ideally create a [reduced test case](https://css-tricks.com/reduced-test-cases/) and a live example. 28 | 29 | 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 30 | experience the problem? What would you expect to be the outcome? All these details will help people to fix any potential bugs. 31 | 32 | Example: 33 | 34 | > Short and descriptive example bug report title 35 | > 36 | > A summary of the issue and the browser/OS environment in which it occurs. If 37 | > suitable, include the steps required to reproduce the bug. 38 | > 39 | > 1. This is the first step 40 | > 2. This is the second step 41 | > 3. Further steps, etc. 42 | > 43 | > `` - a link to the reduced test case 44 | > 45 | > Any other information you want to share that is relevant to the issue being 46 | > reported. This might include the lines of code that you have identified as 47 | > causing the bug, and potential solutions (and your opinions on their 48 | > merits). 49 | 50 | 51 | 52 | ## Feature requests 53 | 54 | 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. 55 | 56 | 57 | 58 | ## Pull requests 59 | 60 | Good pull requests - patches, improvements, new features - are a fantastic 61 | help. They should remain focused in scope and avoid containing unrelated 62 | commits. 63 | 64 | **Please ask first** before embarking on any significant pull request (e.g. 65 | implementing features, refactoring code, porting to a different language), 66 | otherwise you risk spending a lot of time working on something that the 67 | project's developers might not want to merge into the project. 68 | 69 | Please adhere to the coding conventions used throughout a project (indentation, 70 | accurate comments, etc.) and any other requirements (such as test coverage). 71 | 72 | Since the `master` branch is what people actually use in production, we have a 73 | `dev` branch that unstable changes get merged into first. Only when we 74 | consider that stable we merge it into the `master` branch and release the 75 | changes for real. 76 | 77 | Adhering to the following process is the best way to get your work 78 | included in the project: 79 | 80 | 1. [Fork](https://help.github.com/articles/fork-a-repo/) the project, clone your fork, and configure the remotes: 81 | 82 | ```bash 83 | # Clone your fork of the repo into the current directory 84 | git clone https://github.com//react-boilerplate.git 85 | # Navigate to the newly cloned directory 86 | cd react-boilerplate 87 | # Assign the original repo to a remote called "upstream" 88 | git remote add upstream https://github.com/react-boilerplate/react-boilerplate.git 89 | ``` 90 | 91 | 2. If you cloned a while ago, get the latest changes from upstream: 92 | 93 | ```bash 94 | git checkout dev 95 | git pull upstream dev 96 | ``` 97 | 98 | 3. Create a new topic branch (off the `dev` branch) to contain your feature, change, or fix: 99 | 100 | ```bash 101 | git checkout -b 102 | ``` 103 | 104 | 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. 105 | 106 | 5. Locally merge (or rebase) the upstream dev branch into your topic branch: 107 | 108 | ```bash 109 | git pull [--rebase] upstream dev 110 | ``` 111 | 112 | 6. Push your topic branch up to your fork: 113 | 114 | ```bash 115 | git push origin 116 | ``` 117 | 118 | 7. [Open a Pull Request](https://help.github.com/articles/using-pull-requests/) 119 | with a clear title and description. 120 | 121 | **IMPORTANT**: By submitting a patch, you agree to allow the project 122 | owners to license your work under the terms of the [MIT License](https://github.com/react-boilerplate/react-boilerplate/blob/master/LICENSE.md). 123 | 124 | # Collaborating guidelines 125 | You can find the list of all maintainers in [MAINTAINERS.md](./MAINTAINERS.md). 126 | 127 | There are few basic rules to ensure high quality of the boilerplate: 128 | 129 | - Before merging, a PR requires at least two approvals from the collaborators unless it's an architectural change, a large feature, etc. If it is, then at least 50% of the core team have to agree to merge it, with every team member having a full veto right. (i.e. every single one can block any PR) 130 | - A PR should remain open for at least two days before merging (does not apply for trivial contributions like fixing a typo). This way everyone has enough time to look into it. 131 | 132 | 133 | You are always welcome to discuss and propose improvements to this guideline. 134 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 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. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Simple Yelp clone using Redux Saga, Google maps api, React Router v4, react-boilerplate... 2 | 3 | Adapted from https://www.fullstackreact.com/articles/react-tutorial-cloning-yelp 4 | 5 | This extends the original by using Redux and Redux Saga instead of local state, also React Router v4, styled-components, and other modern standards. 6 | 7 | ![2017-10-07_16h05_48](https://user-images.githubusercontent.com/22646941/31308129-a2094cf4-ab79-11e7-81f3-3e9d0c67fbf7.png) 8 | -------------------------------------------------------------------------------- /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 | 46 | 47 | # Do not cache sw.js, required for offline-first updates. 48 | 49 | Header set Cache-Control "private, no-cache, no-store, proxy-revalidate, no-transform" 50 | Header set Pragma "no-cache" 51 | 52 | 53 | -------------------------------------------------------------------------------- /app/.nginx.conf: -------------------------------------------------------------------------------- 1 | ## 2 | # Put this file in /etc/nginx/conf.d folder and make sure 3 | # you have a 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 ssl; 29 | 30 | # Type your domain name below 31 | server_name example.com; 32 | 33 | # Configure the Certificate and Key you got from your CA (e.g. Lets Encrypt) 34 | ssl_certificate /path/to/certificate.crt; 35 | ssl_certificate_key /path/to/server.key; 36 | 37 | ssl_session_timeout 1d; 38 | ssl_session_cache shared:SSL:50m; 39 | ssl_session_tickets off; 40 | 41 | # Only use TLS v1.2 as Transport Security Protocol 42 | ssl_protocols TLSv1.2; 43 | 44 | # Only use ciphersuites that are considered modern and secure by Mozilla 45 | ssl_ciphers 'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256'; 46 | 47 | # Do not let attackers downgrade the ciphersuites in Client Hello 48 | # Always use server-side offered ciphersuites 49 | ssl_prefer_server_ciphers on; 50 | 51 | # HSTS (ngx_http_headers_module is required) (15768000 seconds = 6 months) 52 | add_header Strict-Transport-Security max-age=15768000; 53 | 54 | # Diffie-Hellman parameter for DHE ciphersuites, recommended 2048 bits 55 | # Uncomment if you want to use your own Diffie-Hellman parameter, which can be generated with: openssl ecparam -genkey -out dhparam.pem -name prime256v1 56 | # See https://wiki.mozilla.org/Security/Server_Side_TLS#DHE_handshake_and_dhparam 57 | # ssl_dhparam /path/to/dhparam.pem; 58 | 59 | 60 | ## OCSP Configuration START 61 | # If you want to provide OCSP Stapling, you can uncomment the following lines 62 | # See https://www.digitalocean.com/community/tutorials/how-to-configure-ocsp-stapling-on-apache-and-nginx for more infos about OCSP and its use case 63 | # fetch OCSP records from URL in ssl_certificate and cache them 64 | 65 | #ssl_stapling on; 66 | #ssl_stapling_verify on; 67 | 68 | # verify chain of trust of OCSP response using Root CA and Intermediate certs (you will get this file from your CA) 69 | #ssl_trusted_certificate /path/to/root_CA_cert_plus_intermediates; 70 | 71 | ## OCSP Configuration END 72 | 73 | # To let nginx use its own DNS Resolver 74 | # resolver ; 75 | 76 | 77 | # Always serve index.html for any request 78 | location / { 79 | # Set path 80 | root /var/www/; 81 | try_files $uri /index.html; 82 | } 83 | 84 | # Do not cache sw.js, required for offline-first updates. 85 | location /sw.js { 86 | add_header Cache-Control "no-cache"; 87 | proxy_cache_bypass $http_pragma; 88 | proxy_cache_revalidate on; 89 | expires off; 90 | access_log off; 91 | } 92 | 93 | ## 94 | # If you want to use Node/Rails/etc. API server 95 | # on the same port (443) config Nginx as a reverse proxy. 96 | # For security reasons use a firewall like ufw in Ubuntu 97 | # and deny port 3000/tcp. 98 | ## 99 | 100 | # location /api/ { 101 | # 102 | # proxy_pass http://localhost:3000; 103 | # proxy_http_version 1.1; 104 | # proxy_set_header X-Forwarded-Proto https; 105 | # proxy_set_header Upgrade $http_upgrade; 106 | # proxy_set_header Connection 'upgrade'; 107 | # proxy_set_header Host $host; 108 | # proxy_cache_bypass $http_upgrade; 109 | # 110 | # } 111 | 112 | } 113 | -------------------------------------------------------------------------------- /app/app.js: -------------------------------------------------------------------------------- 1 | /** 2 | * app.js 3 | * 4 | * This is the entry file for the application, only setup and boilerplate 5 | * code. 6 | */ 7 | 8 | // Needed for redux-saga es6 generator support 9 | import 'babel-polyfill'; 10 | 11 | // Import all the third party stuff 12 | import React from 'react'; 13 | import ReactDOM from 'react-dom'; 14 | import { Provider } from 'react-redux'; 15 | import { ConnectedRouter } from 'react-router-redux'; 16 | import createHistory from 'history/createBrowserHistory'; 17 | import 'sanitize.css/sanitize.css'; 18 | 19 | // Import root app 20 | import App from 'containers/App'; 21 | 22 | // Import Language Provider 23 | import LanguageProvider from 'containers/LanguageProvider'; 24 | 25 | // Load the favicon, the manifest.json file and the .htaccess file 26 | /* eslint-disable import/no-unresolved, import/extensions */ 27 | import '!file-loader?name=[name].[ext]!./images/favicon.ico'; 28 | import '!file-loader?name=[name].[ext]!./images/icon-72x72.png'; 29 | import '!file-loader?name=[name].[ext]!./images/icon-96x96.png'; 30 | import '!file-loader?name=[name].[ext]!./images/icon-128x128.png'; 31 | import '!file-loader?name=[name].[ext]!./images/icon-144x144.png'; 32 | import '!file-loader?name=[name].[ext]!./images/icon-152x152.png'; 33 | import '!file-loader?name=[name].[ext]!./images/icon-192x192.png'; 34 | import '!file-loader?name=[name].[ext]!./images/icon-384x384.png'; 35 | import '!file-loader?name=[name].[ext]!./images/icon-512x512.png'; 36 | import '!file-loader?name=[name].[ext]!./manifest.json'; 37 | import 'file-loader?name=[name].[ext]!./.htaccess'; 38 | /* eslint-enable import/no-unresolved, import/extensions */ 39 | 40 | import configureStore from './configureStore'; 41 | 42 | // Import i18n messages 43 | import { translationMessages } from './i18n'; 44 | 45 | // Import CSS reset and Global Styles 46 | import './global-styles'; 47 | 48 | // Create redux store with history 49 | const initialState = {}; 50 | const history = createHistory(); 51 | const store = configureStore(initialState, history); 52 | const MOUNT_NODE = document.getElementById('app'); 53 | 54 | const render = (messages) => { 55 | ReactDOM.render( 56 | 57 | 58 | 59 | 60 | 61 | 62 | , 63 | MOUNT_NODE 64 | ); 65 | }; 66 | 67 | if (module.hot) { 68 | // Hot reloadable React components and translation json files 69 | // modules.hot.accept does not accept dynamic dependencies, 70 | // have to be constants at compile-time 71 | module.hot.accept(['./i18n', 'containers/App'], () => { 72 | ReactDOM.unmountComponentAtNode(MOUNT_NODE); 73 | render(translationMessages); 74 | }); 75 | } 76 | 77 | // Chunked polyfill for browsers without Intl support 78 | if (!window.Intl) { 79 | (new Promise((resolve) => { 80 | resolve(import('intl')); 81 | })) 82 | .then(() => Promise.all([ 83 | import('intl/locale-data/jsonp/en.js'), 84 | ])) 85 | .then(() => render(translationMessages)) 86 | .catch((err) => { 87 | throw err; 88 | }); 89 | } else { 90 | render(translationMessages); 91 | } 92 | 93 | // Install ServiceWorker and AppCache in the end since 94 | // it's not most important operation and if main code fails, 95 | // we do not want it installed 96 | if (process.env.NODE_ENV === 'production') { 97 | require('offline-plugin/runtime').install(); // eslint-disable-line global-require 98 | } 99 | -------------------------------------------------------------------------------- /app/components/Header/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Header 4 | * 5 | */ 6 | 7 | import React from 'react'; 8 | import { Link } from 'react-router-dom'; 9 | import styled from 'styled-components'; 10 | 11 | const Wrapper = styled.div` 12 | position: fixed; 13 | z-index: 10; 14 | top: 0; 15 | left: 0; 16 | background: #48b5e9; 17 | width: 100%; 18 | padding: 0 25px; 19 | height: 80px; 20 | line-height: 80px; 21 | color: #fff; 22 | `; 23 | 24 | const StyledLink = styled(Link)` 25 | text-transform: uppercase; 26 | text-decoration: none; 27 | letter-spacing: 1px; 28 | color: #fff; 29 | line-height: 40px; 30 | h1 { font-size: 28px; } 31 | `; 32 | 33 | // const Section = styled.section` 34 | // position: absolute; 35 | // top: 0px; 36 | // right: 25px; 37 | // `; 38 | 39 | 40 | function Header() { 41 | return ( 42 | 43 |

React Redux Yelp Clone

44 |
45 | ); 46 | } 47 | 48 | Header.propTypes = { 49 | 50 | }; 51 | 52 | export default Header; 53 | -------------------------------------------------------------------------------- /app/components/Header/tests/index.test.js: -------------------------------------------------------------------------------- 1 | // import React from 'react'; 2 | // import { shallow } from 'enzyme'; 3 | 4 | // import Header from '../index'; 5 | 6 | describe('
', () => { 7 | it('Expect to have unit tests specified', () => { 8 | expect(true).toEqual(false); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /app/components/Item/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Item 4 | * 5 | */ 6 | 7 | import React from 'react'; 8 | import styled from 'styled-components'; 9 | 10 | import Rating from 'components/Rating'; 11 | 12 | const Wrapper = styled.div` 13 | display: flex; 14 | flex-direction: row; 15 | border-bottom: 1px solid #eeeeee; 16 | padding: 10px; 17 | text-decoration: none; 18 | &:last-child { 19 | border-bottom: none; 20 | } 21 | &:hover { 22 | color: #48b5e9; 23 | cursor: pointer; 24 | } 25 | `; 26 | 27 | const Title = styled.h4` 28 | flex: 2; 29 | `; 30 | 31 | const StyledRating = styled(Rating)` 32 | text-align: right; 33 | flex: 1; 34 | `; 35 | 36 | 37 | // I believe the onClick div should be replaced with a react-router Link 38 | class Item extends React.Component { // eslint-disable-line react/prefer-stateless-function 39 | render() { 40 | const { place, onClick } = this.props; 41 | return ( 42 | 43 | {place.name} 44 | 45 | 46 | ); 47 | } 48 | } 49 | 50 | Item.propTypes = { 51 | 52 | }; 53 | 54 | export default Item; 55 | -------------------------------------------------------------------------------- /app/components/Item/tests/index.test.js: -------------------------------------------------------------------------------- 1 | // import React from 'react'; 2 | // import { shallow } from 'enzyme'; 3 | 4 | // import Item from '../index'; 5 | 6 | describe('', () => { 7 | it('Expect to have unit tests specified', () => { 8 | expect(true).toEqual(false); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /app/components/Rating/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Rating 4 | * 5 | */ 6 | 7 | import React from 'react'; 8 | import styled from 'styled-components'; 9 | 10 | const RatingIcon = () => (); 11 | 12 | const Sprite = styled.div` 13 | unicode-bidi: bidi-override; 14 | color: #404040; 15 | font-size: 25px; 16 | height: 25px; 17 | width: 100px; 18 | margin: 0 auto; 19 | position: relative; 20 | padding: 0; 21 | text-shadow: 0px 1px 0 lightgray; 22 | `; 23 | 24 | const TopDiv = styled.div` 25 | color: #48b5e9; 26 | padding: 0; 27 | position: absolute; 28 | z-index: 1; 29 | display: block; 30 | top: 0; 31 | left: 0; 32 | overflow: hidden; 33 | `; 34 | 35 | const BottomDiv = styled.div` 36 | padding: 0; 37 | display: block; 38 | z-index: 0; 39 | color: #a2a2a2; 40 | `; 41 | 42 | 43 | class Rating extends React.Component { 44 | render() { 45 | const { percentage } = this.props; 46 | const style = { 47 | width: `${(percentage || 0) * 100}%`, 48 | }; 49 | return ( 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | ); 67 | } 68 | } 69 | 70 | Rating.propTypes = { 71 | 72 | }; 73 | 74 | export default Rating; 75 | -------------------------------------------------------------------------------- /app/components/Rating/tests/index.test.js: -------------------------------------------------------------------------------- 1 | // import React from 'react'; 2 | // import { shallow } from 'enzyme'; 3 | 4 | // import Rating from '../index'; 5 | 6 | describe('', () => { 7 | it('Expect to have unit tests specified', () => { 8 | expect(true).toEqual(false); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /app/components/Sidebar/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Sidebar 4 | * 5 | */ 6 | 7 | import React from 'react'; 8 | import { withRouter } from 'react-router-dom'; 9 | import styled from 'styled-components'; 10 | import Item from 'components/Item'; 11 | 12 | const Wrapper = styled.div` 13 | height: 100%; 14 | background: #fff; 15 | top: 80px; 16 | left: 0; 17 | // overflow: hidden; 18 | position: relative; 19 | flex: 1; 20 | z-index: 0; 21 | `; 22 | 23 | const Heading = styled.div` 24 | flex: 1; 25 | background: #fff; 26 | border-bottom: 1px solid #eee; 27 | padding: 0 10px; 28 | `; 29 | 30 | const H1 = styled.h1` 31 | font-size: 1.8em; 32 | `; 33 | 34 | const Listing = styled.div` 35 | height: 100%; 36 | overflow: auto; 37 | padding-bottom: 60px; 38 | margin: 0; 39 | `; 40 | 41 | 42 | class Sidebar extends React.Component { // eslint-disable-line react/prefer-stateless-function 43 | render() { 44 | const { places, history } = this.props; 45 | return ( 46 | 47 | 48 |

Restaurants

49 |
50 | 51 | {places.map((place) => ( 52 | { 56 | history.push(`/detail/${place.id}`); 57 | }} 58 | /> 59 | ))} 60 | 61 |
62 | ); 63 | } 64 | } 65 | 66 | Sidebar.propTypes = { 67 | 68 | }; 69 | 70 | export default withRouter(Sidebar); 71 | -------------------------------------------------------------------------------- /app/components/Sidebar/tests/index.test.js: -------------------------------------------------------------------------------- 1 | // import React from 'react'; 2 | // import { shallow } from 'enzyme'; 3 | 4 | // import Sidebar from '../index'; 5 | 6 | describe('', () => { 7 | it('Expect to have unit tests specified', () => { 8 | expect(true).toEqual(false); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /app/configureStore.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Create the store with dynamic 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 | 13 | export default function configureStore(initialState = {}, history) { 14 | // Create the store with two middlewares 15 | // 1. sagaMiddleware: Makes redux-sagas work 16 | // 2. routerMiddleware: Syncs the location/URL path to the state 17 | const middlewares = [ 18 | sagaMiddleware, 19 | routerMiddleware(history), 20 | ]; 21 | 22 | const enhancers = [ 23 | applyMiddleware(...middlewares), 24 | ]; 25 | 26 | // If Redux DevTools Extension is installed use it, otherwise use Redux compose 27 | /* eslint-disable no-underscore-dangle */ 28 | const composeEnhancers = 29 | process.env.NODE_ENV !== 'production' && 30 | typeof window === 'object' && 31 | window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ 32 | ? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({ 33 | // TODO Try to remove when `react-router-redux` is out of beta, LOCATION_CHANGE should not be fired more than once after hot reloading 34 | // Prevent recomputing reducers for `replaceReducer` 35 | shouldHotReload: false, 36 | }) 37 | : compose; 38 | /* eslint-enable */ 39 | 40 | const store = createStore( 41 | createReducer(), 42 | fromJS(initialState), 43 | composeEnhancers(...enhancers) 44 | ); 45 | 46 | // Extensions 47 | store.runSaga = sagaMiddleware.run; 48 | store.injectedReducers = {}; // Reducer registry 49 | store.injectedSagas = {}; // Saga registry 50 | 51 | // Make reducers hot reloadable, see http://mxs.is/googmo 52 | /* istanbul ignore next */ 53 | if (module.hot) { 54 | module.hot.accept('./reducers', () => { 55 | store.replaceReducer(createReducer(store.injectedReducers)); 56 | }); 57 | } 58 | 59 | return store; 60 | } 61 | -------------------------------------------------------------------------------- /app/containers/App/constants.js: -------------------------------------------------------------------------------- 1 | /* 2 | * AppConstants 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 DEFAULT_LOCALE = 'en'; 13 | -------------------------------------------------------------------------------- /app/containers/App/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Switch, Route } from 'react-router-dom'; 3 | 4 | import Home from 'containers/Home'; 5 | import NotFoundPage from 'containers/NotFoundPage/Loadable'; 6 | 7 | export default function App() { 8 | return ( 9 |
10 | 11 | 12 | 13 | 14 |
15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /app/containers/App/selectors.js: -------------------------------------------------------------------------------- 1 | import { createSelector } from 'reselect'; 2 | 3 | const selectRoute = (state) => state.get('route'); 4 | 5 | const makeSelectLocation = () => createSelector( 6 | selectRoute, 7 | (routeState) => routeState.get('location').toJS() 8 | ); 9 | 10 | export { 11 | makeSelectLocation, 12 | }; 13 | -------------------------------------------------------------------------------- /app/containers/App/tests/index.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { shallow } from 'enzyme'; 3 | import { Route } from 'react-router-dom'; 4 | 5 | import App from '../index'; 6 | 7 | describe('', () => { 8 | it('should render some routes', () => { 9 | const renderedComponent = shallow( 10 | 11 | ); 12 | expect(renderedComponent.find(Route).length).not.toBe(0); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /app/containers/App/tests/selectors.test.js: -------------------------------------------------------------------------------- 1 | import { fromJS } from 'immutable'; 2 | 3 | import { makeSelectLocation } from 'containers/App/selectors'; 4 | 5 | describe('makeSelectLocation', () => { 6 | it('should select the location', () => { 7 | const route = fromJS({ 8 | location: { pathname: '/foo' }, 9 | }); 10 | const mockedState = fromJS({ 11 | route, 12 | }); 13 | expect(makeSelectLocation()(mockedState)).toEqual(route.get('location').toJS()); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /app/containers/Home/Loadable.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Asynchronously loads the component for Map 4 | * 5 | */ 6 | 7 | import Loadable from 'react-loadable'; 8 | 9 | export default Loadable({ 10 | loader: () => import('./index'), 11 | loading: () => null, 12 | }); 13 | -------------------------------------------------------------------------------- /app/containers/Home/actions.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * Map actions 4 | * 5 | */ 6 | 7 | import { 8 | SEARCH_NEARBY, 9 | RECEIVE_RESPONSE, 10 | } from './constants'; 11 | 12 | export function searchNearby(mapProps, map, google) { 13 | return { 14 | type: SEARCH_NEARBY, 15 | payload: { 16 | mapProps, 17 | map, 18 | google, 19 | }, 20 | }; 21 | } 22 | 23 | export function receiveResponse(response) { 24 | return { 25 | type: RECEIVE_RESPONSE, 26 | payload: { 27 | response, 28 | }, 29 | }; 30 | } 31 | -------------------------------------------------------------------------------- /app/containers/Home/constants.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * Map constants 4 | * 5 | */ 6 | 7 | export const SEARCH_NEARBY = 'app/Map/SEARCH_NEARBY'; 8 | export const RECEIVE_RESPONSE = 'app/Map/RECEIVE_RESULTS'; 9 | -------------------------------------------------------------------------------- /app/containers/Home/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Map 4 | * 5 | */ 6 | 7 | import React from 'react'; 8 | import PropTypes from 'prop-types'; 9 | import { connect } from 'react-redux'; 10 | import { compose } from 'redux'; 11 | import { Map as GoogleMap, GoogleApiWrapper } from 'google-maps-react'; 12 | import styled from 'styled-components'; 13 | import { Route, Redirect, withRouter } from 'react-router-dom'; 14 | 15 | import injectSaga from 'utils/injectSaga'; 16 | import injectReducer from 'utils/injectReducer'; 17 | import Header from 'components/Header'; 18 | import Sidebar from 'components/Sidebar'; 19 | import Map from 'containers/Map'; 20 | import { selectPlaces } from './selectors'; 21 | import reducer from './reducer'; 22 | import saga from './saga'; 23 | import { searchNearby } from './actions'; 24 | 25 | const StyledMap = styled(GoogleMap)` 26 | overflow-y: scroll; 27 | display: flex; 28 | margin: 0; 29 | padding: 0px; 30 | height: 100vh; 31 | `; 32 | 33 | const Content = styled.div` 34 | position: relative; 35 | flex: 2; 36 | top: 80px; 37 | `; 38 | 39 | export class Home extends React.Component { // eslint-disable-line react/prefer-stateless-function 40 | 41 | onReady = (mapProps, map) => { 42 | const { google } = this.props; 43 | this.props.onReady(mapProps, map, google); 44 | } 45 | 46 | onMarkerClick = (item) => { 47 | const { place } = item; 48 | this.props.history.push(`/detail/${place.place_id}`); 49 | } 50 | 51 | render() { 52 | const { places } = this.props; 53 | return ( 54 | 59 |
60 | 61 | 62 | {/* */} 63 | 66 | ( 72 | ) 73 | } 74 | /> 75 | {`Detail Page: ${props.match.params.placeId}`}} /> 76 | 77 | 78 | ); 79 | } 80 | } 81 | 82 | Home.propTypes = { 83 | dispatch: PropTypes.func.isRequired, 84 | onReady: PropTypes.func.isRequired, 85 | google: PropTypes.any, 86 | places: PropTypes.any, 87 | pagination: PropTypes.any, 88 | }; 89 | 90 | const mapStateToProps = (state) => ({ 91 | places: selectPlaces(state), 92 | }); 93 | 94 | function mapDispatchToProps(dispatch) { 95 | return { 96 | dispatch, 97 | onReady: (a, b, c) => dispatch(searchNearby(a, b, c)), 98 | }; 99 | } 100 | 101 | const withConnect = connect(mapStateToProps, mapDispatchToProps); 102 | 103 | const withReducer = injectReducer({ key: 'map', reducer }); 104 | const withSaga = injectSaga({ key: 'map', saga }); 105 | 106 | const withGoogleApi = GoogleApiWrapper({ 107 | apiKey: 'AIzaSyAyesbQMyKVVbBgKVi2g6VX7mop2z96jBo', 108 | }); 109 | 110 | export default compose( 111 | withGoogleApi, 112 | withReducer, 113 | withSaga, 114 | withRouter, 115 | withConnect, 116 | )(Home); 117 | -------------------------------------------------------------------------------- /app/containers/Home/reducer.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * Map reducer 4 | * 5 | */ 6 | 7 | import { fromJS } from 'immutable'; 8 | import { 9 | RECEIVE_RESPONSE, 10 | } from './constants'; 11 | 12 | const initialState = fromJS({ 13 | places: [], 14 | pagination: {}, 15 | }); 16 | 17 | function mapReducer(state = initialState, action) { 18 | switch (action.type) { 19 | case RECEIVE_RESPONSE: 20 | return state 21 | .set('places', action.payload.response); 22 | default: 23 | return state; 24 | } 25 | } 26 | 27 | export default mapReducer; 28 | -------------------------------------------------------------------------------- /app/containers/Home/saga.js: -------------------------------------------------------------------------------- 1 | import { take, call, put, select, fork } from 'redux-saga/effects'; 2 | import { searchNearby as searchNearbyUtil } from 'utils/googleApiHelpers'; 3 | import * as actions from './actions'; 4 | 5 | export function* searchNearby() { 6 | while (true) { 7 | const action = yield take(actions.searchNearby); 8 | const { payload: { map, google } } = action; 9 | 10 | const opts = { 11 | location: map.center, 12 | radius: '500', 13 | types: ['cafe'], 14 | }; 15 | 16 | try { 17 | const response = yield call(searchNearbyUtil, google, map, opts); 18 | yield put(actions.receiveResponse(response)); 19 | } catch (error) { 20 | console.log('ERROR: ', error); 21 | } 22 | } 23 | } 24 | 25 | // Individual exports for testing 26 | export default function* defaultSaga() { 27 | yield [ 28 | fork(searchNearby), 29 | ]; 30 | } 31 | -------------------------------------------------------------------------------- /app/containers/Home/selectors.js: -------------------------------------------------------------------------------- 1 | import { createSelector } from 'reselect'; 2 | 3 | /** 4 | * Direct selector to the map state domain 5 | */ 6 | const selectMapDomain = (state) => state.get('map'); 7 | 8 | /** 9 | * Other specific selectors 10 | */ 11 | 12 | export const selectPlaces = createSelector( 13 | selectMapDomain, 14 | (map) => map.get('places') 15 | ); 16 | 17 | export const selectPagination = createSelector( 18 | selectMapDomain, 19 | (map) => map.get('pagination') 20 | ); 21 | 22 | 23 | /** 24 | * Default selector used by Map 25 | */ 26 | 27 | const makeSelectMap = () => createSelector( 28 | selectMapDomain, 29 | (substate) => substate.toJS() 30 | ); 31 | 32 | export default makeSelectMap; 33 | export { 34 | selectMapDomain, 35 | }; 36 | -------------------------------------------------------------------------------- /app/containers/Home/tests/actions.test.js: -------------------------------------------------------------------------------- 1 | 2 | import { 3 | defaultAction, 4 | } from '../actions'; 5 | import { 6 | DEFAULT_ACTION, 7 | } from '../constants'; 8 | 9 | describe('Map 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 | -------------------------------------------------------------------------------- /app/containers/Home/tests/index.test.js: -------------------------------------------------------------------------------- 1 | // import React from 'react'; 2 | // import { shallow } from 'enzyme'; 3 | 4 | // import { Map } from '../index'; 5 | 6 | describe('', () => { 7 | it('Expect to have unit tests specified', () => { 8 | expect(true).toEqual(false); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /app/containers/Home/tests/reducer.test.js: -------------------------------------------------------------------------------- 1 | 2 | import { fromJS } from 'immutable'; 3 | import mapReducer from '../reducer'; 4 | 5 | describe('mapReducer', () => { 6 | it('returns the initial state', () => { 7 | expect(mapReducer(undefined, {})).toEqual(fromJS({})); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /app/containers/Home/tests/saga.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Test sagas 3 | */ 4 | 5 | /* eslint-disable redux-saga/yield-effects */ 6 | // import { take, call, put, select } from 'redux-saga/effects'; 7 | // import { defaultSaga } from '../saga'; 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 | -------------------------------------------------------------------------------- /app/containers/Home/tests/selectors.test.js: -------------------------------------------------------------------------------- 1 | // import { fromJS } from 'immutable'; 2 | // import { selectMapDomain } from '../selectors'; 3 | 4 | describe('selectMapDomain', () => { 5 | it('Expect to have unit tests specified', () => { 6 | expect(true).toEqual(false); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /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 | export const DEFAULT_LOCALE = 'en'; 9 | -------------------------------------------------------------------------------- /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 PropTypes from 'prop-types'; 11 | import { connect } from 'react-redux'; 12 | import { createSelector } from 'reselect'; 13 | import { IntlProvider } from 'react-intl'; 14 | 15 | import { makeSelectLocale } from './selectors'; 16 | 17 | export class LanguageProvider extends React.PureComponent { // eslint-disable-line react/prefer-stateless-function 18 | render() { 19 | return ( 20 | 21 | {React.Children.only(this.props.children)} 22 | 23 | ); 24 | } 25 | } 26 | 27 | LanguageProvider.propTypes = { 28 | locale: PropTypes.string, 29 | messages: PropTypes.object, 30 | children: PropTypes.element.isRequired, 31 | }; 32 | 33 | 34 | const mapStateToProps = createSelector( 35 | makeSelectLocale(), 36 | (locale) => ({ locale }) 37 | ); 38 | 39 | function mapDispatchToProps(dispatch) { 40 | return { 41 | dispatch, 42 | }; 43 | } 44 | 45 | export default connect(mapStateToProps, mapDispatchToProps)(LanguageProvider); 46 | -------------------------------------------------------------------------------- /app/containers/LanguageProvider/reducer.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * LanguageProvider reducer 4 | * 5 | */ 6 | 7 | import { fromJS } from 'immutable'; 8 | 9 | import { 10 | CHANGE_LOCALE, 11 | } from './constants'; 12 | import { 13 | DEFAULT_LOCALE, 14 | } from '../App/constants'; // eslint-disable-line 15 | 16 | const initialState = fromJS({ 17 | locale: DEFAULT_LOCALE, 18 | }); 19 | 20 | function languageProviderReducer(state = initialState, action) { 21 | switch (action.type) { 22 | case CHANGE_LOCALE: 23 | return state 24 | .set('locale', action.locale); 25 | default: 26 | return state; 27 | } 28 | } 29 | 30 | export default languageProviderReducer; 31 | -------------------------------------------------------------------------------- /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 makeSelectLocale = () => createSelector( 13 | selectLanguage, 14 | (languageState) => languageState.get('locale') 15 | ); 16 | 17 | export { 18 | selectLanguage, 19 | makeSelectLocale, 20 | }; 21 | -------------------------------------------------------------------------------- /app/containers/LanguageProvider/tests/actions.test.js: -------------------------------------------------------------------------------- 1 | import { 2 | changeLocale, 3 | } from '../actions'; 4 | 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 React from 'react'; 2 | import { shallow, mount } from 'enzyme'; 3 | import { FormattedMessage, defineMessages } from 'react-intl'; 4 | import { Provider } from 'react-redux'; 5 | import { browserHistory } from 'react-router-dom'; 6 | 7 | import ConnectedLanguageProvider, { LanguageProvider } from '../index'; 8 | import configureStore from '../../../configureStore'; 9 | 10 | import { translationMessages } from '../../../i18n'; 11 | 12 | const messages = defineMessages({ 13 | someMessage: { 14 | id: 'some.id', 15 | defaultMessage: 'This is some default message', 16 | en: 'This is some en message', 17 | }, 18 | }); 19 | 20 | describe('', () => { 21 | it('should render its children', () => { 22 | const children = (

Test

); 23 | const renderedComponent = shallow( 24 | 25 | {children} 26 | 27 | ); 28 | expect(renderedComponent.contains(children)).toBe(true); 29 | }); 30 | }); 31 | 32 | describe('', () => { 33 | let store; 34 | 35 | beforeAll(() => { 36 | store = configureStore({}, browserHistory); 37 | }); 38 | 39 | it('should render the default language messages', () => { 40 | const renderedComponent = mount( 41 | 42 | 43 | 44 | 45 | 46 | ); 47 | expect(renderedComponent.contains()).toBe(true); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /app/containers/LanguageProvider/tests/reducer.test.js: -------------------------------------------------------------------------------- 1 | import { fromJS } from 'immutable'; 2 | 3 | import languageProviderReducer from '../reducer'; 4 | import { 5 | CHANGE_LOCALE, 6 | } from '../constants'; 7 | 8 | describe('languageProviderReducer', () => { 9 | it('returns the initial state', () => { 10 | expect(languageProviderReducer(undefined, {})).toEqual(fromJS({ 11 | locale: 'en', 12 | })); 13 | }); 14 | 15 | it('changes the locale', () => { 16 | expect(languageProviderReducer(undefined, { type: CHANGE_LOCALE, locale: 'de' }).toJS()).toEqual({ 17 | locale: 'de', 18 | }); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /app/containers/LanguageProvider/tests/selectors.test.js: -------------------------------------------------------------------------------- 1 | import { fromJS } from 'immutable'; 2 | 3 | import { 4 | selectLanguage, 5 | } from '../selectors'; 6 | 7 | describe('selectLanguage', () => { 8 | it('should select the global state', () => { 9 | const globalState = fromJS({}); 10 | const mockedState = fromJS({ 11 | language: globalState, 12 | }); 13 | expect(selectLanguage(mockedState)).toEqual(globalState); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /app/containers/Map/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Map 4 | * 5 | */ 6 | 7 | import React from 'react'; 8 | import { Map as GoogleMap, Marker } from 'google-maps-react'; 9 | // import styled from 'styled-components'; 10 | 11 | 12 | class Map extends React.Component { // eslint-disable-line react/prefer-stateless-function 13 | 14 | _renderMarkers() { 15 | if (!this.props.places) { 16 | return; 17 | } 18 | return this.props.places.map((p) => ()); 27 | } 28 | 29 | _renderChildren() { 30 | const { children } = this.props; 31 | 32 | if (React.Children.count(children) > 0) { 33 | return React.Children.map(children, (c) => React.cloneElement(c, this.props, { 34 | map: this.props.map, 35 | google: this.props.google, 36 | })); 37 | } 38 | return this._renderMarkers(); 39 | } 40 | 41 | render() { 42 | return ( 43 | 47 | {this._renderChildren()} 48 | 49 | ); 50 | } 51 | } 52 | 53 | Map.propTypes = { 54 | 55 | }; 56 | 57 | const identity = (...a) => a; 58 | 59 | Map.defaultProps = { 60 | onMarkerClick: identity 61 | } 62 | 63 | export default Map; 64 | -------------------------------------------------------------------------------- /app/containers/Map/tests/index.test.js: -------------------------------------------------------------------------------- 1 | // import React from 'react'; 2 | // import { shallow } from 'enzyme'; 3 | 4 | // import Map from '../index'; 5 | 6 | describe('', () => { 7 | it('Expect to have unit tests specified', () => { 8 | expect(true).toEqual(false); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /app/containers/NotFoundPage/Loadable.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Asynchronously loads the component for NotFoundPage 3 | */ 4 | import Loadable from 'react-loadable'; 5 | 6 | export default Loadable({ 7 | loader: () => import('./index'), 8 | loading: () => null, 9 | }); 10 | -------------------------------------------------------------------------------- /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 | * 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 necessity 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 | 15 | import messages from './messages'; 16 | 17 | export default class NotFound extends React.PureComponent { // eslint-disable-line react/prefer-stateless-function 18 | render() { 19 | return ( 20 |

21 | 22 |

23 | ); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /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: 'app.components.NotFoundPage.header', 11 | defaultMessage: 'This is NotFoundPage component!', 12 | }, 13 | }); 14 | -------------------------------------------------------------------------------- /app/containers/NotFoundPage/tests/index.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { FormattedMessage } from 'react-intl'; 3 | import { shallow } from 'enzyme'; 4 | 5 | import NotFoundPage from '../index'; 6 | import messages from '../messages'; 7 | 8 | describe('', () => { 9 | it('should render the page message', () => { 10 | const renderedComponent = shallow( 11 | 12 | ); 13 | expect(renderedComponent.contains( 14 | 15 | )).toEqual(true); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /app/global-styles.js: -------------------------------------------------------------------------------- 1 | import { injectGlobal } from 'styled-components'; 2 | 3 | /* eslint no-unused-expressions: 0 */ 4 | injectGlobal` 5 | html, 6 | body { 7 | height: 100%; 8 | width: 100%; 9 | } 10 | 11 | body { 12 | font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; 13 | } 14 | 15 | body.fontLoaded { 16 | font-family: 'Open Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif; 17 | } 18 | 19 | #app { 20 | background-color: #fafafa; 21 | min-height: 100%; 22 | min-width: 100%; 23 | } 24 | 25 | p, 26 | label { 27 | font-family: Georgia, Times, 'Times New Roman', serif; 28 | line-height: 1.5em; 29 | } 30 | `; 31 | -------------------------------------------------------------------------------- /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 | import enLocaleData from 'react-intl/locale-data/en'; 9 | 10 | import { DEFAULT_LOCALE } from './containers/App/constants'; // eslint-disable-line 11 | import enTranslationMessages from './translations/en.json'; 12 | 13 | export const appLocales = [ 14 | 'en', 15 | ]; 16 | 17 | addLocaleData(enLocaleData); 18 | 19 | export const formatTranslationMessages = (locale, messages) => { 20 | const defaultFormattedMessages = locale !== DEFAULT_LOCALE 21 | ? formatTranslationMessages(DEFAULT_LOCALE, enTranslationMessages) 22 | : {}; 23 | return Object.keys(messages).reduce((formattedMessages, key) => { 24 | let message = messages[key]; 25 | if (!message && locale !== DEFAULT_LOCALE) { 26 | message = defaultFormattedMessages[key]; 27 | } 28 | return Object.assign(formattedMessages, { [key]: message }); 29 | }, {}); 30 | }; 31 | 32 | export const translationMessages = { 33 | en: formatTranslationMessages('en', enTranslationMessages), 34 | }; 35 | -------------------------------------------------------------------------------- /app/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jmxo/react-redux-yelp-clone/55d5cf1e8bf208ff50945ae3f2bb3b416fd192d5/app/images/favicon.ico -------------------------------------------------------------------------------- /app/images/icon-120x120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jmxo/react-redux-yelp-clone/55d5cf1e8bf208ff50945ae3f2bb3b416fd192d5/app/images/icon-120x120.png -------------------------------------------------------------------------------- /app/images/icon-128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jmxo/react-redux-yelp-clone/55d5cf1e8bf208ff50945ae3f2bb3b416fd192d5/app/images/icon-128x128.png -------------------------------------------------------------------------------- /app/images/icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jmxo/react-redux-yelp-clone/55d5cf1e8bf208ff50945ae3f2bb3b416fd192d5/app/images/icon-144x144.png -------------------------------------------------------------------------------- /app/images/icon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jmxo/react-redux-yelp-clone/55d5cf1e8bf208ff50945ae3f2bb3b416fd192d5/app/images/icon-152x152.png -------------------------------------------------------------------------------- /app/images/icon-167x167.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jmxo/react-redux-yelp-clone/55d5cf1e8bf208ff50945ae3f2bb3b416fd192d5/app/images/icon-167x167.png -------------------------------------------------------------------------------- /app/images/icon-180x180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jmxo/react-redux-yelp-clone/55d5cf1e8bf208ff50945ae3f2bb3b416fd192d5/app/images/icon-180x180.png -------------------------------------------------------------------------------- /app/images/icon-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jmxo/react-redux-yelp-clone/55d5cf1e8bf208ff50945ae3f2bb3b416fd192d5/app/images/icon-192x192.png -------------------------------------------------------------------------------- /app/images/icon-384x384.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jmxo/react-redux-yelp-clone/55d5cf1e8bf208ff50945ae3f2bb3b416fd192d5/app/images/icon-384x384.png -------------------------------------------------------------------------------- /app/images/icon-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jmxo/react-redux-yelp-clone/55d5cf1e8bf208ff50945ae3f2bb3b416fd192d5/app/images/icon-512x512.png -------------------------------------------------------------------------------- /app/images/icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jmxo/react-redux-yelp-clone/55d5cf1e8bf208ff50945ae3f2bb3b416fd192d5/app/images/icon-72x72.png -------------------------------------------------------------------------------- /app/images/icon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jmxo/react-redux-yelp-clone/55d5cf1e8bf208ff50945ae3f2bb3b416fd192d5/app/images/icon-96x96.png -------------------------------------------------------------------------------- /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 | 22 | -------------------------------------------------------------------------------- /app/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react boilerplate", 3 | "theme_color": "#b1624d", 4 | "background_color": "#fafafa", 5 | "display": "standalone", 6 | "Scope": "/", 7 | "start_url": "/", 8 | "icons": [ 9 | { 10 | "src": "icon-72x72.png", 11 | "sizes": "72x72", 12 | "type": "image/png" 13 | }, 14 | { 15 | "src": "icon-96x96.png", 16 | "sizes": "96x96", 17 | "type": "image/png" 18 | }, 19 | { 20 | "src": "icon-128x128.png", 21 | "sizes": "128x128", 22 | "type": "image/png" 23 | }, 24 | { 25 | "src": "icon-144x144.png", 26 | "sizes": "144x144", 27 | "type": "image/png" 28 | }, 29 | { 30 | "src": "icon-152x152.png", 31 | "sizes": "152x152", 32 | "type": "image/png" 33 | }, 34 | { 35 | "src": "icon-192x192.png", 36 | "sizes": "192x192", 37 | "type": "image/png" 38 | }, 39 | { 40 | "src": "icon-384x384.png", 41 | "sizes": "384x384", 42 | "type": "image/png" 43 | }, 44 | { 45 | "src": "icon-512x512.png", 46 | "sizes": "512x512", 47 | "type": "image/png" 48 | } 49 | ], 50 | "splash_pages": null 51 | } 52 | -------------------------------------------------------------------------------- /app/reducers.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Combine all reducers in this file and export the combined reducers. 3 | */ 4 | 5 | import { combineReducers } from 'redux-immutable'; 6 | import { fromJS } from 'immutable'; 7 | import { LOCATION_CHANGE } from 'react-router-redux'; 8 | 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 | location: 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 | location: action.payload, 33 | }); 34 | default: 35 | return state; 36 | } 37 | } 38 | 39 | /** 40 | * Creates the main reducer with the dynamically injected ones 41 | */ 42 | export default function createReducer(injectedReducers) { 43 | return combineReducers({ 44 | route: routeReducer, 45 | language: languageProviderReducer, 46 | ...injectedReducers, 47 | }); 48 | } 49 | -------------------------------------------------------------------------------- /app/tests/i18n.test.js: -------------------------------------------------------------------------------- 1 | import { DEFAULT_LOCALE } from '../containers/App/constants'; 2 | import { formatTranslationMessages } from '../i18n'; 3 | 4 | jest.mock('../translations/en.json', () => ( 5 | { 6 | message1: 'default message', 7 | message2: 'default message 2', 8 | } 9 | )); 10 | 11 | const esTranslationMessages = { 12 | message1: 'mensaje predeterminado', 13 | message2: '', 14 | }; 15 | 16 | describe('formatTranslationMessages', () => { 17 | it('should build only defaults when DEFAULT_LOCALE', () => { 18 | const result = formatTranslationMessages(DEFAULT_LOCALE, { a: 'a' }); 19 | 20 | expect(result).toEqual({ a: 'a' }); 21 | }); 22 | 23 | 24 | it('should combine default locale and current locale when not DEFAULT_LOCALE', () => { 25 | const result = formatTranslationMessages('', esTranslationMessages); 26 | 27 | expect(result).toEqual({ 28 | message1: 'mensaje predeterminado', 29 | message2: 'default message 2', 30 | }); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /app/tests/store.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Test store addons 3 | */ 4 | 5 | import { browserHistory } from 'react-router-dom'; 6 | import configureStore from '../configureStore'; 7 | 8 | describe('configureStore', () => { 9 | let store; 10 | 11 | beforeAll(() => { 12 | store = configureStore({}, browserHistory); 13 | }); 14 | 15 | describe('injectedReducers', () => { 16 | it('should contain an object for reducers', () => { 17 | expect(typeof store.injectedReducers).toBe('object'); 18 | }); 19 | }); 20 | 21 | describe('injectedSagas', () => { 22 | it('should contain an object for sagas', () => { 23 | expect(typeof store.injectedSagas).toBe('object'); 24 | }); 25 | }); 26 | 27 | describe('runSaga', () => { 28 | it('should contain a hook for `sagaMiddleware.run`', () => { 29 | expect(typeof store.runSaga).toBe('function'); 30 | }); 31 | }); 32 | }); 33 | 34 | describe('configureStore params', () => { 35 | it('should call window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__', () => { 36 | /* eslint-disable no-underscore-dangle */ 37 | const compose = jest.fn(); 38 | window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ = () => compose; 39 | configureStore(undefined, browserHistory); 40 | expect(compose).toHaveBeenCalled(); 41 | /* eslint-enable */ 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /app/translations/en.json: -------------------------------------------------------------------------------- 1 | [] 2 | -------------------------------------------------------------------------------- /app/utils/checkStore.js: -------------------------------------------------------------------------------- 1 | import conformsTo from 'lodash/conformsTo'; 2 | import isFunction from 'lodash/isFunction'; 3 | import isObject from 'lodash/isObject'; 4 | import invariant from 'invariant'; 5 | 6 | /** 7 | * Validate the shape of redux store 8 | */ 9 | export default function checkStore(store) { 10 | const shape = { 11 | dispatch: isFunction, 12 | subscribe: isFunction, 13 | getState: isFunction, 14 | replaceReducer: isFunction, 15 | runSaga: isFunction, 16 | injectedReducers: isObject, 17 | injectedSagas: isObject, 18 | }; 19 | invariant( 20 | conformsTo(store, shape), 21 | '(app/utils...) injectors: Expected a valid redux store' 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /app/utils/constants.js: -------------------------------------------------------------------------------- 1 | export const RESTART_ON_REMOUNT = '@@saga-injector/restart-on-remount'; 2 | export const DAEMON = '@@saga-injector/daemon'; 3 | export const ONCE_TILL_UNMOUNT = '@@saga-injector/once-till-unmount'; 4 | -------------------------------------------------------------------------------- /app/utils/googleApiHelpers.js: -------------------------------------------------------------------------------- 1 | export function searchNearby(google, map, request) { 2 | return new Promise((resolve, reject) => { 3 | const service = new google.maps.places.PlacesService(map); 4 | 5 | service.nearbySearch(request, (results, status, pagination) => { 6 | if (status == google.maps.places.PlacesServiceStatus.OK) { 7 | resolve(results, pagination); 8 | } else { 9 | reject(results, status); 10 | } 11 | }); 12 | }); 13 | } 14 | -------------------------------------------------------------------------------- /app/utils/injectReducer.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import hoistNonReactStatics from 'hoist-non-react-statics'; 4 | 5 | import getInjectors from './reducerInjectors'; 6 | 7 | /** 8 | * Dynamically injects a reducer 9 | * 10 | * @param {string} key A key of the reducer 11 | * @param {function} reducer A reducer that will be injected 12 | * 13 | */ 14 | export default ({ key, reducer }) => (WrappedComponent) => { 15 | class ReducerInjector extends React.Component { 16 | static WrappedComponent = WrappedComponent; 17 | static contextTypes = { 18 | store: PropTypes.object.isRequired, 19 | }; 20 | static displayName = `withReducer(${(WrappedComponent.displayName || WrappedComponent.name || 'Component')})`; 21 | 22 | componentWillMount() { 23 | const { injectReducer } = this.injectors; 24 | 25 | injectReducer(key, reducer); 26 | } 27 | 28 | injectors = getInjectors(this.context.store); 29 | 30 | render() { 31 | return ; 32 | } 33 | } 34 | 35 | return hoistNonReactStatics(ReducerInjector, WrappedComponent); 36 | }; 37 | -------------------------------------------------------------------------------- /app/utils/injectSaga.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import hoistNonReactStatics from 'hoist-non-react-statics'; 4 | 5 | import getInjectors from './sagaInjectors'; 6 | 7 | /** 8 | * Dynamically injects a saga, passes component's props as saga arguments 9 | * 10 | * @param {string} key A key of the saga 11 | * @param {function} saga A root saga that will be injected 12 | * @param {string} [mode] By default (constants.RESTART_ON_REMOUNT) the saga will be started on component mount and 13 | * cancelled with `task.cancel()` on component un-mount for improved performance. Another two options: 14 | * - constants.DAEMON—starts the saga on component mount and never cancels it or starts again, 15 | * - constants.ONCE_TILL_UNMOUNT—behaves like 'RESTART_ON_REMOUNT' but never runs it again. 16 | * 17 | */ 18 | export default ({ key, saga, mode }) => (WrappedComponent) => { 19 | class InjectSaga extends React.Component { 20 | static WrappedComponent = WrappedComponent; 21 | static contextTypes = { 22 | store: PropTypes.object.isRequired, 23 | }; 24 | static displayName = `withSaga(${(WrappedComponent.displayName || WrappedComponent.name || 'Component')})`; 25 | 26 | componentWillMount() { 27 | const { injectSaga } = this.injectors; 28 | 29 | injectSaga(key, { saga, mode }, this.props); 30 | } 31 | 32 | componentWillUnmount() { 33 | const { ejectSaga } = this.injectors; 34 | 35 | ejectSaga(key); 36 | } 37 | 38 | injectors = getInjectors(this.context.store); 39 | 40 | render() { 41 | return ; 42 | } 43 | } 44 | 45 | return hoistNonReactStatics(InjectSaga, WrappedComponent); 46 | }; 47 | -------------------------------------------------------------------------------- /app/utils/reducerInjectors.js: -------------------------------------------------------------------------------- 1 | import invariant from 'invariant'; 2 | import isEmpty from 'lodash/isEmpty'; 3 | import isFunction from 'lodash/isFunction'; 4 | import isString from 'lodash/isString'; 5 | 6 | import checkStore from './checkStore'; 7 | import createReducer from '../reducers'; 8 | 9 | export function injectReducerFactory(store, isValid) { 10 | return function injectReducer(key, reducer) { 11 | if (!isValid) checkStore(store); 12 | 13 | invariant( 14 | isString(key) && !isEmpty(key) && isFunction(reducer), 15 | '(app/utils...) injectReducer: Expected `reducer` to be a reducer function' 16 | ); 17 | 18 | // Check `store.injectedReducers[key] === reducer` for hot reloading when a key is the same but a reducer is different 19 | if (Reflect.has(store.injectedReducers, key) && store.injectedReducers[key] === reducer) return; 20 | 21 | store.injectedReducers[key] = reducer; // eslint-disable-line no-param-reassign 22 | store.replaceReducer(createReducer(store.injectedReducers)); 23 | }; 24 | } 25 | 26 | export default function getInjectors(store) { 27 | checkStore(store); 28 | 29 | return { 30 | injectReducer: injectReducerFactory(store, true), 31 | }; 32 | } 33 | -------------------------------------------------------------------------------- /app/utils/sagaInjectors.js: -------------------------------------------------------------------------------- 1 | import isEmpty from 'lodash/isEmpty'; 2 | import isFunction from 'lodash/isFunction'; 3 | import isString from 'lodash/isString'; 4 | import invariant from 'invariant'; 5 | import conformsTo from 'lodash/conformsTo'; 6 | 7 | import checkStore from './checkStore'; 8 | import { 9 | DAEMON, 10 | ONCE_TILL_UNMOUNT, 11 | RESTART_ON_REMOUNT, 12 | } from './constants'; 13 | 14 | const allowedModes = [RESTART_ON_REMOUNT, DAEMON, ONCE_TILL_UNMOUNT]; 15 | 16 | const checkKey = (key) => invariant( 17 | isString(key) && !isEmpty(key), 18 | '(app/utils...) injectSaga: Expected `key` to be a non empty string' 19 | ); 20 | 21 | const checkDescriptor = (descriptor) => { 22 | const shape = { 23 | saga: isFunction, 24 | mode: (mode) => isString(mode) && allowedModes.includes(mode), 25 | }; 26 | invariant( 27 | conformsTo(descriptor, shape), 28 | '(app/utils...) injectSaga: Expected a valid saga descriptor' 29 | ); 30 | }; 31 | 32 | export function injectSagaFactory(store, isValid) { 33 | return function injectSaga(key, descriptor = {}, args) { 34 | if (!isValid) checkStore(store); 35 | 36 | const newDescriptor = { ...descriptor, mode: descriptor.mode || RESTART_ON_REMOUNT }; 37 | const { saga, mode } = newDescriptor; 38 | 39 | checkKey(key); 40 | checkDescriptor(newDescriptor); 41 | 42 | let hasSaga = Reflect.has(store.injectedSagas, key); 43 | 44 | if (process.env.NODE_ENV !== 'production') { 45 | const oldDescriptor = store.injectedSagas[key]; 46 | // enable hot reloading of daemon and once-till-unmount sagas 47 | if (hasSaga && oldDescriptor.saga !== saga) { 48 | oldDescriptor.task.cancel(); 49 | hasSaga = false; 50 | } 51 | } 52 | 53 | if (!hasSaga || (hasSaga && mode !== DAEMON && mode !== ONCE_TILL_UNMOUNT)) { 54 | store.injectedSagas[key] = { ...newDescriptor, task: store.runSaga(saga, args) }; // eslint-disable-line no-param-reassign 55 | } 56 | }; 57 | } 58 | 59 | export function ejectSagaFactory(store, isValid) { 60 | return function ejectSaga(key) { 61 | if (!isValid) checkStore(store); 62 | 63 | checkKey(key); 64 | 65 | if (Reflect.has(store.injectedSagas, key)) { 66 | const descriptor = store.injectedSagas[key]; 67 | if (descriptor.mode !== DAEMON) { 68 | descriptor.task.cancel(); 69 | // Clean up in production; in development we need `descriptor.saga` for hot reloading 70 | if (process.env.NODE_ENV === 'production') { 71 | // Need some value to be able to detect `ONCE_TILL_UNMOUNT` sagas in `injectSaga` 72 | store.injectedSagas[key] = 'done'; // eslint-disable-line no-param-reassign 73 | } 74 | } 75 | } 76 | }; 77 | } 78 | 79 | export default function getInjectors(store) { 80 | checkStore(store); 81 | 82 | return { 83 | injectSaga: injectSagaFactory(store, true), 84 | ejectSaga: ejectSagaFactory(store, true), 85 | }; 86 | } 87 | -------------------------------------------------------------------------------- /app/utils/tests/checkStore.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Test injectors 3 | */ 4 | 5 | import checkStore from '../checkStore'; 6 | 7 | describe('checkStore', () => { 8 | let store; 9 | 10 | beforeEach(() => { 11 | store = { 12 | dispatch: () => {}, 13 | subscribe: () => {}, 14 | getState: () => {}, 15 | replaceReducer: () => {}, 16 | runSaga: () => {}, 17 | injectedReducers: {}, 18 | injectedSagas: {}, 19 | }; 20 | }); 21 | 22 | it('should not throw if passed valid store shape', () => { 23 | expect(() => checkStore(store)).not.toThrow(); 24 | }); 25 | 26 | it('should throw if passed invalid store shape', () => { 27 | expect(() => checkStore({})).toThrow(); 28 | expect(() => checkStore({ ...store, injectedSagas: null })).toThrow(); 29 | expect(() => checkStore({ ...store, injectedReducers: null })).toThrow(); 30 | expect(() => checkStore({ ...store, runSaga: null })).toThrow(); 31 | expect(() => checkStore({ ...store, replaceReducer: null })).toThrow(); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /app/utils/tests/injectReducer.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Test injectors 3 | */ 4 | 5 | import { memoryHistory } from 'react-router-dom'; 6 | import { shallow } from 'enzyme'; 7 | import React from 'react'; 8 | import identity from 'lodash/identity'; 9 | 10 | import configureStore from '../../configureStore'; 11 | import injectReducer from '../injectReducer'; 12 | import * as reducerInjectors from '../reducerInjectors'; 13 | 14 | // Fixtures 15 | const Component = () => null; 16 | 17 | const reducer = identity; 18 | 19 | describe('injectReducer decorator', () => { 20 | let store; 21 | let injectors; 22 | let ComponentWithReducer; 23 | 24 | beforeAll(() => { 25 | reducerInjectors.default = jest.fn().mockImplementation(() => injectors); 26 | }); 27 | 28 | beforeEach(() => { 29 | store = configureStore({}, memoryHistory); 30 | injectors = { 31 | injectReducer: jest.fn(), 32 | }; 33 | ComponentWithReducer = injectReducer({ key: 'test', reducer })(Component); 34 | reducerInjectors.default.mockClear(); 35 | }); 36 | 37 | it('should inject a given reducer', () => { 38 | shallow(, { context: { store } }); 39 | 40 | expect(injectors.injectReducer).toHaveBeenCalledTimes(1); 41 | expect(injectors.injectReducer).toHaveBeenCalledWith('test', reducer); 42 | }); 43 | 44 | it('should set a correct display name', () => { 45 | expect(ComponentWithReducer.displayName).toBe('withReducer(Component)'); 46 | expect(injectReducer({ key: 'test', reducer })(() => null).displayName).toBe('withReducer(Component)'); 47 | }); 48 | 49 | it('should propagate props', () => { 50 | const props = { testProp: 'test' }; 51 | const renderedComponent = shallow(, { context: { store } }); 52 | 53 | expect(renderedComponent.prop('testProp')).toBe('test'); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /app/utils/tests/injectSaga.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Test injectors 3 | */ 4 | 5 | import { memoryHistory } from 'react-router-dom'; 6 | import { put } from 'redux-saga/effects'; 7 | import { shallow } from 'enzyme'; 8 | import React from 'react'; 9 | 10 | import configureStore from '../../configureStore'; 11 | import injectSaga from '../injectSaga'; 12 | import * as sagaInjectors from '../sagaInjectors'; 13 | 14 | // Fixtures 15 | const Component = () => null; 16 | 17 | function* testSaga() { 18 | yield put({ type: 'TEST', payload: 'yup' }); 19 | } 20 | 21 | describe('injectSaga decorator', () => { 22 | let store; 23 | let injectors; 24 | let ComponentWithSaga; 25 | 26 | beforeAll(() => { 27 | sagaInjectors.default = jest.fn().mockImplementation(() => injectors); 28 | }); 29 | 30 | beforeEach(() => { 31 | store = configureStore({}, memoryHistory); 32 | injectors = { 33 | injectSaga: jest.fn(), 34 | ejectSaga: jest.fn(), 35 | }; 36 | ComponentWithSaga = injectSaga({ key: 'test', saga: testSaga, mode: 'testMode' })(Component); 37 | sagaInjectors.default.mockClear(); 38 | }); 39 | 40 | it('should inject given saga, mode, and props', () => { 41 | const props = { test: 'test' }; 42 | shallow(, { context: { store } }); 43 | 44 | expect(injectors.injectSaga).toHaveBeenCalledTimes(1); 45 | expect(injectors.injectSaga).toHaveBeenCalledWith('test', { saga: testSaga, mode: 'testMode' }, props); 46 | }); 47 | 48 | it('should eject on unmount with a correct saga key', () => { 49 | const props = { test: 'test' }; 50 | const renderedComponent = shallow(, { context: { store } }); 51 | renderedComponent.unmount(); 52 | 53 | expect(injectors.ejectSaga).toHaveBeenCalledTimes(1); 54 | expect(injectors.ejectSaga).toHaveBeenCalledWith('test'); 55 | }); 56 | 57 | it('should set a correct display name', () => { 58 | expect(ComponentWithSaga.displayName).toBe('withSaga(Component)'); 59 | expect(injectSaga({ key: 'test', saga: testSaga })(() => null).displayName).toBe('withSaga(Component)'); 60 | }); 61 | 62 | it('should propagate props', () => { 63 | const props = { testProp: 'test' }; 64 | const renderedComponent = shallow(, { context: { store } }); 65 | 66 | expect(renderedComponent.prop('testProp')).toBe('test'); 67 | }); 68 | }); 69 | -------------------------------------------------------------------------------- /app/utils/tests/reducerInjectors.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Test injectors 3 | */ 4 | 5 | import { memoryHistory } from 'react-router-dom'; 6 | import { fromJS } from 'immutable'; 7 | import identity from 'lodash/identity'; 8 | 9 | import configureStore from '../../configureStore'; 10 | 11 | import getInjectors, { 12 | injectReducerFactory, 13 | } from '../reducerInjectors'; 14 | 15 | // Fixtures 16 | 17 | const initialState = fromJS({ reduced: 'soon' }); 18 | 19 | const reducer = (state = initialState, action) => { 20 | switch (action.type) { 21 | case 'TEST': 22 | return state.set('reduced', action.payload); 23 | default: 24 | return state; 25 | } 26 | }; 27 | 28 | describe('reducer injectors', () => { 29 | let store; 30 | let injectReducer; 31 | 32 | describe('getInjectors', () => { 33 | beforeEach(() => { 34 | store = configureStore({}, memoryHistory); 35 | }); 36 | 37 | it('should return injectors', () => { 38 | expect(getInjectors(store)).toEqual(expect.objectContaining({ 39 | injectReducer: expect.any(Function), 40 | })); 41 | }); 42 | 43 | it('should throw if passed invalid store shape', () => { 44 | Reflect.deleteProperty(store, 'dispatch'); 45 | 46 | expect(() => getInjectors(store)).toThrow(); 47 | }); 48 | }); 49 | 50 | describe('injectReducer helper', () => { 51 | beforeEach(() => { 52 | store = configureStore({}, memoryHistory); 53 | injectReducer = injectReducerFactory(store, true); 54 | }); 55 | 56 | it('should check a store if the second argument is falsy', () => { 57 | const inject = injectReducerFactory({}); 58 | 59 | expect(() => inject('test', reducer)).toThrow(); 60 | }); 61 | 62 | it('it should not check a store if the second argument is true', () => { 63 | Reflect.deleteProperty(store, 'dispatch'); 64 | 65 | expect(() => injectReducer('test', reducer)).not.toThrow(); 66 | }); 67 | 68 | it('should validate a reducer and reducer\'s key', () => { 69 | expect(() => injectReducer('', reducer)).toThrow(); 70 | expect(() => injectReducer(1, reducer)).toThrow(); 71 | expect(() => injectReducer(1, 1)).toThrow(); 72 | }); 73 | 74 | it('given a store, it should provide a function to inject a reducer', () => { 75 | injectReducer('test', reducer); 76 | 77 | const actual = store.getState().get('test'); 78 | const expected = initialState; 79 | 80 | expect(actual.toJS()).toEqual(expected.toJS()); 81 | }); 82 | 83 | it('should not assign reducer if already existing', () => { 84 | store.replaceReducer = jest.fn(); 85 | injectReducer('test', reducer); 86 | injectReducer('test', reducer); 87 | 88 | expect(store.replaceReducer).toHaveBeenCalledTimes(1); 89 | }); 90 | 91 | it('should assign reducer if different implementation for hot reloading', () => { 92 | store.replaceReducer = jest.fn(); 93 | injectReducer('test', reducer); 94 | injectReducer('test', identity); 95 | 96 | expect(store.replaceReducer).toHaveBeenCalledTimes(2); 97 | }); 98 | }); 99 | }); 100 | -------------------------------------------------------------------------------- /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: 7 15 | - nodejs_version: 6 16 | - nodejs_version: 5 17 | 18 | # Fix line endings in Windows. (runs before repo cloning) 19 | init: 20 | - git config --global core.autocrlf input 21 | 22 | # Install scripts--runs after repo cloning 23 | install: 24 | # Install chrome 25 | - choco install -y googlechrome --ignore-checksums 26 | # Install the latest stable version of Node 27 | - ps: Install-Product node $env:nodejs_version 28 | - set PATH=%APPDATA%\yarn;%PATH% 29 | - yarn 30 | 31 | # Disable automatic builds 32 | build: off 33 | 34 | # Post-install test scripts 35 | test_script: 36 | # Output debugging info 37 | - node --version 38 | - node ./internals/scripts/generate-templates-for-linting 39 | # run tests and run build 40 | - yarn run test 41 | - yarn run build 42 | 43 | # Cache node_modules for faster builds 44 | cache: 45 | - "%LOCALAPPDATA%\\Yarn" 46 | - node_modules -> package.json 47 | 48 | # remove, as appveyor doesn't support secure variables on pr builds 49 | # so `COVERALLS_REPO_TOKEN` cannot be set, without hard-coding in this file 50 | #on_success: 51 | #- yarn run coveralls 52 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Documentation 2 | 3 | ## Table of Contents 4 | 5 | - [General](general) 6 | - [**CLI Commands**](general/commands.md) 7 | - [Introduction ](general/introduction.md) 8 | - [Tool Configuration](general/files.md) 9 | - [Server Configurations](general/server-configs.md) 10 | - [Deployment](general/deployment.md) *(currently Heroku and AWS S3 specific)* 11 | - [Debugging](general/debugging.md) 12 | - [FAQ](general/faq.md) 13 | - [Gotchas](general/gotchas.md) 14 | - [Remove](general/remove.md) 15 | - [Extracting components](general/components.md) 16 | - [Testing](testing) 17 | - [Unit Testing](testing/unit-testing.md) 18 | - [Component Testing](testing/component-testing.md) 19 | - [Remote Testing](testing/remote-testing.md) 20 | - [Styling (CSS)](css/README.md) 21 | - [Next Generation CSS](css/README.md#next-generation-css) 22 | - [CSS Support](css/README.md#css-we-support) 23 | - [styled-components](css/README.md#styled-components) 24 | - [Stylesheet](css/README.md#stylesheet) 25 | - [CSS Modules](css/README.md#css-modules) 26 | - [Sass](css/README.md#sass) 27 | - [LESS](css/README.md#less) 28 | - [JS](js) 29 | - [Redux](js/redux.md) 30 | - [ImmutableJS](js/immutablejs.md) 31 | - [reselect](js/reselect.md) 32 | - [redux-saga](js/redux-saga.md) 33 | - [i18n](js/i18n.md) 34 | - [routing](js/routing.md) 35 | - [Maintenance](maintenance) 36 | - [Dependency Update](maintenance/dependency.md) 37 | 38 | ## Overview 39 | 40 | ### Quickstart 41 | 42 | 1. First, let's kick the tyres by launching the sample _Repospective_ app 43 | bundled with this project to demo some of its best features: 44 | 45 | ```Shell 46 | npm run setup && npm start 47 | ``` 48 | 49 | 1. Open [localhost:3000](http://localhost:3000) to see it in action. 50 | 51 | - Add a Github username to see Redux and Redux Sagas in action: effortless 52 | async state updates and side effects are now yours :) 53 | - Edit the file at `./app/components/Header/index.js` so that the text of 54 | the `