├── .circleci └── config.yml ├── .editorconfig ├── .env ├── .env.example ├── .eslintrc.js ├── .gitattributes ├── .github └── workflows │ └── main.yml ├── .gitignore ├── .gitlab-ci.yml ├── .nvmrc ├── .prettierignore ├── .prettierrc ├── .stylelintrc ├── .travis.yml ├── .whitesource ├── LICENSE ├── README.md ├── _config.yml ├── bin ├── cli.js └── copy.js ├── index.html ├── package.json ├── public ├── favicon.ico └── icon-512x512.png ├── readme.svg ├── renovate.json ├── src ├── app.tsx ├── assets │ ├── images │ │ └── react.svg │ ├── locale │ │ ├── de.json │ │ ├── de.po │ │ ├── translations.json │ │ └── translations.pot │ └── styles │ │ ├── app.scss │ │ ├── functions.scss │ │ ├── mixins.scss │ │ └── settings.scss ├── components │ ├── button │ │ ├── __snapshots__ │ │ │ └── index.test.tsx.snap │ │ ├── index.scss │ │ ├── index.test.tsx │ │ └── index.tsx │ ├── field │ │ ├── __snapshots__ │ │ │ └── index.test.tsx.snap │ │ ├── index.test.tsx │ │ └── index.tsx │ ├── footer │ │ ├── __snapshots__ │ │ │ └── index.test.tsx.snap │ │ ├── index.scss │ │ ├── index.test.tsx │ │ └── index.tsx │ ├── header │ │ ├── __snapshots__ │ │ │ └── index.test.tsx.snap │ │ ├── index.scss │ │ ├── index.test.tsx │ │ └── index.tsx │ ├── icon │ │ ├── __snapshots__ │ │ │ └── index.test.tsx.snap │ │ ├── index.scss │ │ ├── index.test.tsx │ │ └── index.tsx │ ├── index.ts │ ├── login-form │ │ ├── __snapshots__ │ │ │ └── index.test.tsx.snap │ │ ├── index.test.tsx │ │ └── index.tsx │ ├── password-reset-form │ │ ├── __snapshots__ │ │ │ └── index.test.tsx.snap │ │ ├── index.test.tsx │ │ └── index.tsx │ ├── signup-form │ │ ├── __snapshots__ │ │ │ └── index.test.tsx.snap │ │ ├── index.test.tsx │ │ └── index.tsx │ ├── test-store-provider │ │ └── index.tsx │ └── wrapper │ │ ├── __snapshots__ │ │ └── index.test.tsx.snap │ │ ├── index.scss │ │ ├── index.test.tsx │ │ └── index.tsx ├── containers │ ├── .boilerplate │ │ ├── __snapshots__ │ │ │ └── index.test.tsx.snap │ │ ├── index.scss │ │ ├── index.test.tsx │ │ └── index.tsx │ ├── auth │ │ ├── index.ts │ │ ├── login │ │ │ ├── __snapshots__ │ │ │ │ └── index.test.tsx.snap │ │ │ ├── index.test.tsx │ │ │ └── index.tsx │ │ ├── password-reset │ │ │ ├── __snapshots__ │ │ │ │ └── index.test.tsx.snap │ │ │ ├── index.test.tsx │ │ │ └── index.tsx │ │ └── signup │ │ │ ├── __snapshots__ │ │ │ └── index.test.tsx.snap │ │ │ ├── index.test.tsx │ │ │ └── index.tsx │ ├── home │ │ ├── __snapshots__ │ │ │ └── index.test.tsx.snap │ │ ├── index.scss │ │ ├── index.test.tsx │ │ └── index.tsx │ └── not-found │ │ ├── __snapshots__ │ │ └── index.test.tsx.snap │ │ ├── index.scss │ │ ├── index.test.tsx │ │ └── index.tsx ├── custom.d.ts ├── i18n │ ├── gettext-converter.mjs │ ├── index.ts │ ├── locales.mjs │ └── scanner-config.js ├── index.tsx ├── loadables.tsx ├── store │ ├── branches │ │ ├── .boilerplate │ │ │ ├── enums.ts │ │ │ ├── interfaces.ts │ │ │ ├── reducer.ts │ │ │ └── sagas.ts │ │ └── auth │ │ │ ├── enums.ts │ │ │ ├── interfaces.ts │ │ │ ├── reducer.ts │ │ │ └── sagas.ts │ ├── enums.ts │ ├── index.ts │ ├── interfaces.ts │ ├── root-reducer.ts │ ├── sagas.ts │ └── selectors.ts └── utilities │ ├── api.ts │ ├── constants.ts │ ├── enums.ts │ ├── helpers.ts │ ├── hooks.ts │ ├── index.ts │ ├── interfaces.ts │ └── local-storage.ts ├── test-config ├── DateMock.js ├── FileMock.js ├── StyleMock.js ├── index.js ├── jest.config.js └── tsconfig.json ├── tsconfig.json ├── vite.config.ts ├── workbox-config.js └── yarn.lock /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | jobs: 3 | build: 4 | docker: 5 | - image: circleci/node:lts 6 | steps: 7 | - checkout 8 | - run: 9 | name: autoreconf 10 | command: sudo apt install automake 11 | - run: 12 | name: install 13 | command: yarn 14 | - run: 15 | name: lint 16 | command: yarn lint 17 | - run: 18 | name: test:coverage 19 | command: yarn test:coverage 20 | - run: 21 | name: build 22 | command: yarn build 23 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | indent_style = tab 8 | indent_size = 4 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | 12 | [{*.json,*.yml}] 13 | indent_style = space 14 | indent_size = 2 15 | 16 | [*.md] 17 | trim_trailing_whitespace = false 18 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | # ENV Variable must be prefixed with VITE_ in order to be accessible 2 | # for reference https://vitejs.dev/guide/env-and-mode.html#env-files 3 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # ENV Variable must be prefixed with VITE_ in order to be accessible 2 | # for reference https://vitejs.dev/guide/env-and-mode.html#env-files 3 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | node: true, 5 | 'jest/globals': true 6 | }, 7 | extends: ['prettier', 'plugin:react/recommended', 'plugin:@typescript-eslint/recommended'], 8 | parser: '@typescript-eslint/parser', 9 | parserOptions: { 10 | project: './tsconfig.json', 11 | sourceType: 'module', 12 | tsconfigRootDir: __dirname 13 | }, 14 | plugins: ['jest', '@typescript-eslint'], 15 | ignorePatterns: ['bin/*', '*.js', '*.mjs'], 16 | rules: { 17 | 'react/display-name': 'off', 18 | '@typescript-eslint/no-explicit-any': 'off' 19 | }, 20 | settings: { 21 | react: { 22 | version: 'detect' 23 | } 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | ## GITATTRIBUTES FOR WEB PROJECTS 2 | # 3 | # These settings are for any web project. 4 | # 5 | # Details per file setting: 6 | # text These files should be normalized (i.e. convert CRLF to LF). 7 | # binary These files are binary and should be left untouched. 8 | # 9 | # Note that binary is a macro for -text -diff. 10 | ###################################################################### 11 | 12 | ## AUTO-DETECT 13 | ## Handle line endings automatically for files detected as 14 | ## text and leave all files detected as binary untouched. 15 | ## This will handle all files NOT defined below. 16 | * text=auto 17 | 18 | ## SOURCE CODE 19 | *.bat text eol=crlf 20 | *.coffee text 21 | *.css text 22 | *.htm text 23 | *.html text 24 | *.inc text 25 | *.ini text 26 | *.js text 27 | *.json text 28 | *.jsx text 29 | *.less text 30 | *.od text 31 | *.onlydata text 32 | *.php text 33 | *.pl text 34 | *.py text 35 | *.rb text 36 | *.sass text 37 | *.scm text 38 | *.scss text 39 | *.sh text eol=lf 40 | *.sql text 41 | *.styl text 42 | *.tag text 43 | *.ts text 44 | *.tsx text 45 | *.xml text 46 | *.xhtml text 47 | 48 | ## DOCKER 49 | *.dockerignore text 50 | Dockerfile text 51 | 52 | ## DOCUMENTATION 53 | *.markdown text 54 | *.md text 55 | *.mdwn text 56 | *.mdown text 57 | *.mkd text 58 | *.mkdn text 59 | *.mdtxt text 60 | *.mdtext text 61 | *.txt text 62 | AUTHORS text 63 | CHANGELOG text 64 | CHANGES text 65 | CONTRIBUTING text 66 | COPYING text 67 | copyright text 68 | *COPYRIGHT* text 69 | INSTALL text 70 | license text 71 | LICENSE text 72 | NEWS text 73 | readme text 74 | *README* text 75 | TODO text 76 | 77 | ## TEMPLATES 78 | *.dot text 79 | *.ejs text 80 | *.haml text 81 | *.handlebars text 82 | *.hbs text 83 | *.hbt text 84 | *.jade text 85 | *.latte text 86 | *.mustache text 87 | *.njk text 88 | *.phtml text 89 | *.tmpl text 90 | *.tpl text 91 | *.twig text 92 | 93 | ## LINTERS 94 | .csslintrc text 95 | .eslintrc text 96 | .htmlhintrc text 97 | .jscsrc text 98 | .jshintrc text 99 | .jshintignore text 100 | .stylelintrc text 101 | 102 | ## CONFIGS 103 | *.bowerrc text 104 | *.cnf text 105 | *.conf text 106 | *.config text 107 | .browserslistrc text 108 | .editorconfig text 109 | .gitattributes text 110 | .gitconfig text 111 | .htaccess text 112 | *.npmignore text 113 | *.yaml text 114 | *.yml text 115 | browserslist text 116 | Makefile text 117 | makefile text 118 | 119 | ## HEROKU 120 | Procfile text 121 | .slugignore text 122 | 123 | ## GRAPHICS 124 | *.ai binary 125 | *.bmp binary 126 | *.eps binary 127 | *.gif binary 128 | *.ico binary 129 | *.jng binary 130 | *.jp2 binary 131 | *.jpg binary 132 | *.jpeg binary 133 | *.jpx binary 134 | *.jxr binary 135 | *.pdf binary 136 | *.png binary 137 | *.psb binary 138 | *.psd binary 139 | *.svg text 140 | *.svgz binary 141 | *.tif binary 142 | *.tiff binary 143 | *.wbmp binary 144 | *.webp binary 145 | 146 | ## AUDIO 147 | *.kar binary 148 | *.m4a binary 149 | *.mid binary 150 | *.midi binary 151 | *.mp3 binary 152 | *.ogg binary 153 | *.ra binary 154 | 155 | ## VIDEO 156 | *.3gpp binary 157 | *.3gp binary 158 | *.as binary 159 | *.asf binary 160 | *.asx binary 161 | *.fla binary 162 | *.flv binary 163 | *.m4v binary 164 | *.mng binary 165 | *.mov binary 166 | *.mp4 binary 167 | *.mpeg binary 168 | *.mpg binary 169 | *.ogv binary 170 | *.swc binary 171 | *.swf binary 172 | *.webm binary 173 | 174 | ## ARCHIVES 175 | *.7z binary 176 | *.gz binary 177 | *.jar binary 178 | *.rar binary 179 | *.tar binary 180 | *.zip binary 181 | 182 | ## FONTS 183 | *.ttf binary 184 | *.eot binary 185 | *.otf binary 186 | *.woff binary 187 | *.woff2 binary 188 | 189 | ## EXECUTABLES 190 | *.exe binary 191 | *.pyc binary 192 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Continuous Integration 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v4 10 | - name: Use Node.js LTS 11 | uses: actions/setup-node@v4 12 | with: 13 | node-version: lts/* 14 | - run: yarn 15 | - run: yarn lint 16 | - run: yarn test:coverage 17 | - run: yarn build 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Dependency directory 9 | node_modules/ 10 | 11 | # Misc 12 | .DS_Store 13 | .DS_Store? 14 | ._* 15 | .Spotlight-V100 16 | .Trashes 17 | ehthumbs.db 18 | Thumbs.db 19 | .env 20 | 21 | # Build 22 | dist/ 23 | 24 | # Coverage 25 | coverage/ 26 | 27 | # Project specific 28 | .vscode/ 29 | -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | image: node:lts 2 | before_script: 3 | - yarn 4 | cache: 5 | paths: 6 | - node_modules/ 7 | build: 8 | script: 9 | - yarn lint 10 | - yarn test:coverage 11 | - yarn build 12 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | lts/* 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "tabWidth": 4, 4 | "useTabs": true, 5 | "semi": true, 6 | "singleQuote": true, 7 | "jsxSingleQuote": false, 8 | "trailingComma": "none", 9 | "bracketSpacing": true, 10 | "jsxBracketSameLine": false, 11 | "arrowParens": "avoid", 12 | "proseWrap": "preserve" 13 | } 14 | -------------------------------------------------------------------------------- /.stylelintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["stylelint-config-recommended", "stylelint-config-standard-scss"], 3 | "plugins": ["stylelint-scss", "stylelint-no-unsupported-browser-features"], 4 | "rules": {} 5 | } 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | sudo: false 3 | node_js: 4 | - 'lts/*' 5 | install: 6 | - yarn 7 | script: 8 | - yarn lint 9 | - yarn test:coverage 10 | - yarn build 11 | -------------------------------------------------------------------------------- /.whitesource: -------------------------------------------------------------------------------- 1 | { 2 | "checkRunSettings": { 3 | "vulnerableCheckRunConclusionLevel": "success" 4 | }, 5 | "issueSettings": { 6 | "minSeverityLevel": "LOW" 7 | } 8 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018-Present Three 11 LTD 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 |
Zero config and fast installation: Run `npx create-react-app-ts && yarn && yarn start` in your terminal.
2 |

3 | 4 | [![GitHub issues](https://img.shields.io/github/issues/three11/react-template-ts.svg)](https://github.com/three11/react-template-ts/issues) 5 | [![GitHub last commit](https://img.shields.io/github/last-commit/three11/react-template-ts.svg)](https://github.com/three11/react-template-ts/commits/master) 6 | [![Build Status](https://travis-ci.org/three11/react-template-ts.svg?branch=master)](https://travis-ci.org/three11/react-template-ts) 7 | [![Analytics](https://ga-beacon.appspot.com/UA-83446952-1/github.com/three11/react-template-ts/README.md)](https://github.com/three11/react-template-ts) 8 | 9 | # Create Awesome React Application 10 | 11 | > Opinionated React starter template using TypeScript, Redux, React Router, Redux Saga, SCSS, PostCSS and more, offering PWA and offline capabilities and many more. 12 | 13 | ## Dependencies 14 | 15 | In order to use this setup you need to have installed the following dependencies: 16 | 17 | 1. Node - min v8.15.0 18 | 2. NPM - min v5.6.0 19 | or 20 | 3. Yarn - min v1.3.2 21 | 4. Bash terminal (Default on OSX/Linux, GitBash or similar on Windows) 22 | 23 | ## One line zero config installation 24 | 25 | ```sh 26 | npx create-react-app-ts && yarn && yarn start 27 | ``` 28 | 29 | **Just a quick note:** You should manually create a `.gitignore` file if you plan on keeping your project in Git. 30 | 31 | ## Download 32 | 33 | You can download this setup [directly](https://github.com/three11/react-template-ts/archive/master.zip) and extract it. 34 | 35 | Then navigate to the `react-template-ts` folder and proceed with the rest of the instructions. 36 | 37 | ## Install 38 | 39 | ```sh 40 | yarn 41 | 42 | # or 43 | 44 | npm i 45 | ``` 46 | 47 | ## Develop 48 | 49 | ```sh 50 | yarn start 51 | 52 | # or 53 | 54 | npm start 55 | ``` 56 | 57 | ## Build 58 | 59 | ```sh 60 | yarn build 61 | 62 | # or 63 | 64 | npm run build 65 | ``` 66 | 67 | ## Lint 68 | 69 | ```sh 70 | yarn lint 71 | 72 | # or 73 | 74 | npm run lint 75 | ``` 76 | 77 | ## Test 78 | 79 | ```sh 80 | yarn test 81 | 82 | # or 83 | 84 | npm run test 85 | ``` 86 | 87 | ## Details 88 | 89 | 1. Folder structure: 90 | 91 | ``` 92 | 📦 project 93 | ┣ 📂 assets - all fonts, images, videos, translation files, etc 94 | ┣ ┣ 📂 locale - all translations 95 | ┣ ┣ 📂 styles - all shared stylesheets 96 | ┃ ┃ ┗ 📜 app.scss - Application's global SCSS entry point 97 | ┃ ┃ ┗ 📜 mixins.scss - Application's SCSS mixins 98 | ┃ ┃ ┗ 📜 functions.scss - Application's SCSS functions 99 | ┃ ┃ ┗ 📜 settings.scss - Application's SCSS settings (variables, etc) 100 | ┣ 📂 components - stateless components 101 | ┣ 📂 containers - statefull components. Each container can export more than one component. An example folder structure is included in (`src/containers/.boilerplate`) 102 | ┣ 📂 i18n - configuration settings for i18n (internationalization) 103 | ┣ 📂 store - The application Redux store 104 | ┣ ┣ 📂 branches - all store branches 105 | ┣ ┣ ┣ ┣ 📂 $BRANCH - A branch in the Redux store 106 | ┃ ┃ ┃ ┗ 📜 enums.ts - Each branch has its own enums 107 | ┃ ┃ ┃ ┗ 📜 interfaces.ts - Each branch has its own interfaces 108 | ┃ ┃ ┃ ┗ 📜 reducer.ts - The branch reducer 109 | ┃ ┃ ┃ ┗ 📜 selectors.ts - The branch selectors (hooks) 110 | ┃ ┃ ┃ ┗ 📜 sagas.ts - The branch sagas 111 | ┃ ┗ 📜 enums.ts - Store's enums 112 | ┃ ┗ 📜 index.ts - Application's main store 113 | ┃ ┗ 📜 interfaces.ts - Store's interfaces 114 | ┃ ┗ 📜 root-reducer.ts - Application's root reducer 115 | ┃ ┗ 📜 sagas.ts - Application's sagas 116 | ┣ 📂 utilities - helpers and utility functions 117 | ┗ 📜 app.tsx - Application's main component 118 | ┗ 📜 custom.d.ts - Custom type definitions 119 | ┗ 📜 index.html - Application's HTML file 120 | ┗ 📜 index.tsx - The main entry point 121 | ┗ 📜 loadables.tsx - Code split and lazy loaded components 122 | ``` 123 | 124 | 2. Latest EcmaScript support 125 | 126 | - Usage of the latest features in EcmaScript 127 | - Using [TypeScript](https://www.typescriptlang.org/) to transpile to ES5 128 | - Minification of the bundled file 129 | - Source maps 130 | 131 | 3. Aliases: Checkout the aliases property in the `vite.config.ts` and `tsconfig.json` files. 132 | 4. SCSS usage. 133 | 5. Lint your files: ESLint (with TypeScript ESLint installed and configured) and Stylelint included 134 | 6. Tests using Jest and React testing library. The Test environment has been configured so you don't have to 135 | 7. PWA ready - Install as a native app on Android and iOS 136 | 8. Code splitting and lazy loading 137 | 9. i18n included: 138 | 1. add your locales in `/src/i18n/locales` 139 | 2. add your po files which are based on the `translations.pot` file located in `/src/assets/locale` 140 | 3. run `yarn locale` to generate `${locale}.json` file from your `${locale}.po` file. 141 | 4. update your UI to reflect the newly added locale 142 | 10. Prerendering - All pages are prerendered based on defined routes. This is included in the build step and needs **no additional configuration**. 143 | 144 | ## Supported Browsers 145 | 146 | This setup uses [Browserslist](https://github.com/browserslist/browserslist) to target browsers. 147 | 148 | The default list of supported browsers is listed in the `package.json` file: 149 | 150 | ```json 151 | { 152 | "browserslist": ["> 1%", "last 2 versions"] 153 | } 154 | ``` 155 | 156 | This means that supported browsers vary based on current usage data and current browser versions. 157 | 158 | In general, this setup supports the two most recent versions of all browsers. 159 | 160 | ## Bonus 161 | 162 | The start template contains a ready-to-use auth flow with Login, Logout, Sign up and Forgotten password forms with validation included. The auth flow includes also route guarding and redirects based on auth status. Please take a look at the `/src/containers/auth` folder for more details. 163 | 164 | The starting files also include ready-to-use layout components such as `Header`, `Footer`, `Wrapper`, `Button`, `Icon` and form `Field`s. 165 | 166 | ## LICENSE 167 | 168 | MIT 169 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-slate -------------------------------------------------------------------------------- /bin/cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * Node dependencies 5 | */ 6 | const { join } = require('path'); 7 | 8 | /** 9 | * Internal dependencies 10 | */ 11 | const { copyDir } = require('./copy'); 12 | 13 | const shouldSkip = name => name === 'node_modules' || name === 'bin' || name === '.github'; 14 | 15 | copyDir(join(__dirname, '../'), process.env.PWD, shouldSkip); 16 | 17 | console.log('Your awesome React App is now setup! Run "npm i" or "yarn" to continue'); 18 | -------------------------------------------------------------------------------- /bin/copy.js: -------------------------------------------------------------------------------- 1 | const { join } = require('path'); 2 | 3 | const { 4 | lstatSync, 5 | mkdirSync, 6 | symlinkSync, 7 | readdirSync, 8 | readlinkSync, 9 | createReadStream, 10 | createWriteStream 11 | } = require('fs'); 12 | 13 | const mkdir = dir => { 14 | try { 15 | // @ts-ignore 16 | mkdirSync(dir, 0755); 17 | } catch (e) { 18 | if (e.code !== 'EEXIST') { 19 | throw e; 20 | } 21 | } 22 | }; 23 | 24 | const copy = (src, dest) => { 25 | const from = createReadStream(src); 26 | const to = createWriteStream(dest); 27 | 28 | from.pipe(to); 29 | }; 30 | 31 | const copyDir = (src, dest, filter) => { 32 | mkdir(dest); 33 | 34 | const files = readdirSync(src); 35 | 36 | for (const file of files) { 37 | const from = join(src, file); 38 | const to = join(dest, file); 39 | const current = lstatSync(from); 40 | 41 | if (typeof filter === 'function' && filter(file)) { 42 | continue; 43 | } 44 | 45 | if (current.isDirectory()) { 46 | copyDir(from, to); 47 | } else if (current.isSymbolicLink()) { 48 | const symlink = readlinkSync(from); 49 | 50 | symlinkSync(symlink, to); 51 | } else { 52 | copy(from, to); 53 | } 54 | } 55 | }; 56 | 57 | module.exports = { mkdir, copy, copyDir }; 58 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | React Template 13 | 14 | 15 |
16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "create-react-app-ts", 3 | "version": "2.1.0", 4 | "description": "Scalable starter boilerplate for React applications", 5 | "main": "./src/index.tsx", 6 | "bin": { 7 | "create-react-app-ts": "./bin/cli.js" 8 | }, 9 | "scripts": { 10 | "svg": "svgo-viewbox -i ./src/assets", 11 | "start": "vite --open", 12 | "build": "vite build", 13 | "tsc": "tsc --noEmit --skipLibCheck", 14 | "lint": "yarn lint:ts && yarn lint:scss", 15 | "lint:ts": "eslint 'src/**/*.{ts,tsx}'", 16 | "lint:scss": "stylelint './src/**/*.scss' --config .stylelintrc", 17 | "test": "jest --config=./test-config/jest.config.js --runInBand --coverage --env jsdom", 18 | "test:coverage": "yarn test --coverage", 19 | "locale:scan": "i18next-scanner --config ./src/i18n/scanner-config.js './src/**/*.{ts,tsx}'", 20 | "locale:pot": "node ./src/i18n/gettext-converter.mjs jsonToPot", 21 | "locale:po": "node ./src/i18n/gettext-converter.mjs poToJson", 22 | "locale": "yarn locale:scan && yarn locale:pot && yarn locale:po", 23 | "run-dist": "yarn build && npx ecstatic ./dist --root=./dist --host=localhost --port=8080 --baseDir=/" 24 | }, 25 | "repository": { 26 | "type": "git", 27 | "url": "git@github.com:three11/react-template.git" 28 | }, 29 | "keywords": [ 30 | "React", 31 | "Starter", 32 | "Template", 33 | "SPA", 34 | "JavaScript" 35 | ], 36 | "authors": [ 37 | { 38 | "name": "Three 11 Ltd", 39 | "email": "hello@three-11.com", 40 | "role": "Developer" 41 | }, 42 | { 43 | "name": "Alexander Panayotov", 44 | "email": "alexander.panayotov@gmail.com", 45 | "role": "Developer" 46 | }, 47 | { 48 | "name": "Atanas Atanasov", 49 | "email": "scriptex.bg@gmail.com", 50 | "role": "Developer" 51 | } 52 | ], 53 | "license": "MIT", 54 | "bugs": { 55 | "url": "https://github.com/three11/react-template/issues" 56 | }, 57 | "homepage": "https://github.com/three11/react-template#readme", 58 | "dependencies": { 59 | "@loadable/component": "5.16.4", 60 | "@redux-devtools/extension": "3.3.0", 61 | "axios": "1.7.9", 62 | "date-fns": "3.6.0", 63 | "i18next": "23.16.8", 64 | "i18next-browser-languagedetector": "7.2.2", 65 | "normalize.css": "8.0.1", 66 | "react": "18.3.1", 67 | "react-dom": "18.3.1", 68 | "react-hook-form": "7.54.0", 69 | "react-i18next": "14.1.3", 70 | "react-inlinesvg": "4.1.5", 71 | "react-redux": "9.1.2", 72 | "react-router": "6.28.0", 73 | "react-router-dom": "6.28.0", 74 | "redux": "4.2.1", 75 | "redux-saga": "1.3.0", 76 | "scss-goodies": "2.2.0" 77 | }, 78 | "devDependencies": { 79 | "@rollup/plugin-alias": "5.1.1", 80 | "@testing-library/jest-dom": "6.6.3", 81 | "@testing-library/react": "14.3.1", 82 | "@types/jest": "29.5.14", 83 | "@types/loadable__component": "5.13.9", 84 | "@types/node": "20.17.9", 85 | "@types/postcss-flexbugs-fixes": "5.0.3", 86 | "@types/react": "18.3.14", 87 | "@types/react-dom": "18.3.3", 88 | "@types/react-loadable": "5.5.11", 89 | "@types/react-redux": "7.1.34", 90 | "@types/react-router": "5.1.20", 91 | "@types/react-router-dom": "5.3.3", 92 | "@types/redux-mock-store": "1.5.0", 93 | "@typescript-eslint/eslint-plugin": "6.21.0", 94 | "@typescript-eslint/parser": "6.21.0", 95 | "@vitejs/plugin-react": "4.3.4", 96 | "autoprefixer": "10.4.20", 97 | "cssnano": "6.1.2", 98 | "eslint": "8.57.1", 99 | "eslint-config-prettier": "9.1.0", 100 | "eslint-plugin-jest": "28.9.0", 101 | "eslint-plugin-react": "7.37.2", 102 | "i18next-conv": "14.1.0", 103 | "i18next-scanner": "4.6.0", 104 | "jest": "29.7.0", 105 | "jest-environment-jsdom": "29.7.0", 106 | "jest-localstorage-mock": "2.4.26", 107 | "postcss": "8.4.49", 108 | "postcss-flexbugs-fixes": "5.0.2", 109 | "redux-mock-store": "1.5.5", 110 | "sass": "1.82.0", 111 | "stylelint": "16.11.0", 112 | "stylelint-config-recommended": "14.0.1", 113 | "stylelint-config-standard-scss": "12.0.0", 114 | "stylelint-no-unsupported-browser-features": "8.0.2", 115 | "stylelint-scss": "6.10.0", 116 | "svgo": "3.3.2", 117 | "svgo-add-viewbox": "3.1.0", 118 | "svgo-viewbox": "3.0.0", 119 | "ts-jest": "29.2.5", 120 | "ts-node": "10.9.2", 121 | "tslib": "2.8.1", 122 | "typescript": "5.7.2", 123 | "vite": "5.4.11", 124 | "vite-plugin-prerender": "1.0.8", 125 | "vite-plugin-pwa": "0.21.1", 126 | "workbox-build": "7.3.0", 127 | "workbox-window": "7.3.0" 128 | }, 129 | "browserslist": [ 130 | "> 1%", 131 | "last 2 versions" 132 | ] 133 | } 134 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/three11/react-template-ts/67de957307295c4d877f47b2cf427ce78282f8a0/public/favicon.ico -------------------------------------------------------------------------------- /public/icon-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/three11/react-template-ts/67de957307295c4d877f47b2cf427ce78282f8a0/public/icon-512x512.png -------------------------------------------------------------------------------- /readme.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 52 | 53 |
54 |
55 |

$ npx create-react-app-ts && yarn && yarn start

56 |
57 |
58 |
59 |
60 |
61 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["config:base", ":automergePatch", ":automergeMinor", ":automergeBranch", ":disableDependencyDashboard"], 3 | "travis": { 4 | "enabled": true 5 | }, 6 | "assignees": ["@scriptex", "@alpanayotov"], 7 | "labels": ["dependencies"], 8 | "rebaseWhen": "conflicted", 9 | "vulnerabilityAlerts": { 10 | "labels": ["security"], 11 | "assignees": ["@scriptex", "@alpanayotov"] 12 | }, 13 | "major": { 14 | "automerge": false 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/app.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Routes, Route } from 'react-router-dom'; 3 | 4 | import * as Loadables from './loadables'; 5 | import { Routes as AppRoutes, isLoggedIn } from '@utilities'; 6 | 7 | import './assets/styles/app.scss'; 8 | 9 | export const App = () => ( 10 | 11 | } /> 12 | {!isLoggedIn() ? } /> : null} 13 | {!isLoggedIn() ? } /> : null} 14 | {!isLoggedIn() ? } /> : null} 15 | } /> 16 | 17 | ); 18 | -------------------------------------------------------------------------------- /src/assets/images/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/locale/de.json: -------------------------------------------------------------------------------- 1 | { 2 | "All rights reserved.": "Alle Rechte vorbehalten.", 3 | "About": "Über", 4 | "Settings": "Einstellungen", 5 | "Logout": "Abmeldung", 6 | "This field is required.": "Dieses Feld muss ausgefüllt werden.", 7 | "Invalid email address.": "Ungültige E-Mail-Adresse.", 8 | "Email Address": "E-Mail-Adresse", 9 | "The password should contain least 8 characters.": "Das Passwort sollte mindestens 8 Zeichen enthalten.", 10 | "Password": "Passwort", 11 | "Login": "Anmeldung", 12 | "New password": "Neues Passwort", 13 | "The passwords do not match": "Die Passwörter stimmen nicht überein", 14 | "Confirm new password": "Neues Passwort bestätigen", 15 | "Reset password": "Passwort zurücksetzen", 16 | "Confirm password": "Passwort bestätigen", 17 | "Username": "Benutzername", 18 | "First name": "Vorname", 19 | "Last name": "Nachname", 20 | "Unspecified": "Nicht spezifiziert", 21 | "Male": "Mann", 22 | "Female": "Frau", 23 | "Gender": "Geschlecht", 24 | "Avatar image URL": "URL des Avatar-Bildes", 25 | "Sign up": "Anmelden", 26 | "Don't have an account?": "Sie haben kein Konto?", 27 | "Forgot password?": "Haben Sie Ihr Passwort vergessen?", 28 | "You can also": "Sie können auch", 29 | "or": "oder", 30 | "Already have an account?": "Haben Sie bereits ein Konto?", 31 | "Homepage": "Startseite", 32 | "Page not found": "Seite nicht gefunden" 33 | } -------------------------------------------------------------------------------- /src/assets/locale/de.po: -------------------------------------------------------------------------------- 1 | msgid "" 2 | msgstr "" 3 | "Project-Id-Version: i18next-conv\n" 4 | "mime-version: 1.0\n" 5 | "Content-Type: text/plain; charset=UTF-8\n" 6 | "Content-Transfer-Encoding: 8bit\n" 7 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 8 | "POT-Creation-Date: 2020-09-10T12:35:09.604Z\n" 9 | "PO-Revision-Date: 2020-09-10 15:37+0300\n" 10 | "Language-Team: \n" 11 | "MIME-Version: 1.0\n" 12 | "X-Generator: Poedit 2.4.1\n" 13 | "Last-Translator: \n" 14 | "Language: en\n" 15 | 16 | msgid "All rights reserved." 17 | msgstr "Alle Rechte vorbehalten." 18 | 19 | msgid "About" 20 | msgstr "Über" 21 | 22 | msgid "Settings" 23 | msgstr "Einstellungen" 24 | 25 | msgid "Logout" 26 | msgstr "Abmeldung" 27 | 28 | msgid "This field is required." 29 | msgstr "Dieses Feld muss ausgefüllt werden." 30 | 31 | msgid "Invalid email address." 32 | msgstr "Ungültige E-Mail-Adresse." 33 | 34 | msgid "Email Address" 35 | msgstr "E-Mail-Adresse" 36 | 37 | msgid "The password should contain least 8 characters." 38 | msgstr "Das Passwort sollte mindestens 8 Zeichen enthalten." 39 | 40 | msgid "Password" 41 | msgstr "Passwort" 42 | 43 | msgid "Login" 44 | msgstr "Anmeldung" 45 | 46 | msgid "New password" 47 | msgstr "Neues Passwort" 48 | 49 | msgid "The passwords do not match" 50 | msgstr "Die Passwörter stimmen nicht überein" 51 | 52 | msgid "Confirm new password" 53 | msgstr "Neues Passwort bestätigen" 54 | 55 | msgid "Reset password" 56 | msgstr "Passwort zurücksetzen" 57 | 58 | msgid "Confirm password" 59 | msgstr "Passwort bestätigen" 60 | 61 | msgid "Username" 62 | msgstr "Benutzername" 63 | 64 | msgid "First name" 65 | msgstr "Vorname" 66 | 67 | msgid "Last name" 68 | msgstr "Nachname" 69 | 70 | msgid "Unspecified" 71 | msgstr "Nicht spezifiziert" 72 | 73 | msgid "Male" 74 | msgstr "Mann" 75 | 76 | msgid "Female" 77 | msgstr "Frau" 78 | 79 | msgid "Gender" 80 | msgstr "Geschlecht" 81 | 82 | msgid "Avatar image URL" 83 | msgstr "URL des Avatar-Bildes" 84 | 85 | msgid "Sign up" 86 | msgstr "Anmelden" 87 | 88 | msgid "Don't have an account?" 89 | msgstr "Sie haben kein Konto?" 90 | 91 | msgid "Forgot password?" 92 | msgstr "Haben Sie Ihr Passwort vergessen?" 93 | 94 | msgid "You can also" 95 | msgstr "Sie können auch" 96 | 97 | msgid "or" 98 | msgstr "oder" 99 | 100 | msgid "Already have an account?" 101 | msgstr "Haben Sie bereits ein Konto?" 102 | 103 | msgid "Homepage" 104 | msgstr "Startseite" 105 | 106 | msgid "Page not found" 107 | msgstr "Seite nicht gefunden" 108 | -------------------------------------------------------------------------------- /src/assets/locale/translations.json: -------------------------------------------------------------------------------- 1 | { 2 | "All rights reserved.": "All rights reserved.", 3 | "About": "About", 4 | "Settings": "Settings", 5 | "Logout": "Logout", 6 | "This field is required.": "This field is required.", 7 | "Invalid email address.": "Invalid email address.", 8 | "Email Address": "Email Address", 9 | "The password should contain least 8 characters.": "The password should contain least 8 characters.", 10 | "Password": "Password", 11 | "Login": "Login", 12 | "New password": "New password", 13 | "The passwords do not match": "The passwords do not match", 14 | "Confirm new password": "Confirm new password", 15 | "Reset password": "Reset password", 16 | "Confirm password": "Confirm password", 17 | "Username": "Username", 18 | "First name": "First name", 19 | "Last name": "Last name", 20 | "Unspecified": "Unspecified", 21 | "Male": "Male", 22 | "Female": "Female", 23 | "Gender": "Gender", 24 | "Avatar image URL": "Avatar image URL", 25 | "Sign up": "Sign up", 26 | "Don't have an account?": "Don't have an account?", 27 | "Forgot password?": "Forgot password?", 28 | "You can also": "You can also", 29 | "or": "or", 30 | "Already have an account?": "Already have an account?", 31 | "Homepage": "Homepage", 32 | "Page not found": "Page not found" 33 | } 34 | -------------------------------------------------------------------------------- /src/assets/locale/translations.pot: -------------------------------------------------------------------------------- 1 | msgid "" 2 | msgstr "" 3 | "Project-Id-Version: i18next-conv\n" 4 | "mime-version: 1.0\n" 5 | "Content-Type: text/plain; charset=utf-8\n" 6 | "Content-Transfer-Encoding: 8bit\n" 7 | "Plural-Forms: nplurals=2; plural=(n != 1)\n" 8 | "POT-Creation-Date: 2022-08-26T09:41:59.560Z\n" 9 | "PO-Revision-Date: 2022-08-26T09:41:59.560Z\n" 10 | 11 | msgid "All rights reserved." 12 | msgstr "All rights reserved." 13 | 14 | msgid "About" 15 | msgstr "About" 16 | 17 | msgid "Settings" 18 | msgstr "Settings" 19 | 20 | msgid "Logout" 21 | msgstr "Logout" 22 | 23 | msgid "This field is required." 24 | msgstr "This field is required." 25 | 26 | msgid "Invalid email address." 27 | msgstr "Invalid email address." 28 | 29 | msgid "Email Address" 30 | msgstr "Email Address" 31 | 32 | msgid "The password should contain least 8 characters." 33 | msgstr "The password should contain least 8 characters." 34 | 35 | msgid "Password" 36 | msgstr "Password" 37 | 38 | msgid "Login" 39 | msgstr "Login" 40 | 41 | msgid "New password" 42 | msgstr "New password" 43 | 44 | msgid "The passwords do not match" 45 | msgstr "The passwords do not match" 46 | 47 | msgid "Confirm new password" 48 | msgstr "Confirm new password" 49 | 50 | msgid "Reset password" 51 | msgstr "Reset password" 52 | 53 | msgid "Confirm password" 54 | msgstr "Confirm password" 55 | 56 | msgid "Username" 57 | msgstr "Username" 58 | 59 | msgid "First name" 60 | msgstr "First name" 61 | 62 | msgid "Last name" 63 | msgstr "Last name" 64 | 65 | msgid "Unspecified" 66 | msgstr "Unspecified" 67 | 68 | msgid "Male" 69 | msgstr "Male" 70 | 71 | msgid "Female" 72 | msgstr "Female" 73 | 74 | msgid "Gender" 75 | msgstr "Gender" 76 | 77 | msgid "Avatar image URL" 78 | msgstr "Avatar image URL" 79 | 80 | msgid "Sign up" 81 | msgstr "Sign up" 82 | 83 | msgid "Don't have an account?" 84 | msgstr "Don't have an account?" 85 | 86 | msgid "Forgot password?" 87 | msgstr "Forgot password?" 88 | 89 | msgid "You can also" 90 | msgstr "You can also" 91 | 92 | msgid "or" 93 | msgstr "or" 94 | 95 | msgid "Already have an account?" 96 | msgstr "Already have an account?" 97 | 98 | msgid "Homepage" 99 | msgstr "Homepage" 100 | 101 | msgid "Page not found" 102 | msgstr "Page not found" 103 | -------------------------------------------------------------------------------- /src/assets/styles/app.scss: -------------------------------------------------------------------------------- 1 | @import 'normalize.css'; 2 | 3 | * { 4 | box-sizing: border-box; 5 | 6 | &::before, 7 | &::after { 8 | box-sizing: inherit; 9 | } 10 | } 11 | 12 | html, 13 | body, 14 | #app, 15 | .o-wrapper { 16 | height: 100%; 17 | } 18 | 19 | body { 20 | font-family: 'Open Sans', Helvetica, Tahoma, Arial, sans-serif; 21 | font-size: 1rem; 22 | line-height: 1.25; 23 | color: $color-base; 24 | background: #fff; 25 | } 26 | 27 | a { 28 | color: inherit; 29 | text-decoration: none; 30 | transition: all $timing $easing; 31 | } 32 | 33 | img { 34 | border: 0; 35 | display: inline-block; 36 | vertical-align: top; 37 | } 38 | -------------------------------------------------------------------------------- /src/assets/styles/functions.scss: -------------------------------------------------------------------------------- 1 | @function str-split($string, $separator) { 2 | // empty array/list 3 | $result: (); 4 | 5 | // first index of separator in string 6 | $index: string.index($string, $separator); 7 | 8 | // loop through string 9 | @while $index != null { 10 | // get the substring from the first character to the separator 11 | $item: string.slice($string, 1, $index - 1); 12 | 13 | // push item to array 14 | $result: string.append($result, $item); 15 | 16 | // remove item and separator from string 17 | $string: string.slice($string, $index + 1); 18 | 19 | // find new index of separator 20 | $index: string.index($string, $separator); 21 | } 22 | 23 | // add the remaining string to list (the last item) 24 | $result: string.append($result, $string); 25 | 26 | @return $result; 27 | } 28 | -------------------------------------------------------------------------------- /src/assets/styles/mixins.scss: -------------------------------------------------------------------------------- 1 | @mixin flex($align, $justify) { 2 | display: flex; 3 | align-items: $align; 4 | justify-content: $justify; 5 | } 6 | 7 | @mixin flex-row($wrap: wrap, $align: stretch, $justify: space-between) { 8 | @include flex($align, $justify); 9 | 10 | flex-flow: row $wrap; 11 | } 12 | 13 | @mixin flex-column($wrap: nowrap, $align: stretch, $justify: space-between) { 14 | @include flex($align, $justify); 15 | 16 | flex-flow: column $wrap; 17 | } 18 | 19 | @mixin small-desktop { 20 | @media (max-width: #{$small-desktop}) { 21 | @content; 22 | } 23 | } 24 | 25 | @mixin desktop-only { 26 | @media (calc($tablet-landscape + 1) <= width) { 27 | @content; 28 | } 29 | } 30 | 31 | @mixin tablet-landscape { 32 | @media (max-width: #{$tablet-landscape}) { 33 | @content; 34 | } 35 | } 36 | 37 | @mixin tablet-landscape-only { 38 | @media (calc($tablet-portrait + 1) <= width <= $tablet-landscape) { 39 | @content; 40 | } 41 | } 42 | 43 | @mixin tablet-portrait { 44 | @media (max-width: #{$tablet-portrait}) { 45 | @content; 46 | } 47 | } 48 | 49 | @mixin tablet-portrait-only { 50 | @media (calc($mobile + 1) <= width <= $tablet-portrait) { 51 | @content; 52 | } 53 | } 54 | 55 | @mixin mobile { 56 | @media (max-width: #{$mobile}) { 57 | @content; 58 | } 59 | } 60 | 61 | @mixin hover { 62 | &:hover { 63 | @media (-ms-high-contrast: none), (-ms-high-contrast: active), (-moz-touch-enabled: 0), (hover) { 64 | @content; 65 | } 66 | } 67 | } 68 | 69 | @mixin centered { 70 | position: absolute; 71 | inset: 0; 72 | margin: auto; 73 | } 74 | 75 | @mixin transition($properties, $will-change: true, $timing: $timing, $easing: $easing) { 76 | $transition: ''; 77 | 78 | @each $property in str-split($properties, ',') { 79 | $single: $property + ' ' + $timing + ' ' + $easing + ','; 80 | $transition: $transition + $single; 81 | } 82 | 83 | transition: $transition; 84 | 85 | @if $will-change == true and $properties != 'all' { 86 | will-change: string.unquote($properties); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/assets/styles/settings.scss: -------------------------------------------------------------------------------- 1 | @import 'mixins'; 2 | 3 | $color-base: #333; 4 | $color-white: #fff; 5 | $color-black: #000; 6 | $color-error: #f00; 7 | $color-action: #00f; 8 | $color-warning: #ff0; 9 | $timing: 0.4s; 10 | $easing: ease-in-out; 11 | $small-desktop: 1439px; 12 | $tablet-landscape: 1279px; 13 | $tablet-portrait: 1023px; 14 | $mobile: 767px; 15 | -------------------------------------------------------------------------------- /src/components/button/__snapshots__/index.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`should render successfully 1`] = ` 4 | 5 | 19 | 24 |

25 | 26 | 27 |
28 | `; 29 | -------------------------------------------------------------------------------- /src/components/footer/index.scss: -------------------------------------------------------------------------------- 1 | .c-footer { 2 | text-align: right; 3 | width: 100%; 4 | padding: 1rem 0; 5 | 6 | button { 7 | margin-left: 0.5rem; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/components/footer/index.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { render } from '@testing-library/react'; 3 | 4 | import { Footer } from '.'; 5 | 6 | test('Footer component should render successfully', () => { 7 | const { asFragment } = render(