├── .env ├── .env.development ├── .env.production ├── .env.test ├── .eslintignore ├── .eslintrc ├── .gitattributes ├── .github ├── CODEOWNERS ├── dependabot.yml ├── pull_request_template.md └── workflows │ ├── ci.yml │ └── dep-check.yml ├── .gitignore ├── .npmrc.template ├── .prettierignore ├── .prettierrc ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── netlify.toml ├── package.json ├── public ├── favicon.ico ├── index.html ├── logo192.png ├── logo512.png ├── manifest.json └── robots.txt ├── src ├── assets │ └── icons.js ├── emotion.d.ts ├── features │ ├── app │ │ ├── components │ │ │ ├── App │ │ │ │ ├── App.js │ │ │ │ ├── App.styles.js │ │ │ │ ├── App.test.js │ │ │ │ └── index.js │ │ │ ├── AuthenticatedApp │ │ │ │ ├── AuthenticatedApp.js │ │ │ │ ├── AuthenticatedApp.test.js │ │ │ │ └── index.js │ │ │ ├── ErrorBoundary │ │ │ │ ├── ErrorBoundary.js │ │ │ │ ├── ErrorBoundary.styles.js │ │ │ │ ├── ErrorBoundary.test.js │ │ │ │ └── index.js │ │ │ ├── Footer │ │ │ │ ├── Footer.tsx │ │ │ │ └── index.ts │ │ │ ├── Form │ │ │ │ ├── Button.tsx │ │ │ │ ├── Form.styles.ts │ │ │ │ ├── Form.tsx │ │ │ │ ├── Input.tsx │ │ │ │ ├── InputLabel.tsx │ │ │ │ ├── Select.tsx │ │ │ │ └── index.ts │ │ │ ├── Home │ │ │ │ ├── Home.styles.tsx │ │ │ │ ├── Home.test.tsx │ │ │ │ ├── Home.tsx │ │ │ │ └── index.ts │ │ │ ├── LanguageSelector │ │ │ │ ├── LanguageSelector.styles.ts │ │ │ │ ├── LanguageSelector.test.tsx │ │ │ │ ├── LanguageSelector.tsx │ │ │ │ └── index.ts │ │ │ ├── Loading │ │ │ │ ├── Loading.styles.tsx │ │ │ │ ├── Loading.tsx │ │ │ │ └── index.tsx │ │ │ ├── Navbar │ │ │ │ ├── Button.tsx │ │ │ │ ├── Logo.tsx │ │ │ │ ├── Menu.test.tsx │ │ │ │ ├── Menu.tsx │ │ │ │ ├── NavBar.styles.tsx │ │ │ │ ├── NavBar.test.tsx │ │ │ │ ├── Navbar.tsx │ │ │ │ ├── UserOptions.test.tsx │ │ │ │ ├── UserOptions.tsx │ │ │ │ └── index.tsx │ │ │ ├── NoMatch │ │ │ │ ├── NoMatch.styles.tsx │ │ │ │ ├── NoMatch.test.tsx │ │ │ │ ├── NoMatch.tsx │ │ │ │ └── index.ts │ │ │ ├── Settings │ │ │ │ ├── ChangePasswordForm.tsx │ │ │ │ ├── Form.tsx │ │ │ │ ├── Settings.styles.ts │ │ │ │ ├── Settings.test.tsx │ │ │ │ ├── Settings.tsx │ │ │ │ └── index.tsx │ │ │ └── UnauthenticatedApp │ │ │ │ ├── UnauthenticatedApp.js │ │ │ │ ├── UnauthenticatedApp.test.js │ │ │ │ └── index.js │ │ ├── context │ │ │ └── guestLocale.js │ │ ├── hooks │ │ │ ├── events.js │ │ │ └── guestLocale.js │ │ ├── index.js │ │ ├── layouts │ │ │ └── MainLayout │ │ │ │ ├── MainLayout.styles.ts │ │ │ │ ├── MainLayout.tsx │ │ │ │ └── index.tsx │ │ ├── locales │ │ │ ├── entries │ │ │ │ ├── en-US.js │ │ │ │ └── es-ES.js │ │ │ ├── index.js │ │ │ └── messages │ │ │ │ ├── en-US.js │ │ │ │ └── es-ES.js │ │ ├── pages │ │ │ ├── HomePage.js │ │ │ ├── NoMatchPage.js │ │ │ └── SettingsPage.js │ │ └── services │ │ │ ├── httpClient.js │ │ │ └── localeMiddleware.js │ └── auth │ │ ├── components │ │ ├── AuthWrapper │ │ │ ├── AuthWrapper.js │ │ │ ├── AuthWrapper.styles.js │ │ │ ├── AuthWrapper.test.js │ │ │ └── index.js │ │ ├── ForgotPassword │ │ │ ├── EmailForm.js │ │ │ ├── ForgotPassword.js │ │ │ ├── ForgotPassword.styles.js │ │ │ ├── ForgotPassword.test.js │ │ │ ├── PasswordForm.js │ │ │ ├── TokenForm.js │ │ │ └── index.js │ │ ├── SignIn │ │ │ ├── Form.js │ │ │ ├── SignIn.styles.js │ │ │ ├── SignIn.test.js │ │ │ ├── SignIn.tsx │ │ │ └── index.js │ │ └── SignUp │ │ │ ├── Form.js │ │ │ ├── SignUp.js │ │ │ ├── SignUp.test.js │ │ │ └── index.js │ │ ├── index.js │ │ ├── layouts │ │ └── AuthLayout │ │ │ ├── AuthLayout.js │ │ │ ├── AuthLayout.styles.js │ │ │ └── index.js │ │ └── pages │ │ ├── ForgotPasswordPage.js │ │ ├── SignInPage.js │ │ ├── SignUpPage.js │ │ └── index.js ├── helpers │ ├── decamelize.js │ ├── decamelize.test.js │ ├── errors.js │ └── string.js ├── index.js ├── index.test.js ├── react-app-env.d.ts ├── serviceWorker.js ├── setupTests.js ├── testUtils │ ├── fieldHelpers.js │ ├── index.js │ └── mocks │ │ ├── auth.js │ │ └── base.js └── theme.ts ├── tsconfig.json └── yarn.lock /.env: -------------------------------------------------------------------------------- 1 | REACT_APP_API_URL=https://rails-api-boilerplate.herokuapp.com/api/v1 2 | -------------------------------------------------------------------------------- /.env.development: -------------------------------------------------------------------------------- 1 | REACT_APP_API_URL=https://rails-api-boilerplate.herokuapp.com/api/v1 2 | -------------------------------------------------------------------------------- /.env.production: -------------------------------------------------------------------------------- 1 | REACT_APP_API_URL=https://path_to_server 2 | -------------------------------------------------------------------------------- /.env.test: -------------------------------------------------------------------------------- 1 | REACT_APP_API_URL=http://test.com/api/v1 2 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | build/ 2 | node_modules/ 3 | src/serviceWorker.js 4 | yarn.lock 5 | package.json 6 | /**/*.d.ts 7 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "es2020": true, 4 | "jest": true, 5 | "browser": true, 6 | "commonjs": true, 7 | "serviceworker": true 8 | }, 9 | "extends": [ 10 | "airbnb", 11 | "react-app", 12 | "plugin:import/errors", 13 | "plugin:import/warnings", 14 | "plugin:jest-dom/recommended", 15 | "plugin:testing-library/react", 16 | "plugin:prettier/recommended", 17 | "prettier/react" 18 | ], 19 | "settings": { 20 | "import/ignore": ["/testUtils"], 21 | "import/resolver": { 22 | "node": { 23 | "moduleDirectory": ["node_modules", "src/"], 24 | "extensions": [".js", ".jsx", ".ts", ".tsx"] 25 | } 26 | } 27 | }, 28 | "parser": "babel-eslint", 29 | "rules": { 30 | "no-console": "warn", 31 | "import/no-cycle": "off", 32 | "import/no-extraneous-dependencies": ["error", { "devDependencies": true }], 33 | "import/extensions": [ 34 | "error", 35 | "always", 36 | { 37 | "js": "never", 38 | "jsx": "never", 39 | "ts": "never", 40 | "tsx": "never" 41 | } 42 | ], 43 | "import/prefer-default-export": "off", 44 | "react/forbid-prop-types": "off", 45 | "react/jsx-props-no-spreading": "off", 46 | "react/jsx-filename-extension": "off", 47 | "prefer-promise-reject-errors": "off", 48 | "react/jsx-uses-react": "off", 49 | "react/react-in-jsx-scope": "off", 50 | "no-restricted-imports": [ 51 | "warn", 52 | { 53 | "paths": [ 54 | { 55 | "name": "react", 56 | "importNames": ["default"] 57 | } 58 | ] 59 | } 60 | ] 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | #.gitattributes 2 | # Declare files that will always have LF line endings on checkout. 3 | * text=auto eol=lf 4 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @loopstudio/react-devs 2 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: github-actions 9 | directory: / 10 | schedule: 11 | interval: daily 12 | 13 | - package-ecosystem: npm 14 | directory: / 15 | schedule: 16 | interval: daily 17 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | #### :page_facing_up: Description: 2 | 3 | Provide a good description of the problem this PR is trying to address. 4 | 5 | --- 6 | 7 | #### :pushpin: Notes: 8 | 9 | * Include TODOS, warnings, things other developers need to be aware of when merging, etc. 10 | 11 | --- 12 | 13 | #### :heavy_check_mark: Tasks: 14 | 15 | * Write the list of things you performed to help the reviewer. 16 | 17 | --- 18 | 19 | #### :play_or_pause_button: Demo: 20 | * Include a video or picture. 21 | 22 | @loopstudio/react-devs 23 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | workflow_dispatch: 5 | pull_request: 6 | branches: 7 | - 'master' 8 | - 'develop' 9 | push: 10 | branches: 11 | - 'master' 12 | - 'develop' 13 | - /^release.*/ 14 | 15 | jobs: 16 | build: 17 | name: Run tests and linters 18 | 19 | runs-on: ubuntu-latest 20 | 21 | steps: 22 | - name: Checkout 23 | uses: actions/checkout@v3 24 | 25 | - name: Setup node 26 | uses: actions/setup-node@v3.3.0 27 | with: 28 | node-version: '12.x' 29 | registry-url: 'https://npm.pkg.github.com' 30 | scope: '@loopstudio' 31 | 32 | - name: Restore cache 33 | uses: actions/cache@v3 34 | with: 35 | path: node_modules 36 | key: node-modules-${{ hashFiles('**/yarn.lock') }} 37 | restore-keys: | 38 | node-modules- 39 | 40 | - name: Install dependencies 41 | run: yarn install 42 | env: 43 | NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 44 | 45 | - name: Run linters 46 | run: yarn lint 47 | 48 | - name: Run tests 49 | run: yarn test 50 | 51 | - name: Run test coverage 52 | run: yarn test:coverage 53 | -------------------------------------------------------------------------------- /.github/workflows/dep-check.yml: -------------------------------------------------------------------------------- 1 | on: 2 | schedule: 3 | - cron: 0 6 * * 1 4 | workflow_dispatch: 5 | name: Update Dependencies 6 | jobs: 7 | package-update: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v3 11 | - name: set remote url 12 | run: git remote set-url --push origin https://$GITHUB_ACTOR:${{ secrets.DEPENDACOP_LOOPITO_PAT }}@github.com/$GITHUB_REPOSITORY 13 | - name: Dependacop 14 | uses: elestu/actions-dependacop@v1.0.4 15 | env: 16 | AUTHOR_EMAIL: hello@loopstudio.dev 17 | AUTHOR_NAME: Loopito 18 | EXECUTE: "true" 19 | GITHUB_TOKEN: ${{ secrets.DEPENDACOP_LOOPITO_PAT }} 20 | COMMIT_MESSAGE: "🛠 Update Dependencies" 21 | LOG_LEVEL: debug 22 | with: 23 | args: -u --packageFile package.json --dep prod 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | /.netlify 21 | /.vscode 22 | /.idea 23 | 24 | # logs 25 | npm-debug.log* 26 | yarn-debug.log* 27 | yarn-error.log* 28 | 29 | .npmrc 30 | .eslintcache 31 | -------------------------------------------------------------------------------- /.npmrc.template: -------------------------------------------------------------------------------- 1 | # Font Awesome Pro configuration. 2 | @fortawesome:registry=https://npm.fontawesome.com/ 3 | //npm.fontawesome.com/:_authToken=XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX 4 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | build/ 2 | node_modules/ 3 | yarn.lock 4 | package.json 5 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true 3 | } 4 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | #### Table Of Contents 2 | 3 | [Introduction](#introduction) 4 | 5 | [Styleguides](#styleguide) 6 | 7 | - [React Styleguide](#react-styleguide) 8 | 9 | [How Can I Contribute?](#how-can-i-contribute) 10 | 11 | - [Issue Tracker](#using-the-issue-tracker) 12 | - [Reporting Bugs](#bug-reports) 13 | - [Feature Requests](#feature-requests) 14 | - [Pull Requests](#pull-requests) 15 | 16 | [Maintainers](#maintainers) 17 | 18 | - [Reviewing Changes](#reviewing-changes) 19 | 20 | [Naming & Labelling](#naming-and-labelling) 21 | 22 | - [Branch Naming Convention](#branch-naming-convention) 23 | - [Issue and Pull Request Labels](#labeling-convention-for-pull-requests--issues) 24 | 25 | ## Introduction 26 | 27 | ### Welcome to LoopStudio's React App boilerplate! 28 | 29 | First off, thank you for considering contributing to our React App Boilerplate. It's people like you that make this boilerplate such a great tool. 30 | 31 | ### Why is this important? 32 | 33 | 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, assessing changes, and helping you finalize your pull requests. 34 | 35 | ### What sort of contributions are we looking for? 36 | 37 | LoopStudio's React App Boilerplate is an open-source project and we love to receive contributions from our community — you! There are many ways to contribute, from improving the documentation, submitting bug reports and feature requests or writing code which can be incorporated into the boilerplate itself. 38 | 39 | ## Styleguide 40 | 41 | ### React Style Guide 42 | 43 | We use our custom ESLint file to lint the code, so please make sure you set up the necessary tools in your preferred text editor. 44 | Pull Requests that do not adhere to our linting rules will be rejected. 45 | 46 | ## How can I contribute? 47 | 48 | ### Using the issue tracker 49 | 50 | The issue tracker is the preferred channel for bug reports, features requests and submitting pull requests, but please respect the following restrictions: 51 | 52 | - Please **do not** derail or troll issues. Keep the discussion on topic and 53 | respect the opinions of others. 54 | 55 | ### Bug reports 56 | 57 | A bug is a _demonstrable problem_ that is caused by the code in the repository. 58 | Good bug reports are extremely helpful - thank you! 59 | 60 | Guidelines for bug reports: 61 | 62 | 1. **Use the GitHub issue search** — check if the issue has already been reported. 63 | 64 | 2. **Check if the issue has been fixed** — try to reproduce it using the `master` branch in the repository. 65 | 66 | 3. **Isolate the problem** — ideally, create a reduced test case. 67 | 68 | A good bug report shouldn't leave others needing to chase you up for more 69 | information. Please try to be as detailed as possible in your report. What is 70 | your environment? What steps will reproduce the issue? What OS experiences the 71 | problem? What would you expect to be the outcome? All these details will help 72 | people to fix any potential bugs. 73 | 74 | Example: 75 | 76 | > Short and descriptive example bug report title 77 | > 78 | > A summary of the issue and the browser/OS environment in which it occurs. If 79 | > suitable, include the steps required to reproduce the bug. 80 | > 81 | > 1. This is the first step 82 | > 2. This is the second step 83 | > 3. Further steps, etc. 84 | > 85 | > `` - a link to the reduced test case, screenshots, videos, etc... 86 | > 87 | > Any other information you want to share that is relevant to the issue being 88 | > reported. This might include the lines of code that you have identified as 89 | > causing the bug, and potential solutions (and your opinions on their 90 | > merits). 91 | 92 | ### Feature requests 93 | 94 | Feature requests are welcome. But take a moment to find out whether your idea 95 | fits with the scope and aims of the project. It's up to _you_ to make a strong 96 | case to convince the project's developers of the merits of this feature. Please 97 | provide as much detail and context as possible. 98 | 99 | ### Pull requests 100 | 101 | Good pull requests - patches, improvements, new features - are a fantastic 102 | help. They should remain focused in scope and avoid containing unrelated 103 | commits. 104 | 105 | **Please ask first** before embarking on any significant pull request (e.g. 106 | implementing features, refactoring code), otherwise, you risk spending a lot of 107 | time working on something that the team might not want to merge 108 | into the project. 109 | 110 | #### For new Contributors 111 | 112 | 1. Make sure to update, or add to the tests when appropriate. Patches and 113 | features will not be accepted without tests. Run `npm test` to check that 114 | all tests pass after you've made changes. Look for a `Testing` section in 115 | the project’s README for more information. 116 | 117 | 2. If you added or changed a feature, make sure to document it accordingly in 118 | the `README.md` file. 119 | 120 | 3. Open a Pull Request using your branch with a clear title and description. 121 | 122 | ## Maintainers 123 | 124 | ### Reviewing changes 125 | 126 | 1. Check that a change is within the scope and philosophy of the component. 127 | 2. Check that a change has any necessary tests. 128 | 3. Check that a change has any necessary documentation. 129 | 4. If there is anything you don’t like, leave a comment below the respective 130 | lines and submit a "Request changes" review. Repeat until everything has 131 | been addressed. 132 | 5. If you are not sure about something, mention `@LoopStudio` or specific 133 | people for help in a comment. 134 | 6. Once everything looks good, add an "Approve" review. Don’t forget to say 135 | something nice 👏🐶💖✨ 136 | 137 | \*\* **We require at least 2 approvals to consider a Pull Request good to be merged** \*\* 138 | 139 | ## Naming and Labelling 140 | 141 | ### Branch naming convention 142 | 143 | | Label name | Description | 144 | | ---------- | ------------------------------------------------ | 145 | | `feature` | Developing a new feature | 146 | | `bug` | Fixing a bug | 147 | | `chore` | Maintenance work. I.E: dependencies update, etc. | 148 | 149 | ### Labeling convention for Pull Requests & Issues 150 | 151 | This section lists the labels we use to help us track and manage issues and pull requests. 152 | 153 | [GitHub search](https://help.github.com/articles/searching-issues/) makes it easy to use labels for finding groups of issues or pull requests you're interested in. For example, you might be interested in [open issues](https://github.com/search?utf8=%E2%9C%93&q=is%3Aopen+is%3Aissue+user%3Aloopstudio+label%3Abug+label%3Aneeds-reproduction) across `LoopStudio`'s repos and which are labeled as bugs, but still need to be reliably reproduced or perhaps [open pull requests](https://github.com/search?q=is%3Aopen+is%3Apr+repo%3ALoopStudio%2Freact-app-boilerplate+comments%3A0) in `LoopStudio/react-app-boilerplate` which haven't been reviewed yet. To help you find issues and pull requests, each label is listed with search links for finding open items with that label in `LoopStudio/react-app-boilerplate` only and also across all LoopStudio's repositories. We encourage you to read about [other search filters](https://help.github.com/articles/searching-issues/) which will help you write more focused queries. 154 | 155 | The labels are loosely grouped by their purpose, but it's not required that every issue has a label from every group or that an issue can't have more than one label from the same group. 156 | 157 | Please open an issue on our [style guides repo](https://github.com/LoopStudio/loop-studio-guides) if you have suggestions for new labels, and if you notice some labels are missing on some repositories, then please open an issue on that repository. 158 | 159 | #### Type of Issue and Issue State 160 | 161 | | Label name | `LoopStudio/react-app-boilerplate` :mag_right: | `LoopStudio`‑org :mag_right: | Description | 162 | | ------------------------- | ------------------------------------------------------------------------- | ------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- | 163 | | `enhancement` | [search][search-react-app-boilerplate-repo-label-enhancement] | [search][search-loopstudio-org-label-enhancement] | Feature requests. | 164 | | `bug` | [search][search-react-app-boilerplate-repo-label-bug] | [search][search-loopstudio-org-label-bug] | Confirmed bugs or reports that are very likely to be bugs. | 165 | | `question` | [search][search-react-app-boilerplate-repo-label-question] | [search][search-loopstudio-org-label-question] | Questions more than bug reports or feature requests (e.g. how do I do X). | 166 | | `feedback` | [search][search-react-app-boilerplate-repo-label-feedback] | [search][search-loopstudio-org-label-feedback] | General feedback more than bug reports or feature requests. | 167 | | `help-wanted` | [search][search-react-app-boilerplate-repo-label-help-wanted] | [search][search-loopstudio-org-label-help-wanted] | When help from the team in resolving these issues is needed. | 168 | | `beginner` | [search][search-react-app-boilerplate-repo-label-beginner] | [search][search-loopstudio-org-label-beginner] | Less complex issues which would be good first issues to work on for users who want to contribute. | 169 | | `more-information-needed` | [search][search-react-app-boilerplate-repo-label-more-information-needed] | [search][search-loopstudio-org-label-more-information-needed] | More information needs to be collected about these problems or feature requests (e.g. steps to reproduce). | 170 | | `needs-reproduction` | [search][search-react-app-boilerplate-repo-label-needs-reproduction] | [search][search-loopstudio-org-label-needs-reproduction] | Likely bugs, but haven't been reliably reproduced. | 171 | | `blocked` | [search][search-react-app-boilerplate-repo-label-blocked] | [search][search-loopstudio-org-label-blocked] | Issues blocked on other issues. | 172 | | `duplicate` | [search][search-react-app-boilerplate-repo-label-duplicate] | [search][search-loopstudio-org-label-duplicate] | Issues which are duplicates of other issues, i.e. they have been reported before. | 173 | | `wontfix` | [search][search-react-app-boilerplate-repo-label-wontfix] | [search][search-loopstudio-org-label-wontfix] | LoopStudio team has decided not to fix these issues for now, either because they're working as intended or for some other reason. | 174 | | `invalid` | [search][search-react-app-boilerplate-repo-label-invalid] | [search][search-loopstudio-org-label-invalid] | Issues which aren't valid (e.g. user errors). | 175 | | `package-idea` | [search][search-react-app-boilerplate-repo-label-package-idea] | [search][search-loopstudio-org-label-package-idea] | Feature request which might be good candidates for a library. | 176 | | `wrong-repo` | [search][search-react-app-boilerplate-repo-label-wrong-repo] | [search][search-loopstudio-org-label-wrong-repo] | Issues reported on the wrong repository. | 177 | 178 | #### Topic Categories 179 | 180 | | Label name | `LoopStudio/react-app-boilerplate` :mag_right: | `LoopStudio`‑org :mag_right: | Description | 181 | | ------------------ | ------------------------------------------------------------------ | ------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------- | 182 | | `documentation` | [search][search-react-app-boilerplate-repo-label-documentation] | [search][search-loopstudio-org-label-documentation] | Related to any type of documentation. | 183 | | `performance` | [search][search-react-app-boilerplate-repo-label-performance] | [search][search-loopstudio-org-label-performance] | Related to performance. | 184 | | `security` | [search][search-react-app-boilerplate-repo-label-security] | [search][search-loopstudio-org-label-security] | Related to security. | 185 | | `ui` | [search][search-react-app-boilerplate-repo-label-ui] | [search][search-loopstudio-org-label-ui] | Related to visual design. | 186 | | `api` | [search][search-react-app-boilerplate-repo-label-api] | [search][search-loopstudio-org-label-api] | Related to the boilerplate's APIs service. | 187 | | `crash` | [search][search-react-app-boilerplate-repo-label-crash] | [search][search-loopstudio-org-label-crash] | Reports of the boilerplate completely crashing. | 188 | | `git` | [search][search-react-app-boilerplate-repo-label-git] | [search][search-loopstudio-org-label-git] | Related to Git functionality (e.g. problems with gitignore files or with showing the correct file status). | 189 | | `build-error` | [search][search-react-app-boilerplate-repo-label-build-error] | [search][search-loopstudio-org-label-build-error] | Related to problems with building the boilerplate from source | 190 | | `deprecation-help` | [search][search-react-app-boilerplate-repo-label-deprecation-help] | [search][search-loopstudio-org-label-deprecation-help] | Issues related to deprecation of APIs. | 191 | 192 | #### Pull Request Labels 193 | 194 | | Label name | `LoopStudio/react-app-boilerplate` :mag_right: | `LoopStudio`‑org :mag_right: | Description | 195 | | --------------------------- | --------------------------------------------------------------------------- | --------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | 196 | | `work-in-progress` | [search][search-react-app-boilerplate-repo-label-work-in-progress] | [search][search-loopstudio-org-label-work-in-progress] | Pull requests which are still being worked on, more changes will follow. | 197 | | `depends-on-another-branch` | [search][search-react-app-boilerplate-repo-label-depends-on-another-branch] | [search][search-loopstudio-org-label-depends-on-another-branch] | Pull requests which depends on another branch to be merged. | 198 | | `needs-review` | [search][search-react-app-boilerplate-repo-label-needs-review] | [search][search-loopstudio-org-label-needs-review] | Pull requests which need code review and approval from maintainers. | 199 | | `under-review` | [search][search-react-app-boilerplate-repo-label-under-review] | [search][search-loopstudio-org-label-under-review] | Pull requests being reviewed by maintainers. | 200 | | `requires-changes` | [search][search-react-app-boilerplate-repo-label-requires-changes] | [search][search-loopstudio-org-label-requires-changes] | Pull requests which need to be updated based on review comments and then reviewed again. | 201 | | `needs-testing` | [search][search-react-app-boilerplate-repo-label-needs-testing] | [search][search-loopstudio-org-label-needs-testing] | Pull requests which need manual testing. | 202 | 203 | ## Community 204 | 205 | We gather together once a month to discuss industry trends, best practices, and, of course, this project, in what we decided to call **Loop's React Tech Council**. 206 | 207 | [search-react-app-boilerplate-repo-label-enhancement]: https://github.com/search?q=is%3Aopen+is%3Aissue+repo%3ALoopStudio%2Freact%2Dapp%2Dboilerplate+label%3Aenhancement 208 | [search-loopstudio-org-label-enhancement]: https://github.com/search?q=is%3Aopen+is%3Aissue+user%3Aloopstudio+label%3Aenhancement 209 | [search-react-app-boilerplate-repo-label-bug]: https://github.com/search?q=is%3Aopen+is%3Aissue+repo%3ALoopStudio%2Freact%2Dapp%2Dboilerplate+label%3Abug 210 | [search-loopstudio-org-label-bug]: https://github.com/search?q=is%3Aopen+is%3Aissue+user%3Aloopstudio+label%3Abug 211 | [search-react-app-boilerplate-repo-label-question]: https://github.com/search?q=is%3Aopen+is%3Aissue+repo%3ALoopStudio%2Freact%2Dapp%2Dboilerplate+label%3Aquestion 212 | [search-loopstudio-org-label-question]: https://github.com/search?q=is%3Aopen+is%3Aissue+user%3Aloopstudio+label%3Aquestion 213 | [search-react-app-boilerplate-repo-label-feedback]: https://github.com/search?q=is%3Aopen+is%3Aissue+repo%3ALoopStudio%2Freact%2Dapp%2Dboilerplate+label%3Afeedback 214 | [search-loopstudio-org-label-feedback]: https://github.com/search?q=is%3Aopen+is%3Aissue+user%3Aloopstudio+label%3Afeedback 215 | [search-react-app-boilerplate-repo-label-help-wanted]: https://github.com/search?q=is%3Aopen+is%3Aissue+repo%3ALoopStudio%2Freact%2Dapp%2Dboilerplate+label%3Ahelp-wanted 216 | [search-loopstudio-org-label-help-wanted]: https://github.com/search?q=is%3Aopen+is%3Aissue+user%3Aloopstudio+label%3Ahelp-wanted 217 | [search-react-app-boilerplate-repo-label-beginner]: https://github.com/search?q=is%3Aopen+is%3Aissue+repo%3ALoopStudio%2Freact%2Dapp%2Dboilerplate+label%3Abeginner 218 | [search-loopstudio-org-label-beginner]: https://github.com/search?q=is%3Aopen+is%3Aissue+user%3Aloopstudio+label%3Abeginner 219 | [search-react-app-boilerplate-repo-label-more-information-needed]: https://github.com/search?q=is%3Aopen+is%3Aissue+repo%3LoopStudio%2Freact%2Dapp%2Dboilerplate+label%3Amore-information-needed 220 | [search-loopstudio-org-label-more-information-needed]: https://github.com/search?q=is%3Aopen+is%3Aissue+user%3Aloopstudio+label%3Amore-information-needed 221 | [search-react-app-boilerplate-repo-label-needs-reproduction]: https://github.com/search?q=is%3Aopen+is%3Aissue+repo%3ALoopStudio%2Freact%2Dapp%2Dboilerplate+label%3Aneeds-reproduction 222 | [search-loopstudio-org-label-needs-reproduction]: https://github.com/search?q=is%3Aopen+is%3Aissue+user%3Aloopstudio+label%3Aneeds-reproduction 223 | [search-react-app-boilerplate-repo-label-documentation]: https://github.com/search?q=is%3Aopen+is%3Aissue+repo%3ALoopStudio%2Freact%2Dapp%2Dboilerplate+label%3Adocumentation 224 | [search-loopstudio-org-label-documentation]: https://github.com/search?q=is%3Aopen+is%3Aissue+user%3Aloopstudio+label%3Adocumentation 225 | [search-react-app-boilerplate-repo-label-performance]: https://github.com/search?q=is%3Aopen+is%3Aissue+repo%3ALoopStudio%2Freact%2Dapp%2Dboilerplate+label%3Aperformance 226 | [search-loopstudio-org-label-performance]: https://github.com/search?q=is%3Aopen+is%3Aissue+user%3Aloopstudio+label%3Aperformance 227 | [search-react-app-boilerplate-repo-label-security]: https://github.com/search?q=is%3Aopen+is%3Aissue+repo%3ALoopStudio%2Freact%2Dapp%2Dboilerplate+label%3Asecurity 228 | [search-loopstudio-org-label-security]: https://github.com/search?q=is%3Aopen+is%3Aissue+user%3Aloopstudio+label%3Asecurity 229 | [search-react-app-boilerplate-repo-label-ui]: https://github.com/search?q=is%3Aopen+is%3Aissue+repo%3ALoopStudio%2Freact%2Dapp%2Dboilerplate+label%3Aui 230 | [search-loopstudio-org-label-ui]: https://github.com/search?q=is%3Aopen+is%3Aissue+user%3Aloopstudio+label%3Aui 231 | [search-react-app-boilerplate-repo-label-api]: https://github.com/search?q=is%3Aopen+is%3Aissue+repo%3ALoopStudio%2Freact%2Dapp%2Dboilerplate+label%3Aapi 232 | [search-loopstudio-org-label-api]: https://github.com/search?q=is%3Aopen+is%3Aissue+user%3Aloopstudio+label%3Aapi 233 | [search-react-app-boilerplate-repo-label-crash]: https://github.com/search?q=is%3Aopen+is%3Aissue+repo%3ALoopStudio%2Freact%2Dapp%2Dboilerplate+label%3Acrash 234 | [search-loopstudio-org-label-crash]: https://github.com/search?q=is%3Aopen+is%3Aissue+user%3Aloopstudio+label%3Acrash 235 | [search-react-app-boilerplate-repo-label-git]: https://github.com/search?q=is%3Aopen+is%3Aissue+repo%3ALoopStudio%2Freact%2Dapp%2Dboilerplate+label%3Agit 236 | [search-loopstudio-org-label-git]: https://github.com/search?q=is%3Aopen+is%3Aissue+user%3Aloopstudio+label%3Agit 237 | [search-react-app-boilerplate-repo-label-blocked]: https://github.com/search?q=is%3Aopen+is%3Aissue+repo%3ALoopStudio%2Freact%2Dapp%2Dboilerplate+label%3Ablocked 238 | [search-loopstudio-org-label-blocked]: https://github.com/search?q=is%3Aopen+is%3Aissue+user%3Aloopstudio+label%3Ablocked 239 | [search-react-app-boilerplate-repo-label-duplicate]: https://github.com/search?q=is%3Aopen+is%3Aissue+repo%3ALoopStudio%2Freact%2Dapp%2Dboilerplate+label%3Aduplicate 240 | [search-loopstudio-org-label-duplicate]: https://github.com/search?q=is%3Aopen+is%3Aissue+user%3Aloopstudio+label%3Aduplicate 241 | [search-react-app-boilerplate-repo-label-wontfix]: https://github.com/search?q=is%3Aopen+is%3Aissue+repo%3ALoopStudio%2Freact%2Dapp%2Dboilerplate+label%3Awontfix 242 | [search-loopstudio-org-label-wontfix]: https://github.com/search?q=is%3Aopen+is%3Aissue+user%3Aloopstudio+label%3Awontfix 243 | [search-react-app-boilerplate-repo-label-invalid]: https://github.com/search?q=is%3Aopen+is%3Aissue+repo%3ALoopStudio%2Freact%2Dapp%2Dboilerplate+label%3Ainvalid 244 | [search-loopstudio-org-label-invalid]: https://github.com/search?q=is%3Aopen+is%3Aissue+user%3Aloopstudio+label%3Ainvalid 245 | [search-react-app-boilerplate-repo-label-wrong-repo]: https://github.com/search?q=is%3Aopen+is%3Aissue+repo%3ALoopStudio%2Freact%2Dapp%2Dboilerplate+label%3Awrong-repo 246 | [search-react-app-boilerplate-repo-label-package-idea]: https://github.com/search?q=is%3Aopen+is%3Aissue+repo%3ALoopStudio%2Freact%2Dapp%2Dboilerplate+label%3Apackage-idea 247 | [search-loopstudio-org-label-package-idea]: https://github.com/search?q=is%3Aopen+is%3Aissue+user%3Aloopstudio+label%3Apackage-idea 248 | [search-loopstudio-org-label-wrong-repo]: https://github.com/search?q=is%3Aopen+is%3Aissue+user%3Aloopstudio+label%3Awrong-repo 249 | [search-react-app-boilerplate-repo-label-build-error]: https://github.com/search?q=is%3Aopen+is%3Aissue+repo%3ALoopStudio%2Freact%2Dapp%2Dboilerplate+label%3Abuild-error 250 | [search-loopstudio-org-label-build-error]: https://github.com/search?q=is%3Aopen+is%3Aissue+user%3Aloopstudio+label%3Abuild-error 251 | [search-react-app-boilerplate-repo-label-deprecation-help]: https://github.com/search?q=is%3Aopen+is%3Aissue+repo%3ALoopStudio%2Freact%2Dapp%2Dboilerplate+label%3Adeprecation-help 252 | [search-loopstudio-org-label-deprecation-help]: https://github.com/search?q=is%3Aopen+is%3Aissue+user%3Aloopstudio+label%3Adeprecation-help 253 | [search-react-app-boilerplate-repo-label-work-in-progress]: https://github.com/search?q=is%3Aopen+is%3Apr+repo%3ALoopStudio%2Freact%2Dapp%2Dboilerplate+label%3Awork-in-progress 254 | [search-loopstudio-org-label-work-in-progress]: https://github.com/search?q=is%3Aopen+is%3Apr+user%3Aloopstudio+label%3Awork-in-progress 255 | [search-react-app-boilerplate-repo-label-depends-on-another-branch]: https://github.com/search?q=is%3Aopen+is%3Apr+repo%3ALoopStudio%2Freact%2Dapp%2Dboilerplate+label%3Adepends-on-another-branch 256 | [search-loopstudio-org-label-depends-on-another-branch]: https://github.com/search?q=is%3Aopen+is%3Apr+user%3Aloopstudio+label%3Adepends-on-another-branch 257 | [search-react-app-boilerplate-repo-label-needs-review]: https://github.com/search?q=is%3Aopen+is%3Apr+repo%3ALoopStudio%2Freact%2Dapp%2Dboilerplate+label%3Aneeds-review 258 | [search-loopstudio-org-label-needs-review]: https://github.com/search?q=is%3Aopen+is%3Apr+user%3Aloopstudio+label%3Aneeds-review 259 | [search-react-app-boilerplate-repo-label-under-review]: https://github.com/search?q=is%3Aopen+is%3Apr+repo%3ALoopStudio%2Freact%2Dapp%2Dboilerplate+label%3Aunder-review 260 | [search-loopstudio-org-label-under-review]: https://github.com/search?q=is%3Aopen+is%3Apr+user%3Aloopstudio+label%3Aunder-review 261 | [search-react-app-boilerplate-repo-label-requires-changes]: https://github.com/search?q=is%3Aopen+is%3Apr+repo%3ALoopStudio%2Freact%2Dapp%2Dboilerplate+label%3Arequires-changes 262 | [search-loopstudio-org-label-requires-changes]: https://github.com/search?q=is%3Aopen+is%3Apr+user%3Aloopstudio+label%3Arequires-changes 263 | [search-react-app-boilerplate-repo-label-needs-testing]: https://github.com/search?q=is%3Aopen+is%3Apr+repo%3ALoopStudio%2Freact%2Dapp%2Dboilerplate+label%3Aneeds-testing 264 | [search-loopstudio-org-label-needs-testing]: https://github.com/search?q=is%3Aopen+is%3Apr+user%3Aloopstudio+label%3Aneeds-testing 265 | [beginner]: https://github.com/search?utf8=%E2%9C%93&q=is%3Aopen+is%3Aissue+label%3Abeginner+label%3Ahelp-wanted+user%3Aloopstudio+sort%3Acomments-desc 266 | [help-wanted]: https://github.com/search?q=is%3Aopen+is%3Aissue+label%3Ahelp-wanted+user%3Aloopstudio+sort%3Acomments-desc+-label%3Abeginner 267 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 LoopStudio 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![react app banner](https://user-images.githubusercontent.com/15303963/84329475-1e7b6b80-ab5b-11ea-828d-9249c79ab522.png) 2 | 3 |

An opinionated boilerplate code for starting a new react web project.

4 | 5 | ![GitHub Actions Badge](https://github.com/loopstudio/react-app-boilerplate/workflows/CI/badge.svg) 6 | [![Codebeat Badge](https://codebeat.co/badges/6382173b-82aa-4fe5-9087-87d53bd5595e)](https://codebeat.co/a/loopstudio/projects/github-com-loopstudio-react-app-boilerplate-master) 7 | [![Netlify Status](https://api.netlify.com/api/v1/badges/5ead8491-1da0-4eaa-8739-fdc8971d1d07/deploy-status)](https://app.netlify.com/sites/react-app-boilerplate/deploys) 8 | [![Total alerts](https://img.shields.io/lgtm/alerts/g/loopstudio/react-app-boilerplate.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/loopstudio/react-app-boilerplate/alerts/) 9 | [![Language grade: JavaScript](https://img.shields.io/lgtm/grade/javascript/g/loopstudio/react-app-boilerplate.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/loopstudio/react-app-boilerplate/context:javascript) 10 | 11 | Created and maintained with ❤️ by LoopStudio 12 | 13 | ## Table of Contents 14 | - [Project Structure](#project-structure) 15 | - [Features](#features) 16 | - [Prerequisites](#prerequisites) 17 | - [List of Packages](#list-of-packages) 18 | - [Recommended Extensions](#recommended-extensions) 19 | - [Getting Started](#getting-started) 20 | - [Running the Test Suite](#running-the-test-suite) 21 | - [Contributing](CONTRIBUTING.md) 22 | - [Credits](#credits) 23 | 24 | ## Project Structure 25 | 26 | ``` 27 | . 28 | ├── .github/ 29 | ├── public/ 30 | ├── src 31 | │   ├── assets/ 32 | │   ├── features/ 33 | │   ├── helpers/ 34 | │   ├── store/ 35 | │   ├── testUtils/ 36 | │   ├── index.js 37 | │   ├── serviceWorker.js 38 | │   ├── setupTests.js 39 | │   └── theme.js 40 | ├── .env 41 | ├── .gitignore 42 | ├── .eslintrc 43 | ├── .prettierrc 44 | ├── CONTRIBUTING.md 45 | ├── jsconfig.json 46 | ├── LICENSE 47 | ├── netlify.toml 48 | ├── package.json 49 | ├── README.md 50 | └── yarn.lock 51 | ``` 52 | 53 | ## Feature Structure 54 | 55 | ``` 56 | ├── features 57 | │ └── myFeature 58 | │ ├── components/ 59 | │ ├── hooks/ 60 | │ ├── layouts/ 61 | │ ├── locales/ 62 | │ ├── pages/ 63 | │ ├── services/ 64 | │ ├── actions.js 65 | │ ├── reducer.js 66 | │ ├── types.js 67 | │ └── index.js 68 | ``` 69 | 70 | #### What does a feature export? 71 | 72 | A feature should export anything that is meant to be consumed from outside the feature 73 | IE: 74 | - actions 75 | - reducer 76 | - components (if they're meant to be used outside the feature) 77 | - hooks (if they're meant to be used outside the feature) 78 | - helpers (if they're meant to be used outside the feature) 79 | 80 | ## Component Structure 81 | 82 | ``` 83 | ├── MyComponent 84 | │ ├── index.js 85 | │ ├── MyComponent.js 86 | │ ├── MyComponent.styles.js 87 | │ ├── MyComponent.test.js 88 | ``` 89 | 90 | ### Optional 91 | 92 | If you want to split your component into pieces for readability, maintainability, or any other reason you could put the secondary components in the same folder. This is only for cases where these secondary components are only used inside MyComponent. If later they want to be used in other places they should be extracted to their own folder inside components. 93 | 94 | ``` 95 | ├── MyComponent 96 | │ ├── index.js 97 | │ ├── MyComponent.js 98 | │ ├── MyComponent.styles.js 99 | │ ├── MyComponent.test.js 100 | │ ├── SecondaryComponent.js 101 | │ ├── SecondaryComponent.styles.js 102 | ``` 103 | 104 | ## Features 105 | 106 | 1. Based on [create-react-app](https://create-react-app.dev/). 107 | 2. Code splitting and prefetching. 108 | 3. Errors handling. 109 | 4. The httpClient provides status code errors handling and camelCase to snake_case automatic conversion. 110 | 5. Async actions and store hydration. 111 | 6. Internationalization. 112 | 7. Concurrent Mode ready. 113 | 8. [Absolute imports](https://create-react-app.dev/docs/importing-a-component/#absolute-imports). 114 | 9. Environment-specific settings provided through the built-in [environment variables](https://create-react-app.dev/docs/adding-custom-environment-variables) system provided by CRA. 115 | 116 | ## Prerequisites 117 | 118 | 1. Install [Node.js](https://nodejs.org/en/) 10.16.3 or greater. 119 | 2. Install [Yarn](https://yarnpkg.com/lang/en/) as a package manager. 120 | 121 | ## List of Packages 122 | 123 | ### State management 124 | 125 | - [redux](https://redux.js.org): the state management solution. 126 | - [react-redux](https://react-redux.js.org): provides an easy way to connect the React components to the store. 127 | - [redux-persist](https://github.com/rt2zz/redux-persist): persists the user session data in the localStorage. 128 | - [redux-promise-middleware](https://docs.psb.design/redux-promise-middleware/): handle async actions and side effects. 129 | - [redux-thunk](https://github.com/reduxjs/redux-thunk): as a power-up for the 'redux-promise-middleware'. 130 | - [redux-devtools-extension](http://extension.remotedev.io/): useful tool for debugging. 131 | 132 | ### Immutability helper: 133 | 134 | - [immer](https://immerjs.github.io/immer): provides an easy way to work with immutable state. 135 | 136 | ### API Requests: 137 | 138 | - [axios](https://github.com/axios/axios): HTTP client. 139 | - [axios-case-converter](https://github.com/domchristie/humps): axios interceptor that converts snake_case/camelCase. 140 | 141 | ### Routing: 142 | 143 | - [react-router-dom](https://reacttraining.com/react-router/web/guides/quick-start): the most popular and powerful routing solution for React. 144 | 145 | ### Type checking: 146 | 147 | - [prop-types](https://github.com/facebook/prop-types): Runtime type checking for React props and similar objects. 148 | 149 | ### Datetime: 150 | 151 | - [dayjs](https://github.com/iamkun/dayjs): A minimalist JavaScript library that parses, validates, manipulates, and displays dates and times for modern browsers with a largely Moment.js-compatible API. This tool isn't installed in our project, but we recommend to use it, you can install it running `yarn add dayjs`. 152 | 153 | ### Testing: 154 | 155 | - [jest](https://jestjs.io/): a delightful JavaScript Testing Framework with a focus on simplicity. 156 | - [react-testing-library](https://testing-library.com/docs/react-testing-library/intro): a very light-weight solution for testing React components. 157 | - [nock](https://github.com/nock/nock): HTTP server mocking and expectations library for Node.js 158 | - [cypress](https://www.cypress.io/): automated integration tests. This tool isn't installed in our project, but we recommend to use it. You can install it running `yarn add cypress --dev`. For more information about the configuration, you can read [this guide](https://docs.cypress.io/guides/getting-started/installing-cypress.html#System-requirements) 159 | - [storybook](https://storybook.js.org/docs/react/get-started/introduction): when your app is going to have a set of components that you want to manual test on isolation you should consider Storybook which is not installed on our project but you could simply do so by running `npx sb init`. After running that command you must check out its many addons that will improve your experience using this tool. 160 | 161 | ### Styling: 162 | 163 | - [emotion](https://emotion.sh/docs/introduction): Provides powerful and predictable style composition in addition to a great developer experience with features such as source maps, labels, and testing utilities. Both string and object styles are supported. 164 | - [font-awesome](https://fontawesome.com/): A comprehensive icon library. Currently, the free version of Font Awesome is added by default to the boilerplate. To upgrade and use the pro version sign in to your Font Awesome account and follow the instructions on this [link](https://fontawesome.com/how-to-use/on-the-web/setup/using-package-managers#installing-pro). 165 | Imported icons are encouraged to be stored in `assets/icons`, and invoked as a string in each component. 166 | 167 | ### Deployment: 168 | 169 | - [netlify](https://www.netlify.com/): "The fastest way to build the fastest sites". We recommend Netlify as a hosting solution. The free plan is very generous and meets the basic needs of any standard project. Also, it is dead simple to set up and use. For more information, you can dig into the [official docs](https://docs.netlify.com/). 170 | 171 | ### Error Monitoring: 172 | 173 | **NOTE: These tools are not enabled by default. For instructions on how to set them up, please visit their respective vendor website** 174 | 175 | - [sentry](https://github.com/getsentry/sentry-javascript#installation-and-usage): Sentry provides self-hosted and cloud-based error monitoring that helps all software teams discover, triage, and prioritize errors in real-time. 176 | - [sentry + logrocket](https://docs.logrocket.com/docs/sentry): Add a LogRocket session recording URL to every Sentry exception report. The integration of both of these tools allows us to access a video recording of the exact moment a user encountered an error including network events and console output. 177 | 178 | 179 | ## Recommended Extensions 180 | 181 | ### Style / Linting 182 | 183 | VSCode: 184 | - [Prettier](https://github.com/prettier/prettier-vscode) - An opinionated code formatter. 185 | - [ESLint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) - Integrates [ESLint](http://eslint.org/) into VS Code. 186 | 187 | Sublime: 188 | - [Prettier](https://packagecontrol.io/packages/JsPrettier) - JsPrettier is a Sublime Text Plug-in for Prettier, the opinionated code formatter. 189 | - [ESLint](https://packagecontrol.io/packages/ESLint) - ESLint any JavaScript file in Sublime Text. 190 | 191 | ### Intellisense 192 | 193 | VSCode: 194 | - [Visual Studio IntelliCode](https://marketplace.visualstudio.com/items?itemName=VisualStudioExptTeam.vscodeintellicode) - AI-assisted development features. 195 | - [Path Intellisense](https://marketplace.visualstudio.com/items?itemName=christian-kohler.path-intellisense) - Visual Studio Code plugin that autocompletes filenames. 196 | - [npm Intellisense](https://marketplace.visualstudio.com/items?itemName=christian-kohler.npm-intellisense) - Visual Studio Code plugin that autocompletes npm modules in import statements. 197 | 198 | Sublime: 199 | - [SublimeCodeIntel](https://github.com/SublimeCodeIntel/SublimeCodeIntel) - Full-featured code intelligence and smart autocomplete engine. 200 | - [AutoFileName](https://packagecontrol.io/packages/AutoFileName) - Sublime Text plugin that autocompletes filenames. 201 | 202 | ### Version Control 203 | 204 | VSCode: 205 | - [Git Blame](https://marketplace.visualstudio.com/items?itemName=waderyan.gitblame) - See Git Blame information in the status bar for the currently selected line. 206 | 207 | Sublime: 208 | - [Git Blame](https://packagecontrol.io/packages/Git%20blame) - Show Git blame information while viewing a file in Sublime Text. 209 | 210 | ### Syntax Highlighting 211 | 212 | VSCode: 213 | - [DotENV](https://marketplace.visualstudio.com/items?itemName=mikestead.dotenv) - A port of [DotENV](https://github.com/zaynali53/DotENV) for VSCode. 214 | - [Color Highlight](https://marketplace.visualstudio.com/items?itemName=naumovs.color-highlight) - This extension styles CSS/web colors found in your document. 215 | - [VSCode Styled Components](https://marketplace.visualstudio.com/items?itemName=jpoissonnier.vscode-styled-components) - Syntax highlighting and IntelliSense for styled-components or emotion. 216 | 217 | Sublime: 218 | - [DotENV](https://packagecontrol.io/packages/DotENV) - SublimeText Syntax Highlighting support for Environment (.env) Files 219 | - [Color Highlight](https://packagecontrol.io/packages/Color%20Highlight) - 🎨 Lightweight Color Highlight colorizer for Sublime Text 220 | 221 | ### Snippets 222 | 223 | VSCode: 224 | - [ES7 React/Redux/GraphQL/React-Native snippets](https://marketplace.visualstudio.com/items?itemName=dsznajder.es7-react-js-snippets) - This extension provides you JavaScript and React/Redux snippets in ES7 with Babel plugin features for VSCode. 225 | 226 | Sublime: 227 | - [ES7 React/Redux/GraphQL/React-Native snippets](https://packagecontrol.io/packages/Sublime%20ES7%20React%20Redux%20ReactNative%20JS%20snippets) - Sublime ES7 React/Redux/React-Native/JS snippets. 228 | 229 | ## Getting Started 230 | 231 | 1. Clone this repository and navigate to the folder. 232 | 2. Install all dependencies by running the `yarn install` command in the root directory. 233 | 3. Modify the environment variables files in root folder(.env.dev, .env.staging and .env.prod). 234 | 4. Start the dev server: `yarn start` command. 235 | 236 | ## Running the Test Suite 237 | 238 | 1. Run the `yarn test` command. 239 | 240 | ## Credits 241 | 242 | React App Boilerplate is maintained by [LoopStudio](https://loopstudio.dev). 243 | 244 | [](https://loopstudio.dev) 245 | -------------------------------------------------------------------------------- /netlify.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | base = "" 3 | publish = "build" 4 | command = "yarn build" 5 | 6 | [context.production.environment] 7 | YARN_VERSION = "1.22.4" 8 | NODE_VERSION = "12.16.3" 9 | 10 | [[redirects]] 11 | from = "/*" 12 | to = "/index.html" 13 | status = 200 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-boilerplate", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "analyze": "source-map-explorer 'build/static/js/*.js'", 7 | "start": "react-scripts start", 8 | "build": "react-scripts build", 9 | "test": "react-scripts test --env=jest-environment-jsdom-sixteen", 10 | "test:debug": "react-scripts --inspect-brk test --runInBand --no-cache", 11 | "test:debug-nock": "DEBUG=nock.* react-scripts test --env=jest-environment-jsdom-sixteen", 12 | "test:coverage": "react-scripts test --ci --env=jest-environment-jsdom-sixteen --coverage --watchAll=false", 13 | "lint": "eslint .", 14 | "eject": "react-scripts eject", 15 | "pre-push": "eslint ." 16 | }, 17 | "jest": { 18 | "coverageReporters": [ 19 | "json", 20 | "lcov", 21 | "text", 22 | "clover", 23 | "text-summary" 24 | ], 25 | "coverageThreshold": { 26 | "global": { 27 | "branches": 85, 28 | "functions": 85, 29 | "lines": 85, 30 | "statements": 85 31 | } 32 | } 33 | }, 34 | "browserslist": { 35 | "production": [ 36 | ">0.2%", 37 | "not dead", 38 | "not op_mini all" 39 | ], 40 | "development": [ 41 | "last 1 chrome version", 42 | "last 1 firefox version", 43 | "last 1 safari version" 44 | ] 45 | }, 46 | "dependencies": { 47 | "@emotion/react": "^11.4.1", 48 | "@emotion/styled": "^11.3.0", 49 | "@fortawesome/fontawesome-free": "^5.15.4", 50 | "@fortawesome/fontawesome-svg-core": "^1.2.36", 51 | "@fortawesome/free-solid-svg-icons": "^5.15.4", 52 | "@fortawesome/react-fontawesome": "^0.1.15", 53 | "@hookform/resolvers": "2.8.1", 54 | "loop-react-auth": "^1.0.0", 55 | "@types/jest": "^27.0.2", 56 | "@types/md5": "^2.3.1", 57 | "@types/node": "^16.10.2", 58 | "@types/react": "^17.0.26", 59 | "@types/react-dom": "^17.0.9", 60 | "axios": "^0.21.4", 61 | "axios-case-converter": "^0.8.1", 62 | "emotion": "^11.0.0", 63 | "emotion-theming": "^11.0.0", 64 | "flat": "^5.0.2", 65 | "md5": "^2.3.0", 66 | "prop-types": "^15.7.2", 67 | "query-string": "^7.0.1", 68 | "react": "^17.0.2", 69 | "react-dom": "^17.0.2", 70 | "react-hook-form": "^7.16.2", 71 | "react-intl": "^5.20.12", 72 | "react-router-dom": "^5.3.0", 73 | "react-scripts": "4.0.3", 74 | "typescript": "^4.4.3", 75 | "yup": "^0.32.9" 76 | }, 77 | "devDependencies": { 78 | "@testing-library/jest-dom": "^5.14.1", 79 | "@testing-library/react": "^12.1.1", 80 | "@types/react-intl": "^3.0.0", 81 | "@types/react-router-dom": "^5.3.0", 82 | "decamelize": "^5.0.1", 83 | "eslint-config-airbnb": "18.2.1", 84 | "eslint-config-prettier": "^6.11.0", 85 | "eslint-plugin-import": "^2.22.0", 86 | "eslint-plugin-jest-dom": "^3.9.2", 87 | "eslint-plugin-jsx-a11y": "^6.3.1", 88 | "eslint-plugin-prettier": "^3.1.4", 89 | "eslint-plugin-react": "^7.26.1", 90 | "eslint-plugin-react-hooks": "^4.1.2", 91 | "eslint-plugin-testing-library": "^4.12.4", 92 | "history": "^5.0.1", 93 | "husky": "^4.3.0", 94 | "jest-environment-jsdom-sixteen": "^2.0.0", 95 | "map-obj": "^4.3.0", 96 | "nock": "^13.1.3", 97 | "prettier": "^2.4.1", 98 | "react-axe": "^3.5.3", 99 | "source-map-explorer": "^2.5.2" 100 | }, 101 | "husky": { 102 | "hooks": { 103 | "pre-push": "yarn pre-push" 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loopstudio/react-app-boilerplate/a980c09a5c9b46238e786634cab735efea16d1e7/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | 33 | 39 | React Boilerplate 40 | 41 | 42 | 43 |
44 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loopstudio/react-app-boilerplate/a980c09a5c9b46238e786634cab735efea16d1e7/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loopstudio/react-app-boilerplate/a980c09a5c9b46238e786634cab735efea16d1e7/public/logo512.png -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React Boilerplate", 3 | "name": "LoopStudio React Boilerplate", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | -------------------------------------------------------------------------------- /src/assets/icons.js: -------------------------------------------------------------------------------- 1 | import { faBars } from '@fortawesome/free-solid-svg-icons'; 2 | 3 | const icons = [faBars]; 4 | 5 | export default icons; 6 | -------------------------------------------------------------------------------- /src/emotion.d.ts: -------------------------------------------------------------------------------- 1 | import '@emotion/react'; 2 | 3 | declare module '@emotion/react' { 4 | export interface BorderRadius { 5 | none: string; 6 | sm: string; 7 | base: string; 8 | md: string; 9 | lg: string; 10 | full: string; 11 | } 12 | 13 | export interface BoxShadow { 14 | base: string; 15 | darkMd: string; 16 | darkLg: string; 17 | darkXl: string; 18 | indigo: string; 19 | none: string; 20 | } 21 | 22 | export interface Color { 23 | black: string; 24 | blue700: string; 25 | blue800: string; 26 | gray100: string; 27 | gray200: string; 28 | gray300: string; 29 | gray600: string; 30 | gray700: string; 31 | gray800: string; 32 | green700: string; 33 | green800: string; 34 | indigo200: string; 35 | indigo400: string; 36 | indigo500: string; 37 | indigo600: string; 38 | indigo700: string; 39 | red600: string; 40 | white: string; 41 | } 42 | 43 | export interface FontSize { 44 | xs: string; 45 | sm: string; 46 | base: string; 47 | lg: string; 48 | xl: string; 49 | xxl: string; 50 | x3l: string; 51 | x4l: string; 52 | x5l: string; 53 | x6l: string; 54 | x7l: string; 55 | x8l: string; 56 | } 57 | 58 | export interface Height { 59 | header: string; 60 | } 61 | 62 | export interface LetterSpacing { 63 | tighter: string; 64 | tight: string; 65 | normal: string; 66 | wide: string; 67 | wider: string; 68 | widest: string; 69 | } 70 | 71 | export interface Theme { 72 | borderRadius: BorderRadius; 73 | boxShadow: BoxShadow; 74 | color: Color; 75 | fontSize: FontSize; 76 | height: Height; 77 | letterSpacing: LetterSpacing; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/features/app/components/App/App.js: -------------------------------------------------------------------------------- 1 | import { lazy, Suspense } from 'react'; 2 | import { IntlProvider } from 'react-intl'; 3 | import { library } from '@fortawesome/fontawesome-svg-core'; 4 | import { BrowserRouter } from 'react-router-dom'; 5 | import flatten from 'flat'; 6 | import { Global } from '@emotion/react'; 7 | import { useAuth } from 'loop-react-auth'; 8 | 9 | import icons from 'assets/icons'; 10 | import { useGuestLocale } from '../../hooks/guestLocale'; 11 | import AppLocale from '../../locales'; 12 | import ErrorBoundary from '../ErrorBoundary'; 13 | 14 | import { globalStyles, CustomLoading } from './App.styles'; 15 | 16 | const UnauthenticatedApp = lazy(() => import('../UnauthenticatedApp')); 17 | const AuthenticatedApp = lazy(() => 18 | import(/* webpackPrefetch: true */ '../AuthenticatedApp') 19 | ); 20 | 21 | library.add(icons); 22 | 23 | const App = () => { 24 | const { isLoading, isAuthenticated, user } = useAuth(); 25 | const { guestLocale } = useGuestLocale(); 26 | 27 | const locale = user?.locale || guestLocale; 28 | const appLocale = AppLocale[locale]; 29 | 30 | return ( 31 | 32 | 33 | 34 | 35 | {isLoading ? ( 36 | 37 | ) : ( 38 | }> 39 | {isAuthenticated ? : } 40 | 41 | )} 42 | 43 | 44 | 45 | ); 46 | }; 47 | 48 | export default App; 49 | -------------------------------------------------------------------------------- /src/features/app/components/App/App.styles.js: -------------------------------------------------------------------------------- 1 | import styled from '@emotion/styled'; 2 | import { css } from '@emotion/react'; 3 | 4 | import Loading from '../Loading'; 5 | 6 | export const CustomLoading = styled(Loading)` 7 | height: 100vh; 8 | `; 9 | 10 | export const globalStyles = css` 11 | html { 12 | font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', 13 | Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 14 | 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 15 | 'Noto Color Emoji'; 16 | line-height: 1.5; 17 | -webkit-text-size-adjust: 100%; 18 | } 19 | 20 | body { 21 | margin: 0; 22 | } 23 | 24 | main { 25 | display: block; 26 | } 27 | 28 | h1 { 29 | font-size: 2em; 30 | margin: 0.67em 0; 31 | } 32 | 33 | hr { 34 | box-sizing: content-box; 35 | height: 0; 36 | overflow: visible; 37 | } 38 | 39 | pre { 40 | font-family: monospace, monospace; 41 | font-size: 1em; 42 | } 43 | 44 | a { 45 | background-color: transparent; 46 | } 47 | 48 | abbr[title] { 49 | border-bottom: none; 50 | text-decoration: underline; 51 | text-decoration: underline dotted; 52 | } 53 | 54 | b, 55 | strong { 56 | font-weight: bolder; 57 | } 58 | 59 | code, 60 | kbd, 61 | samp { 62 | font-family: monospace, monospace; 63 | font-size: 1em; 64 | } 65 | 66 | small { 67 | font-size: 80%; 68 | } 69 | 70 | sub, 71 | sup { 72 | font-size: 75%; 73 | line-height: 0; 74 | position: relative; 75 | vertical-align: baseline; 76 | } 77 | 78 | sub { 79 | bottom: -0.25em; 80 | } 81 | 82 | sup { 83 | top: -0.5em; 84 | } 85 | 86 | img { 87 | border-style: none; 88 | } 89 | 90 | button, 91 | input, 92 | optgroup, 93 | select, 94 | textarea { 95 | font-family: inherit; 96 | font-size: 100%; 97 | line-height: 1.15; 98 | margin: 0; 99 | } 100 | 101 | button, 102 | input { 103 | overflow: visible; 104 | } 105 | 106 | button, 107 | select { 108 | text-transform: none; 109 | } 110 | 111 | button, 112 | [type='button'], 113 | [type='reset'], 114 | [type='submit'] { 115 | -webkit-appearance: button; 116 | } 117 | 118 | button::-moz-focus-inner, 119 | [type='button']::-moz-focus-inner, 120 | [type='reset']::-moz-focus-inner, 121 | [type='submit']::-moz-focus-inner { 122 | border-style: none; 123 | padding: 0; 124 | } 125 | 126 | button:-moz-focusring, 127 | [type='button']:-moz-focusring, 128 | [type='reset']:-moz-focusring, 129 | [type='submit']:-moz-focusring { 130 | outline: 1px dotted ButtonText; 131 | } 132 | 133 | fieldset { 134 | padding: 0.35em 0.75em 0.625em; 135 | } 136 | 137 | legend { 138 | box-sizing: border-box; 139 | color: inherit; 140 | display: table; 141 | max-width: 100%; 142 | padding: 0; 143 | white-space: normal; 144 | } 145 | 146 | progress { 147 | vertical-align: baseline; 148 | } 149 | 150 | textarea { 151 | overflow: auto; 152 | } 153 | 154 | [type='checkbox'], 155 | [type='radio'] { 156 | box-sizing: border-box; 157 | padding: 0; 158 | } 159 | 160 | [type='number']::-webkit-inner-spin-button, 161 | [type='number']::-webkit-outer-spin-button { 162 | height: auto; 163 | } 164 | 165 | [type='search'] { 166 | -webkit-appearance: textfield; 167 | outline-offset: -2px; 168 | } 169 | 170 | [type='search']::-webkit-search-decoration { 171 | -webkit-appearance: none; 172 | } 173 | 174 | ::-webkit-file-upload-button { 175 | -webkit-appearance: button; 176 | font: inherit; 177 | } 178 | 179 | details { 180 | display: block; 181 | } 182 | 183 | summary { 184 | display: list-item; 185 | } 186 | 187 | template { 188 | display: none; 189 | } 190 | 191 | [hidden] { 192 | display: none; 193 | } 194 | 195 | blockquote, 196 | dl, 197 | dd, 198 | h1, 199 | h2, 200 | h3, 201 | h4, 202 | h5, 203 | h6, 204 | hr, 205 | figure, 206 | p, 207 | pre { 208 | margin: 0; 209 | } 210 | 211 | button { 212 | background-color: transparent; 213 | background-image: none; 214 | } 215 | 216 | button:focus { 217 | outline: 1px dotted; 218 | outline: 5px auto -webkit-focus-ring-color; 219 | } 220 | 221 | fieldset { 222 | margin: 0; 223 | padding: 0; 224 | } 225 | 226 | ol, 227 | ul { 228 | list-style: none; 229 | margin: 0; 230 | padding: 0; 231 | } 232 | 233 | *, 234 | ::before, 235 | ::after { 236 | box-sizing: border-box; 237 | border-width: 0; 238 | border-style: solid; 239 | border-color: #e2e8f0; 240 | } 241 | 242 | hr { 243 | border-top-width: 1px; 244 | } 245 | 246 | img { 247 | border-style: solid; 248 | } 249 | 250 | textarea { 251 | resize: vertical; 252 | } 253 | 254 | input::placeholder, 255 | textarea::placeholder { 256 | color: #a0aec0; 257 | } 258 | 259 | button, 260 | [role='button'] { 261 | cursor: pointer; 262 | } 263 | 264 | table { 265 | border-collapse: collapse; 266 | } 267 | 268 | h1, 269 | h2, 270 | h3, 271 | h4, 272 | h5, 273 | h6 { 274 | font-size: inherit; 275 | font-weight: inherit; 276 | } 277 | 278 | a { 279 | color: inherit; 280 | text-decoration: inherit; 281 | } 282 | 283 | button, 284 | input, 285 | optgroup, 286 | select, 287 | textarea { 288 | padding: 0; 289 | line-height: inherit; 290 | color: inherit; 291 | } 292 | 293 | pre, 294 | code, 295 | kbd, 296 | samp { 297 | font-family: Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', 298 | monospace; 299 | } 300 | 301 | img, 302 | svg, 303 | video, 304 | canvas, 305 | audio, 306 | iframe, 307 | embed, 308 | object { 309 | display: block; 310 | vertical-align: middle; 311 | } 312 | 313 | img, 314 | video { 315 | max-width: 100%; 316 | height: auto; 317 | } 318 | `; 319 | -------------------------------------------------------------------------------- /src/features/app/components/App/App.test.js: -------------------------------------------------------------------------------- 1 | import { render } from 'testUtils'; 2 | import App from './App'; 3 | 4 | describe('App', () => { 5 | it('renders without crashing', () => { 6 | render(); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /src/features/app/components/App/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './App'; 2 | -------------------------------------------------------------------------------- /src/features/app/components/AuthenticatedApp/AuthenticatedApp.js: -------------------------------------------------------------------------------- 1 | import { Route, Switch } from 'react-router-dom'; 2 | 3 | import HomePage from '../../pages/HomePage'; 4 | import NoMatchPage from '../../pages/NoMatchPage'; 5 | import SettingsPage from '../../pages/SettingsPage'; 6 | 7 | const AuthenticatedApp = () => ( 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | ); 20 | 21 | export default AuthenticatedApp; 22 | -------------------------------------------------------------------------------- /src/features/app/components/AuthenticatedApp/AuthenticatedApp.test.js: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom/extend-expect'; 2 | import { screen } from '@testing-library/react'; 3 | 4 | import { renderWithRouter } from 'testUtils'; 5 | import { userData, authenticationHeaders } from 'testUtils/mocks/auth'; 6 | import AuthenticatedApp from './AuthenticatedApp'; 7 | 8 | const state = { 9 | auth: { 10 | user: userData, 11 | session: authenticationHeaders, 12 | }, 13 | }; 14 | 15 | describe('Authenticated App', () => { 16 | it('renders the Home Page', () => { 17 | renderWithRouter(, { 18 | state, 19 | history: ['/'], 20 | }); 21 | 22 | expect(screen.getByText('Hello World')).toBeInTheDocument(); 23 | }); 24 | 25 | it('renders the Settings Page', () => { 26 | renderWithRouter(, { 27 | state, 28 | history: ['/settings'], 29 | }); 30 | 31 | expect(screen.getByText('Account settings')).toBeInTheDocument(); 32 | }); 33 | 34 | it('renders the No match Page', () => { 35 | renderWithRouter(, { 36 | state, 37 | history: ['/no-match'], 38 | }); 39 | 40 | expect(screen.getByText('404')).toBeInTheDocument(); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /src/features/app/components/AuthenticatedApp/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './AuthenticatedApp'; 2 | -------------------------------------------------------------------------------- /src/features/app/components/ErrorBoundary/ErrorBoundary.js: -------------------------------------------------------------------------------- 1 | import { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { FormattedMessage } from 'react-intl'; 4 | 5 | import { 6 | Content, 7 | ErrorMessage, 8 | Legend, 9 | ReportButton, 10 | Wrapper, 11 | } from './ErrorBoundary.styles'; 12 | 13 | class ErrorBoundary extends Component { 14 | constructor(props) { 15 | super(props); 16 | this.state = { 17 | hasError: false, 18 | error: {}, 19 | }; 20 | } 21 | 22 | static getDerivedStateFromError(error) { 23 | return { hasError: true, error }; 24 | } 25 | 26 | render() { 27 | const { 28 | hasError, 29 | error: { message }, 30 | } = this.state; 31 | const { children } = this.props; 32 | const isDevelopmentMode = process.env.NODE_ENV === 'development'; 33 | 34 | if (hasError) 35 | return ( 36 | 37 | 38 | 39 | 40 | 41 | {isDevelopmentMode && ( 42 | <> 43 | {message} 44 | 49 | 50 | 51 | 52 | )} 53 | 54 | 55 | ); 56 | 57 | return children; 58 | } 59 | } 60 | 61 | ErrorBoundary.propTypes = { 62 | children: PropTypes.element.isRequired, 63 | }; 64 | 65 | export default ErrorBoundary; 66 | -------------------------------------------------------------------------------- /src/features/app/components/ErrorBoundary/ErrorBoundary.styles.js: -------------------------------------------------------------------------------- 1 | import styled from '@emotion/styled'; 2 | 3 | export const Content = styled.main` 4 | align-items: center; 5 | background-color: ${({ theme }) => theme.color.gray100}; 6 | color: ${({ theme }) => theme.color.gray800}; 7 | display: flex; 8 | flex-direction: column; 9 | justify-content: center; 10 | min-height: 100vh; 11 | padding: 1.5rem; 12 | text-align: center; 13 | `; 14 | 15 | export const Wrapper = styled.div` 16 | align-items: center; 17 | display: flex; 18 | flex-direction: column; 19 | justify-content: center; 20 | `; 21 | 22 | export const Legend = styled.p` 23 | font-size: 1.875rem; 24 | `; 25 | 26 | export const ErrorMessage = styled.p` 27 | margin: 1.25rem 0; 28 | width: 50%; 29 | text-align: left; 30 | `; 31 | 32 | export const ReportButton = styled.a` 33 | background-color: ${({ theme }) => theme.color.indigo600}; 34 | border-radius: ${({ theme }) => theme.borderRadius.full}; 35 | box-shadow: 0 0 1.5rem 0.1rem rgba(102, 126, 234, 0.6); 36 | color: ${({ theme }) => theme.color.white}; 37 | font-weight: 700; 38 | letter-spacing: ${({ theme }) => theme.letterSpacing.wide}; 39 | margin: 2.5rem 0; 40 | padding: 0.5rem 1rem; 41 | width: 10rem; 42 | `; 43 | -------------------------------------------------------------------------------- /src/features/app/components/ErrorBoundary/ErrorBoundary.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | import '@testing-library/jest-dom/extend-expect'; 3 | import { screen } from '@testing-library/react'; 4 | 5 | import { render } from 'testUtils'; 6 | import ErrorBoundary from './ErrorBoundary'; 7 | 8 | const Throw = () => { 9 | throw new Error('Dummy error'); 10 | }; 11 | 12 | describe('ErrorBoundary', () => { 13 | it('shows the detail of the error on the screen', () => { 14 | const originalEnv = process.env.NODE_ENV; 15 | process.env.NODE_ENV = 'development'; 16 | const errorBoundary = ( 17 | 18 | 19 | 20 | ); 21 | 22 | const spy = jest.spyOn(console, 'error'); 23 | spy.mockImplementation(() => {}); 24 | render(errorBoundary); 25 | console.error.mockRestore(); 26 | 27 | expect(screen.queryByText('An error occurred')).toBeInTheDocument(); 28 | expect(screen.queryByText('Dummy error')).toBeInTheDocument(); 29 | expect(screen.queryByText('Report error')).toBeInTheDocument(); 30 | 31 | process.env.NODE_ENV = originalEnv; 32 | }); 33 | 34 | it('shows a general message on the screen', () => { 35 | const errorBoundary = ( 36 | 37 | 38 | 39 | ); 40 | 41 | const spy = jest.spyOn(console, 'error'); 42 | spy.mockImplementation(() => {}); 43 | render(errorBoundary); 44 | console.error.mockRestore(); 45 | 46 | expect(screen.queryByText('An error occurred')).toBeInTheDocument(); 47 | expect(screen.queryByText('Dummy error')).not.toBeInTheDocument(); 48 | expect(screen.queryByText('Report error')).not.toBeInTheDocument(); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /src/features/app/components/ErrorBoundary/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './ErrorBoundary'; 2 | -------------------------------------------------------------------------------- /src/features/app/components/Footer/Footer.tsx: -------------------------------------------------------------------------------- 1 | import LanguageSelector from '../LanguageSelector'; 2 | 3 | const Footer = () => ( 4 |
5 | 6 |
7 | ); 8 | 9 | export default Footer; 10 | -------------------------------------------------------------------------------- /src/features/app/components/Footer/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './Footer'; 2 | -------------------------------------------------------------------------------- /src/features/app/components/Form/Button.tsx: -------------------------------------------------------------------------------- 1 | import { ButtonHTMLAttributes, forwardRef, Ref } from 'react'; 2 | import { MessageFormatElement } from 'react-intl'; 3 | import { SerializedStyles } from '@emotion/react'; 4 | 5 | import { Button, CustomLoading } from './Form.styles'; 6 | 7 | interface FormButtonProps extends ButtonHTMLAttributes { 8 | isDisabled?: boolean; 9 | isLoading?: boolean; 10 | text: string | MessageFormatElement[]; 11 | styles?: SerializedStyles; 12 | } 13 | 14 | const FormButton = forwardRef( 15 | ( 16 | { isDisabled, isLoading, text, ...rest }: FormButtonProps, 17 | ref: Ref 18 | ) => ( 19 | 27 | ) 28 | ); 29 | 30 | export default FormButton; 31 | -------------------------------------------------------------------------------- /src/features/app/components/Form/Form.styles.ts: -------------------------------------------------------------------------------- 1 | import styled from '@emotion/styled'; 2 | import Loading from '../Loading'; 3 | 4 | interface InputContainerProps { 5 | hasError: boolean; 6 | } 7 | 8 | interface StylesProps { 9 | styles?: any; 10 | } 11 | 12 | export const Button = styled.button` 13 | background-color: ${(props) => props.theme.color.indigo600}; 14 | border-radius: ${(props) => props.theme.borderRadius.base}; 15 | box-shadow: ${(props) => props.theme.boxShadow.base}; 16 | color: ${(props) => props.theme.color.white}; 17 | font-weight: 500; 18 | letter-spacing: ${(props) => props.theme.letterSpacing.wide}; 19 | margin-bottom: 1.5rem; 20 | margin-top: 2.5rem; 21 | padding-right: 1rem; 22 | padding-left: 1rem; 23 | padding-top: 0.5rem; 24 | padding-bottom: 0.5rem; 25 | text-transform: uppercase; 26 | width: 100%; 27 | 28 | &:hover { 29 | background-color: ${(props) => props.theme.color.indigo700}; 30 | } 31 | 32 | &:disabled { 33 | background-color: ${(props) => props.theme.color.indigo200}; 34 | box-shadow: ${(props) => props.theme.boxShadow.none}; 35 | } 36 | `; 37 | 38 | export const Error = styled.span` 39 | color: ${(props) => props.theme.color.red600}; 40 | display: block; 41 | font-size: ${(props) => props.theme.fontSize.sm}; 42 | max-width: fit-content; 43 | `; 44 | 45 | export const FormContent = styled.form` 46 | ${(props) => props.styles}; 47 | `; 48 | 49 | export const Input = styled.input` 50 | background-color: ${(props) => props.theme.color.gray100}; 51 | border-radius: ${(props) => props.theme.borderRadius.base}; 52 | border-width: 1px; 53 | color: ${(props) => props.theme.color.gray700}; 54 | font-size: ${(props) => props.theme.fontSize.sm}; 55 | height: 2.5rem; 56 | line-height: 1.25; 57 | max-width: 100%; 58 | padding-left: 0.75rem; 59 | padding-right: 0.75rem; 60 | width: 16rem; 61 | 62 | &:focus { 63 | border-color: ${(props) => props.theme.color.gray300}; 64 | outline: 0; 65 | } 66 | `; 67 | 68 | export const InputContainer = styled.div` 69 | margin-bottom: 0.75rem; 70 | margin-top: ${({ hasError }) => hasError && '0.25rem'}; 71 | 72 | > input { 73 | border-color: ${({ hasError, theme }) => 74 | hasError ? theme.color.red600 : theme.color.gray300}; 75 | 76 | &:focus { 77 | outline: ${({ hasError }) => hasError && '0'}; 78 | border-color: ${({ hasError, theme }) => hasError && theme.color.red600}; 79 | } 80 | } 81 | `; 82 | 83 | export const Label = styled.label` 84 | color: ${(props) => props.theme.color.gray700}; 85 | display: block; 86 | font-size: ${(props) => props.theme.fontSize.sm}; 87 | font-weight: 400; 88 | `; 89 | 90 | export const LabelContent = styled.div` 91 | display: flex; 92 | justify-content: space-between; 93 | `; 94 | 95 | export const CustomLoading = styled(Loading)` 96 | margin-top: -0.5rem; 97 | margin-bottom: -0.5rem; 98 | `; 99 | 100 | export const Select = styled.select` 101 | background-color: ${(props) => props.theme.color.gray100}; 102 | border-radius: ${(props) => props.theme.borderRadius.base}; 103 | border-width: 1px; 104 | color: ${(props) => props.theme.color.gray700}; 105 | height: 2.5rem; 106 | line-height: 1.25; 107 | max-width: 100%; 108 | padding-left: 0.75rem; 109 | padding-right: 0.75rem; 110 | width: 16rem; 111 | 112 | &:focus { 113 | border-color: ${(props) => props.theme.color.gray300}; 114 | outline: 0; 115 | } 116 | `; 117 | 118 | export const Span = styled.span` 119 | display: block; 120 | margin-bottom: 0.5rem; 121 | `; 122 | -------------------------------------------------------------------------------- /src/features/app/components/Form/Form.tsx: -------------------------------------------------------------------------------- 1 | import { FormProvider } from 'react-hook-form'; 2 | import FormButton from './Button'; 3 | import FormInput from './Input'; 4 | import FormSelect from './Select'; 5 | 6 | import { Error, FormContent } from './Form.styles'; 7 | 8 | interface FormProps { 9 | children: React.ReactNode; 10 | onSubmit: Promise; 11 | formMethods: any; 12 | } 13 | 14 | const Form = ({ children, formMethods, onSubmit, ...props }: FormProps) => { 15 | const { 16 | formState: { errors }, 17 | handleSubmit, 18 | } = formMethods; 19 | 20 | return ( 21 | 22 | 23 | {children} 24 | 25 | {errors?.general && {errors.general.message}} 26 | 27 | ); 28 | }; 29 | 30 | Form.displayName = 'CustomForm'; 31 | Form.Button = FormButton; 32 | Form.Input = FormInput; 33 | Form.Select = FormSelect; 34 | 35 | export default Form; 36 | -------------------------------------------------------------------------------- /src/features/app/components/Form/Input.tsx: -------------------------------------------------------------------------------- 1 | import { InputHTMLAttributes } from 'react'; 2 | import { useFormContext } from 'react-hook-form'; 3 | 4 | import InputLabel from './InputLabel'; 5 | 6 | import { Error, Input, InputContainer } from './Form.styles'; 7 | 8 | interface FormInputProps extends InputHTMLAttributes { 9 | id: string; 10 | name: string; 11 | label?: string; 12 | } 13 | 14 | const FormInput = ({ id, name, label, ...rest }: FormInputProps) => { 15 | const { 16 | register, 17 | formState: { errors }, 18 | } = useFormContext(); 19 | const error = errors[name]; 20 | 21 | return ( 22 | 23 | 24 | 25 | {error?.message} 26 | 27 | ); 28 | }; 29 | 30 | export default FormInput; 31 | -------------------------------------------------------------------------------- /src/features/app/components/Form/InputLabel.tsx: -------------------------------------------------------------------------------- 1 | import { LabelHTMLAttributes } from 'react'; 2 | import { FormattedMessage } from 'react-intl'; 3 | 4 | import { Label, LabelContent, Span } from './Form.styles'; 5 | 6 | interface InputLabelProps extends LabelHTMLAttributes { 7 | htmlFor: string; 8 | name: string; 9 | label?: string; 10 | } 11 | 12 | const InputLabel = ({ htmlFor, name, label, ...rest }: InputLabelProps) => ( 13 | 14 | 17 | 18 | ); 19 | 20 | export default InputLabel; 21 | -------------------------------------------------------------------------------- /src/features/app/components/Form/Select.tsx: -------------------------------------------------------------------------------- 1 | import { SelectHTMLAttributes } from 'react'; 2 | import { useIntl } from 'react-intl'; 3 | import { useFormContext } from 'react-hook-form'; 4 | 5 | import InputLabel from './InputLabel'; 6 | 7 | import { Error, InputContainer, Select } from './Form.styles'; 8 | 9 | interface Option { 10 | label: string; 11 | value: string; 12 | } 13 | 14 | interface FormSelectProps extends SelectHTMLAttributes { 15 | id: string; 16 | name: string; 17 | label?: string; 18 | options: Array