├── .editorconfig ├── .env.production ├── .eslintrc.json ├── .github ├── dependabot.yml └── workflows │ └── ci.yml ├── .gitignore ├── .husky └── pre-commit ├── .jest ├── match-media-mock.js └── setup.ts ├── .prettierignore ├── .prettierrc ├── .storybook ├── main.js └── preview.js ├── .vscode └── settings.json ├── README.md ├── cypress.json ├── cypress ├── .eslintrc.json ├── e2e │ └── example.ts ├── fixtures │ ├── example.json │ ├── profile.json │ └── users.json ├── plugins │ └── index.js ├── support │ ├── commands.ts │ ├── index.d.ts │ └── index.ts └── tsconfig.json ├── generators ├── plopfile.js └── templates │ ├── Component.tsx.hbs │ ├── stories.tsx.hbs │ ├── styles.ts.hbs │ └── test.tsx.hbs ├── jest.config.js ├── next-env.d.ts ├── next.config.js ├── package.json ├── public ├── assets │ ├── 403.svg │ ├── 404.svg │ ├── 500.svg │ ├── bg.jpg │ └── vercel.svg ├── favicon.ico ├── fonts │ ├── montserrat-v15-latin-300.woff2 │ ├── montserrat-v15-latin-600.woff2 │ └── montserrat-v15-latin-regular.woff2 ├── img │ └── bg.png ├── manifest.json ├── sw.js └── workbox-4bf1368b.js ├── src ├── @types │ ├── environment.d.ts │ └── styled-components.d.ts ├── components │ └── Main │ │ ├── Main.spec.tsx │ │ ├── Main.stories.tsx │ │ ├── Main.tsx │ │ ├── __snapshots__ │ │ └── Main.spec.tsx.snap │ │ ├── index.ts │ │ └── styles.ts ├── constants │ ├── environment-variables.ts │ ├── is-running-on-server.ts │ └── times.ts ├── errors │ ├── BadRequestError.ts │ ├── BaseError.ts │ ├── InternalError.ts │ └── NotFoundError.ts ├── functions │ ├── check-is-numeric │ │ ├── index.spec.ts │ │ └── index.ts │ ├── check-object-is-empty │ │ ├── index.spec.ts │ │ └── index.ts │ ├── check-user-authenticated │ │ ├── index.spec.ts │ │ └── index.ts │ └── index.ts ├── middlewares │ ├── api.middleware.ts │ ├── cache.middleware.ts │ ├── errorHandler.middleware.ts │ └── logger.middleware.ts ├── pages │ ├── _app.tsx │ ├── _document.tsx │ ├── about.tsx │ ├── api │ │ └── hello.ts │ └── index.tsx ├── services │ └── api.ts ├── styles │ ├── global.ts │ └── theme.ts ├── templates │ └── Dashboard │ │ ├── index.tsx │ │ └── styles.ts └── utils │ ├── delay │ └── index.ts │ ├── index.ts │ ├── localStorage │ ├── index.spec.ts │ └── index.ts │ └── tests │ ├── __config__ │ └── helpers │ │ └── index.tsx │ └── __mocks__ │ ├── generateArray │ └── index.ts │ ├── generateRandomDate │ └── index.ts │ ├── generateRandomNumberFromInterval │ └── index.ts │ └── index.ts ├── tsconfig.json ├── tsconfig.tsbuildinfo └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true -------------------------------------------------------------------------------- /.env.production: -------------------------------------------------------------------------------- 1 | NODE_ENV="production" 2 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true, 5 | "jest": true, 6 | "node": true 7 | }, 8 | "settings": { 9 | "react": { 10 | "version": "detect" 11 | } 12 | }, 13 | "extends": [ 14 | "eslint:recommended", 15 | "plugin:react/recommended", 16 | "plugin:@typescript-eslint/recommended", 17 | "plugin:prettier/recommended" 18 | ], 19 | "parser": "@typescript-eslint/parser", 20 | "parserOptions": { 21 | "ecmaFeatures": { 22 | "jsx": true 23 | }, 24 | "ecmaVersion": 12, 25 | "sourceType": "module" 26 | }, 27 | "plugins": ["react", "@typescript-eslint", "react-hooks", "import-helpers"], 28 | "rules": { 29 | "react-hooks/rules-of-hooks": "error", 30 | "react-hooks/exhaustive-deps": "warn", 31 | "react/prop-types": "off", 32 | "react/react-in-jsx-scope": "off", // para trabalhar com NextJS apenas 33 | "@typescript-eslint/explicit-module-boundary-types": "off", 34 | "@typescript-eslint/no-non-null-assertion": "off", 35 | "import-helpers/order-imports": [ 36 | "warn", 37 | { 38 | "newlinesBetween": "always", // Cria uma nova linha para separar as importacoes 39 | "groups": [ 40 | ["/^react/", "/^next/"], 41 | "module", 42 | "/^@shared/", 43 | "absolute", 44 | "/^components/", 45 | "/^pages/", 46 | "/utils/", 47 | "/constants/", 48 | "/^store/", 49 | "/^styles/", 50 | "/^templates/", 51 | ["parent", "sibling", "index"] 52 | ], 53 | "alphabetize": { "order": "asc", "ignoreCase": true } 54 | } 55 | ] 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: "05:00" 8 | open-pull-requests-limit: 10 9 | ignore: 10 | - dependency-name: "@types/node" 11 | versions: 12 | - 15.0.0 13 | - dependency-name: "@storybook/addon-essentials" 14 | versions: 15 | - 6.1.17 16 | - 6.2.3 17 | - 6.2.7 18 | - 6.2.8 19 | - dependency-name: "@storybook/react" 20 | versions: 21 | - 6.1.17 22 | - 6.2.2 23 | - 6.2.3 24 | - 6.2.4 25 | - 6.2.7 26 | - dependency-name: styled-components 27 | versions: 28 | - 5.2.2 29 | - dependency-name: "@babel/core" 30 | versions: 31 | - 7.13.13 32 | - dependency-name: "@typescript-eslint/parser" 33 | versions: 34 | - 4.17.0 35 | - dependency-name: husky 36 | versions: 37 | - 5.0.9 38 | - 5.1.2 39 | - dependency-name: eslint 40 | versions: 41 | - 7.20.0 42 | - dependency-name: next 43 | versions: 44 | - 10.0.6 45 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | on: [pull_request] 3 | 4 | jobs: 5 | build: 6 | runs-on: ubuntu-latest 7 | steps: 8 | - name: Checkout Repository 9 | uses: actions/checkout@v2 10 | 11 | - name: Setup Node 12 | uses: actions/setup-node@v1 13 | with: 14 | node-version: 18.x 15 | 16 | - uses: actions/cache@v2 17 | id: yarn-cache 18 | with: 19 | path: | 20 | ~/cache 21 | !~/cache/exclude 22 | **/node_modules 23 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} 24 | restore-keys: | 25 | ${{ runner.os }}-yarn- 26 | 27 | - name: Install dependencies 28 | run: yarn install 29 | 30 | - name: Linting 31 | run: yarn lint 32 | 33 | - name: Test 34 | run: yarn test 35 | 36 | - name: Build 37 | run: yarn build 38 | -------------------------------------------------------------------------------- /.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 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env 29 | .env.local 30 | .env.development.local 31 | .env.test.local 32 | .env.production.local 33 | 34 | # vercel 35 | .vercel 36 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx --no-install lint-staged 5 | -------------------------------------------------------------------------------- /.jest/match-media-mock.js: -------------------------------------------------------------------------------- 1 | Object.defineProperty(window, 'matchMedia', { 2 | writable: true, 3 | value: jest.fn().mockImplementation((query) => ({ 4 | matches: false, 5 | media: query, 6 | onchange: null, 7 | addListener: jest.fn(), 8 | removeListener: jest.fn(), 9 | addEventListener: jest.fn(), 10 | removeEventListener: jest.fn(), 11 | dispatchEvent: jest.fn() 12 | })) 13 | }) 14 | -------------------------------------------------------------------------------- /.jest/setup.ts: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom'; 2 | import 'jest-styled-components'; 3 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | dist -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "none", 3 | "semi": true, 4 | "singleQuote": true 5 | } 6 | -------------------------------------------------------------------------------- /.storybook/main.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | stories: ['../src/components/**/*.stories.tsx'], 3 | addons: ['@storybook/addon-essentials', 'storybook-addon-next-router', 'storybook-addon-next'], 4 | features: { 5 | postcss: false, 6 | }, 7 | framework: '@storybook/react', 8 | webpackFinal: (config) => { 9 | config.resolve.modules.push(`${process.cwd()}/src`) 10 | return config 11 | }, 12 | core: { 13 | builder: 'webpack5', 14 | }, 15 | } 16 | -------------------------------------------------------------------------------- /.storybook/preview.js: -------------------------------------------------------------------------------- 1 | import { RouterContext } from "next/dist/shared/lib/router-context"; 2 | 3 | import { ThemeProvider } from 'styled-components'; 4 | import GlobalStyles from '../src/styles/global'; 5 | import theme from '../src/styles/theme'; 6 | 7 | export const decorators = [ 8 | (Story) => ( 9 | 10 | 11 | 12 | 13 | ) 14 | ] 15 | 16 | export const parameters = { 17 | actions: { argTypesRegex: "^on[A-Z].*" }, 18 | layout:'fullscreen', 19 | nextRouter: { 20 | Provider: RouterContext.Provider, 21 | }, 22 | backgrounds: { 23 | default: 'white', 24 | values: [ 25 | { 26 | name: 'white', 27 | value: '#fff', 28 | }, 29 | { 30 | name: 'dark', 31 | value: '#343a40', 32 | }, 33 | ], 34 | }, 35 | } 36 | 37 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": false, 3 | "editor.codeActionsOnSave": { 4 | "source.fixAll.eslint": true, 5 | "source.fixAll.tslint": true, 6 | "source.fixAll.stylelint": true 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | A TypeScript starter for Next.js that includes all you need to build amazing projects 🔥 2 | 3 | **With Next.js 13 & React 18!** 4 | 5 | - 📏 **ESLint** — Pluggable JavaScript linter 6 | - 💖 **Prettier** - Opinionated Code Formatter 7 | - 🐶 **Husky** — Use git hooks with ease 8 | - 🚫 **lint-staged** - Run linters against staged git files 9 | - 🐙 **React Testing Library (RTL)** - Builds by adding APIs for working with React components 10 | - 🃏 **Jest** - A delightful JavaScript Testing Framework with a focus on simplicity 11 | - 🧑‍🔬 **Cypress** - Fast, easy and reliable testing for anything that runs in a browser. 12 | - 💅 **Styled Components (with SSR)** - Use the best bits of ES6 and CSS to style your apps without stress 13 | 14 | ### Other libs 15 | - **Axios** 16 | - **Polished** 17 | - **React Hook Form** 18 | - **Yup** 19 | 20 | 21 | ## 🚀 Getting started 22 | 23 | The best way to start with this template is using `create-next-app`. 24 | 25 | ``` 26 | npx create-next-app project-name -e https://github.com/jjunior96/next-template 27 | ``` 28 | 29 | If you prefer you can clone this repository and run the following commands inside the project folder: 30 | 31 | 1. `npm install` or `yarn`; 32 | 2. `yarn dev`; 33 | 34 | To view the project open `http://localhost:3000`. 35 | 36 | ## 🤝 Contributing 37 | 38 | 1. Fork this repository; 39 | 2. Create your branch: `git checkout -b my-new-feature`; 40 | 3. Commit your changes: `git commit -m 'Add some feature'`; 41 | 4. Push to the branch: `git push origin my-new-feature`. 42 | 43 | **After your pull request is merged**, you can safely delete your branch. 44 | --- 45 | 46 | Made with ♥ by Junior Alves 47 | -------------------------------------------------------------------------------- /cypress.json: -------------------------------------------------------------------------------- 1 | { 2 | "video": false, 3 | "integrationFolder": "cypress/e2e" 4 | } 5 | -------------------------------------------------------------------------------- /cypress/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "extends": [ 4 | "plugin:cypress/recommended" 5 | ], 6 | "env": { "cypress/globals": true } 7 | } 8 | -------------------------------------------------------------------------------- /cypress/e2e/example.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | describe('Cypress Ts', () => { 4 | it('should to visit Google', () => { 5 | cy.google(); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /cypress/fixtures/example.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Using fixtures to represent data", 3 | "email": "hello@cypress.io", 4 | "body": "Fixtures are a great way to mock data for responses to routes" 5 | } 6 | -------------------------------------------------------------------------------- /cypress/fixtures/profile.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 8739, 3 | "name": "Jane", 4 | "email": "jane@example.com" 5 | } -------------------------------------------------------------------------------- /cypress/fixtures/users.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": 1, 4 | "name": "Leanne Graham", 5 | "username": "Bret", 6 | "email": "Sincere@april.biz", 7 | "address": { 8 | "street": "Kulas Light", 9 | "suite": "Apt. 556", 10 | "city": "Gwenborough", 11 | "zipcode": "92998-3874", 12 | "geo": { 13 | "lat": "-37.3159", 14 | "lng": "81.1496" 15 | } 16 | }, 17 | "phone": "1-770-736-8031 x56442", 18 | "website": "hildegard.org", 19 | "company": { 20 | "name": "Romaguera-Crona", 21 | "catchPhrase": "Multi-layered client-server neural-net", 22 | "bs": "harness real-time e-markets" 23 | } 24 | }, 25 | { 26 | "id": 2, 27 | "name": "Ervin Howell", 28 | "username": "Antonette", 29 | "email": "Shanna@melissa.tv", 30 | "address": { 31 | "street": "Victor Plains", 32 | "suite": "Suite 879", 33 | "city": "Wisokyburgh", 34 | "zipcode": "90566-7771", 35 | "geo": { 36 | "lat": "-43.9509", 37 | "lng": "-34.4618" 38 | } 39 | }, 40 | "phone": "010-692-6593 x09125", 41 | "website": "anastasia.net", 42 | "company": { 43 | "name": "Deckow-Crist", 44 | "catchPhrase": "Proactive didactic contingency", 45 | "bs": "synergize scalable supply-chains" 46 | } 47 | }, 48 | { 49 | "id": 3, 50 | "name": "Clementine Bauch", 51 | "username": "Samantha", 52 | "email": "Nathan@yesenia.net", 53 | "address": { 54 | "street": "Douglas Extension", 55 | "suite": "Suite 847", 56 | "city": "McKenziehaven", 57 | "zipcode": "59590-4157", 58 | "geo": { 59 | "lat": "-68.6102", 60 | "lng": "-47.0653" 61 | } 62 | }, 63 | "phone": "1-463-123-4447", 64 | "website": "ramiro.info", 65 | "company": { 66 | "name": "Romaguera-Jacobson", 67 | "catchPhrase": "Face to face bifurcated interface", 68 | "bs": "e-enable strategic applications" 69 | } 70 | }, 71 | { 72 | "id": 4, 73 | "name": "Patricia Lebsack", 74 | "username": "Karianne", 75 | "email": "Julianne.OConner@kory.org", 76 | "address": { 77 | "street": "Hoeger Mall", 78 | "suite": "Apt. 692", 79 | "city": "South Elvis", 80 | "zipcode": "53919-4257", 81 | "geo": { 82 | "lat": "29.4572", 83 | "lng": "-164.2990" 84 | } 85 | }, 86 | "phone": "493-170-9623 x156", 87 | "website": "kale.biz", 88 | "company": { 89 | "name": "Robel-Corkery", 90 | "catchPhrase": "Multi-tiered zero tolerance productivity", 91 | "bs": "transition cutting-edge web services" 92 | } 93 | }, 94 | { 95 | "id": 5, 96 | "name": "Chelsey Dietrich", 97 | "username": "Kamren", 98 | "email": "Lucio_Hettinger@annie.ca", 99 | "address": { 100 | "street": "Skiles Walks", 101 | "suite": "Suite 351", 102 | "city": "Roscoeview", 103 | "zipcode": "33263", 104 | "geo": { 105 | "lat": "-31.8129", 106 | "lng": "62.5342" 107 | } 108 | }, 109 | "phone": "(254)954-1289", 110 | "website": "demarco.info", 111 | "company": { 112 | "name": "Keebler LLC", 113 | "catchPhrase": "User-centric fault-tolerant solution", 114 | "bs": "revolutionize end-to-end systems" 115 | } 116 | }, 117 | { 118 | "id": 6, 119 | "name": "Mrs. Dennis Schulist", 120 | "username": "Leopoldo_Corkery", 121 | "email": "Karley_Dach@jasper.info", 122 | "address": { 123 | "street": "Norberto Crossing", 124 | "suite": "Apt. 950", 125 | "city": "South Christy", 126 | "zipcode": "23505-1337", 127 | "geo": { 128 | "lat": "-71.4197", 129 | "lng": "71.7478" 130 | } 131 | }, 132 | "phone": "1-477-935-8478 x6430", 133 | "website": "ola.org", 134 | "company": { 135 | "name": "Considine-Lockman", 136 | "catchPhrase": "Synchronised bottom-line interface", 137 | "bs": "e-enable innovative applications" 138 | } 139 | }, 140 | { 141 | "id": 7, 142 | "name": "Kurtis Weissnat", 143 | "username": "Elwyn.Skiles", 144 | "email": "Telly.Hoeger@billy.biz", 145 | "address": { 146 | "street": "Rex Trail", 147 | "suite": "Suite 280", 148 | "city": "Howemouth", 149 | "zipcode": "58804-1099", 150 | "geo": { 151 | "lat": "24.8918", 152 | "lng": "21.8984" 153 | } 154 | }, 155 | "phone": "210.067.6132", 156 | "website": "elvis.io", 157 | "company": { 158 | "name": "Johns Group", 159 | "catchPhrase": "Configurable multimedia task-force", 160 | "bs": "generate enterprise e-tailers" 161 | } 162 | }, 163 | { 164 | "id": 8, 165 | "name": "Nicholas Runolfsdottir V", 166 | "username": "Maxime_Nienow", 167 | "email": "Sherwood@rosamond.me", 168 | "address": { 169 | "street": "Ellsworth Summit", 170 | "suite": "Suite 729", 171 | "city": "Aliyaview", 172 | "zipcode": "45169", 173 | "geo": { 174 | "lat": "-14.3990", 175 | "lng": "-120.7677" 176 | } 177 | }, 178 | "phone": "586.493.6943 x140", 179 | "website": "jacynthe.com", 180 | "company": { 181 | "name": "Abernathy Group", 182 | "catchPhrase": "Implemented secondary concept", 183 | "bs": "e-enable extensible e-tailers" 184 | } 185 | }, 186 | { 187 | "id": 9, 188 | "name": "Glenna Reichert", 189 | "username": "Delphine", 190 | "email": "Chaim_McDermott@dana.io", 191 | "address": { 192 | "street": "Dayna Park", 193 | "suite": "Suite 449", 194 | "city": "Bartholomebury", 195 | "zipcode": "76495-3109", 196 | "geo": { 197 | "lat": "24.6463", 198 | "lng": "-168.8889" 199 | } 200 | }, 201 | "phone": "(775)976-6794 x41206", 202 | "website": "conrad.com", 203 | "company": { 204 | "name": "Yost and Sons", 205 | "catchPhrase": "Switchable contextually-based project", 206 | "bs": "aggregate real-time technologies" 207 | } 208 | }, 209 | { 210 | "id": 10, 211 | "name": "Clementina DuBuque", 212 | "username": "Moriah.Stanton", 213 | "email": "Rey.Padberg@karina.biz", 214 | "address": { 215 | "street": "Kattie Turnpike", 216 | "suite": "Suite 198", 217 | "city": "Lebsackbury", 218 | "zipcode": "31428-2261", 219 | "geo": { 220 | "lat": "-38.2386", 221 | "lng": "57.2232" 222 | } 223 | }, 224 | "phone": "024-648-3804", 225 | "website": "ambrose.net", 226 | "company": { 227 | "name": "Hoeger LLC", 228 | "catchPhrase": "Centralized empowering task-force", 229 | "bs": "target end-to-end models" 230 | } 231 | } 232 | ] -------------------------------------------------------------------------------- /cypress/plugins/index.js: -------------------------------------------------------------------------------- 1 | /// 2 | // *********************************************************** 3 | // This example plugins/index.js can be used to load plugins 4 | // 5 | // You can change the location of this file or turn off loading 6 | // the plugins file with the 'pluginsFile' configuration option. 7 | // 8 | // You can read more here: 9 | // https://on.cypress.io/plugins-guide 10 | // *********************************************************** 11 | 12 | // This function is called when a project is opened or re-opened (e.g. due to 13 | // the project's config changing) 14 | 15 | /** 16 | * @type {Cypress.PluginConfig} 17 | */ 18 | // eslint-disable-next-line no-unused-vars 19 | module.exports = (on, config) => { 20 | // `on` is used to hook into various events Cypress emits 21 | // `config` is the resolved Cypress config 22 | } 23 | -------------------------------------------------------------------------------- /cypress/support/commands.ts: -------------------------------------------------------------------------------- 1 | // *********************************************** 2 | // This example commands.js shows you how to 3 | // create various custom commands and overwrite 4 | // existing commands. 5 | // 6 | // For more comprehensive examples of custom 7 | // commands please read more here: 8 | // https://on.cypress.io/custom-commands 9 | // *********************************************** 10 | // 11 | // 12 | // -- This is a parent command -- 13 | // Cypress.Commands.add('login', (email, password) => { ... }) 14 | // 15 | // 16 | // -- This is a child command -- 17 | // Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... }) 18 | // 19 | // 20 | // -- This is a dual command -- 21 | // Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... }) 22 | // 23 | // 24 | // -- This will overwrite an existing command -- 25 | // Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... }) 26 | 27 | import '@testing-library/cypress/add-commands'; 28 | 29 | Cypress.Commands.add('google', () => cy.visit('http://google.com')); 30 | -------------------------------------------------------------------------------- /cypress/support/index.d.ts: -------------------------------------------------------------------------------- 1 | // Load type definition Cypress Modules 2 | /// 3 | 4 | declare namespace Cypress { 5 | interface Chainable { 6 | /** 7 | * Custom command to visit Google 8 | * @example cy.google() 9 | */ 10 | google(): Chainable; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /cypress/support/index.ts: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/index.js is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | // Import commands.js using ES2015 syntax: 17 | import './commands'; 18 | 19 | // Alternatively you can use CommonJS syntax: 20 | // require('./commands') 21 | -------------------------------------------------------------------------------- /cypress/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["es5", "dom"], 5 | "types": ["cypress", "@testing-library/cypress"] 6 | }, 7 | "include": ["**/*.ts"] 8 | } 9 | -------------------------------------------------------------------------------- /generators/plopfile.js: -------------------------------------------------------------------------------- 1 | module.exports = (plop) => { 2 | plop.setGenerator('component', { 3 | description: 'Create a component', 4 | prompts: [ 5 | { 6 | type: 'input', 7 | name: 'name', 8 | message: 'What is your component name?' 9 | } 10 | ], 11 | actions: [ 12 | { 13 | type: 'add', 14 | path: '../src/components/{{pascalCase name}}/index.tsx', 15 | templateFile: 'templates/Component.tsx.hbs' 16 | }, 17 | { 18 | type: 'add', 19 | path: '../src/components/{{pascalCase name}}/stories.tsx', 20 | templateFile: 'templates/stories.tsx.hbs' 21 | }, 22 | { 23 | type: 'add', 24 | path: '../src/components/{{pascalCase name}}/styles.ts', 25 | templateFile: 'templates/styles.ts.hbs' 26 | }, 27 | 28 | { 29 | type: 'add', 30 | path: '../src/components/{{pascalCase name}}/test.tsx', 31 | templateFile: 'templates/test.tsx.hbs' 32 | } 33 | ] 34 | }); 35 | }; 36 | -------------------------------------------------------------------------------- /generators/templates/Component.tsx.hbs: -------------------------------------------------------------------------------- 1 | import * as S from './styles'; 2 | 3 | const {{pascalCase name}} = () => { 4 | return ( 5 | 6 |

{{pascalCase name}}

7 |
8 | ); 9 | }; 10 | 11 | export default {{pascalCase name}}; 12 | -------------------------------------------------------------------------------- /generators/templates/stories.tsx.hbs: -------------------------------------------------------------------------------- 1 | import { Story, Meta } from '@storybook/react/types-6-0'; 2 | import {{pascalCase name}} from '.'; 3 | 4 | export default { 5 | title: '{{pascalCase name}}', 6 | component: {{pascalCase name}} 7 | } as Meta; 8 | 9 | export const Default: Story = () => <{{pascalCase name}} />; 10 | -------------------------------------------------------------------------------- /generators/templates/styles.ts.hbs: -------------------------------------------------------------------------------- 1 | import styled, { css } from 'styled-components'; 2 | 3 | export const Container = styled.div` 4 | ${({ theme }) => css``} 5 | `; 6 | -------------------------------------------------------------------------------- /generators/templates/test.tsx.hbs: -------------------------------------------------------------------------------- 1 | import { screen } from '@testing-library/react'; 2 | 3 | import { renderWithTheme } from '../../utils/tests/helpers'; 4 | 5 | import {{pascalCase name}} from '.'; 6 | 7 | describe('<{{pascalCase name}} />', () => { 8 | it('should render the heading', () => { 9 | const { container } = renderWithTheme(<{{pascalCase name}} />); 10 | 11 | expect( 12 | screen.getByRole('heading', { name: /{{pascalCase name}}/i }) 13 | ).toBeInTheDocument(); 14 | 15 | expect(container.firstChild).toMatchSnapshot(); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef */ 2 | // eslint-disable-next-line @typescript-eslint/no-var-requires 3 | const nextJest = require('next/jest'); 4 | 5 | const createJestConfig = nextJest({ dir: './' }); 6 | 7 | const customJestConfig = { 8 | setupFilesAfterEnv: ['/.jest/setup.ts'], 9 | testEnvironment: 'jest-environment-jsdom', 10 | testPathIgnorePatterns: ['/node_modules', '/.next/'], 11 | collectCoverage: true, 12 | collectCoverageFrom: [ 13 | 'src/**/*.{ts,tsx}', 14 | '!src/@types/**/*', 15 | '!src/pages/**/*', 16 | '!src/constants/**/*', 17 | '!src/styles/**/*', 18 | '!src/**/*.stories.tsx', 19 | '!src/**/styles.ts', 20 | '!src/**/types.ts' 21 | ], 22 | modulePaths: ['/src/', '/.jest'], 23 | moduleNameMapper: { 24 | '\\.(css|scss)$': 'react-toastify', 25 | '^styled-components': 26 | '/node_modules/styled-components/dist/styled-components.browser.cjs.js' 27 | } 28 | }; 29 | 30 | module.exports = createJestConfig(customJestConfig); 31 | -------------------------------------------------------------------------------- /next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | 3 | /** @type {import('next').NextConfig} */ 4 | module.exports = { 5 | reactStrictMode: true, 6 | swcMinify: true, 7 | images: { 8 | formats: ['image/avif', 'image/webp'] 9 | }, 10 | compiler: { 11 | styledComponents: true, 12 | removeConsole: { 13 | exclude: ['error'] 14 | } 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "app-name", 3 | "version": "0.1.0", 4 | "author": "Junior Alves ", 5 | "private": true, 6 | "scripts": { 7 | "dev": "next dev", 8 | "build": "next build", 9 | "start": "next start", 10 | "test": "jest", 11 | "typecheck": "tsc --project tsconfig.json --noEmit", 12 | "lint": "eslint src --max-warnings=0", 13 | "postinstall": "husky install", 14 | "cy:open": "cypress open", 15 | "cy:run": "cypress run", 16 | "test:watch": "yarn test --watch", 17 | "generate": "yarn plop --plopfile generators/plopfile.js", 18 | "storybook": "start-storybook -p 6006 -s public", 19 | "build-storybook": "build-storybook -s public" 20 | }, 21 | "husky": { 22 | "hooks": { 23 | "pre-commit": "lint-staged" 24 | } 25 | }, 26 | "lint-staged": { 27 | "src/**/*": [ 28 | "yarn lint --fix", 29 | "yarn test --findRelatedTests --bail" 30 | ] 31 | }, 32 | "dependencies": { 33 | "axios": "^1.8.1", 34 | "cors": "^2.8.5", 35 | "eslint-config-next": "^15.2.0", 36 | "next": "^13.5.6", 37 | "next-connect": "^1.0.0", 38 | "next-pwa": "^5.5.5", 39 | "polished": "^4.3.1", 40 | "react": "^18.3.1", 41 | "react-dom": "^18.3.1", 42 | "react-hook-form": "^7.54.2", 43 | "react-icons": "^5.5.0", 44 | "styled-components": "5.3.11" 45 | }, 46 | "devDependencies": { 47 | "@faker-js/faker": "^9.0.0", 48 | "@storybook/addon-essentials": "8.1.11", 49 | "@storybook/builder-webpack5": "^8.1.11", 50 | "@storybook/manager-webpack5": "^6.5.16", 51 | "@storybook/react": "6.4.22", 52 | "@swc/core": "^1.11.5", 53 | "@swc/jest": "^0.2.37", 54 | "@testing-library/cypress": "^10.0.3", 55 | "@testing-library/jest-dom": "^6.6.3", 56 | "@testing-library/react": "^15.0.7", 57 | "@types/cors": "^2.8.17", 58 | "@types/jest": "^27.5.0", 59 | "@types/node": "^22.13.8", 60 | "@types/react": "^18.3.13", 61 | "@types/styled-components": "^5.1.26", 62 | "@typescript-eslint/eslint-plugin": "^5.62.0", 63 | "@typescript-eslint/parser": "^5.62.0", 64 | "cypress": "^14.1.0", 65 | "eslint": "^8.57.1", 66 | "eslint-config-prettier": "^10.0.2", 67 | "eslint-plugin-cypress": "^3.6.0", 68 | "eslint-plugin-import-helpers": "^2.0.0", 69 | "eslint-plugin-prettier": "^4.2.1", 70 | "eslint-plugin-react": "^7.37.4", 71 | "eslint-plugin-react-hooks": "^5.2.0", 72 | "husky": "9", 73 | "jest": "^27.5.1", 74 | "jest-styled-components": "^7.2.0", 75 | "lint-staged": "^15.4.3", 76 | "plop": "^4.0.1", 77 | "prettier": "^2.8.8", 78 | "storybook-addon-next": "^1.8.0", 79 | "storybook-addon-next-router": "^4.0.2", 80 | "ts-jest": "^27.1.4", 81 | "typescript": "^4.9.5", 82 | "webpack": "5.98.0" 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /public/assets/403.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /public/assets/404.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /public/assets/500.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /public/assets/bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jjunior96/next-template/ae30b30f16b32edcbe7e8956a539e345f19e4928/public/assets/bg.jpg -------------------------------------------------------------------------------- /public/assets/vercel.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jjunior96/next-template/ae30b30f16b32edcbe7e8956a539e345f19e4928/public/favicon.ico -------------------------------------------------------------------------------- /public/fonts/montserrat-v15-latin-300.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jjunior96/next-template/ae30b30f16b32edcbe7e8956a539e345f19e4928/public/fonts/montserrat-v15-latin-300.woff2 -------------------------------------------------------------------------------- /public/fonts/montserrat-v15-latin-600.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jjunior96/next-template/ae30b30f16b32edcbe7e8956a539e345f19e4928/public/fonts/montserrat-v15-latin-600.woff2 -------------------------------------------------------------------------------- /public/fonts/montserrat-v15-latin-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jjunior96/next-template/ae30b30f16b32edcbe7e8956a539e345f19e4928/public/fonts/montserrat-v15-latin-regular.woff2 -------------------------------------------------------------------------------- /public/img/bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jjunior96/next-template/ae30b30f16b32edcbe7e8956a539e345f19e4928/public/img/bg.png -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "App name", 3 | "short_name": "APP", 4 | "icons": [ 5 | { 6 | "src": "/img/bg.png", 7 | "type": "image/png", 8 | "sizes": "192x192" 9 | }, 10 | { 11 | "src": "/img/bg.png", 12 | "type": "image/png", 13 | "sizes": "512x512" 14 | } 15 | ], 16 | "background_color": "#0c0f16", 17 | "description": "App PWA", 18 | "display": "fullscreen", 19 | "start_url": "/", 20 | "theme_color": "#0c0f16" 21 | } 22 | -------------------------------------------------------------------------------- /public/sw.js: -------------------------------------------------------------------------------- 1 | if(!self.define){let e,s={};const n=(n,t)=>(n=new URL(n+".js",t).href,s[n]||new Promise((s=>{if("document"in self){const e=document.createElement("script");e.src=n,e.onload=s,document.head.appendChild(e)}else e=n,importScripts(n),s()})).then((()=>{let e=s[n];if(!e)throw new Error(`Module ${n} didn’t register its module`);return e})));self.define=(t,a)=>{const i=e||("document"in self?document.currentScript.src:"")||location.href;if(s[i])return;let c={};const r=e=>n(e,i),o={module:{uri:i},exports:c,require:r};s[i]=Promise.all(t.map((e=>o[e]||r(e)))).then((e=>(a(...e),c)))}}define(["./workbox-4bf1368b"],(function(e){"use strict";importScripts(),self.skipWaiting(),e.clientsClaim(),e.precacheAndRoute([{url:"/_next/static/81p2ANMbDzu1QzW7Wsn0b/_buildManifest.js",revision:"22ec3c1f0770258d011d0ee94795d00e"},{url:"/_next/static/81p2ANMbDzu1QzW7Wsn0b/_middlewareManifest.js",revision:"468e9a0ecca0c65bcb0ee673b762445d"},{url:"/_next/static/81p2ANMbDzu1QzW7Wsn0b/_ssgManifest.js",revision:"5352cb582146311d1540f6075d1f265e"},{url:"/_next/static/chunks/894.3c24362559cab12e.js",revision:"3c24362559cab12e"},{url:"/_next/static/chunks/framework-3581bb99e4b98af8.js",revision:"3581bb99e4b98af8"},{url:"/_next/static/chunks/main-79c9ff5761025bed.js",revision:"79c9ff5761025bed"},{url:"/_next/static/chunks/pages/_app-4fd4f38d31577046.js",revision:"4fd4f38d31577046"},{url:"/_next/static/chunks/pages/_error-07466524dba6669b.js",revision:"07466524dba6669b"},{url:"/_next/static/chunks/pages/index-4425293dce4dead9.js",revision:"4425293dce4dead9"},{url:"/_next/static/chunks/polyfills-5cd94c89d3acac5f.js",revision:"99442aec5788bccac9b2f0ead2afdd6b"},{url:"/_next/static/chunks/webpack-bff6e9f4898c29cf.js",revision:"bff6e9f4898c29cf"},{url:"/assets/bg.jpg",revision:"dd327162dd67a05e5bbef629c04fdfc1"},{url:"/assets/vercel.svg",revision:"4b4f1876502eb6721764637fe5c41702"},{url:"/favicon.ico",revision:"7d3eb966feb5c6060ad7b5843e749f10"},{url:"/fonts/montserrat-v15-latin-300.woff2",revision:"7c3daf12b706645b5d3710f863a4da04"},{url:"/fonts/montserrat-v15-latin-600.woff2",revision:"6fb1b5623e528e27c18658fecf5ee0ee"},{url:"/fonts/montserrat-v15-latin-regular.woff2",revision:"bc3aa95dca08f5fee5291e34959c27bc"},{url:"/img/bg.png",revision:"7d3eb966feb5c6060ad7b5843e749f10"},{url:"/manifest.json",revision:"0ba6a4dae212781b79a7ceda50a5c8b0"}],{ignoreURLParametersMatching:[]}),e.cleanupOutdatedCaches(),e.registerRoute("/",new e.NetworkFirst({cacheName:"start-url",plugins:[{cacheWillUpdate:async({request:e,response:s,event:n,state:t})=>s&&"opaqueredirect"===s.type?new Response(s.body,{status:200,statusText:"OK",headers:s.headers}):s}]}),"GET"),e.registerRoute(/^https:\/\/fonts\.(?:gstatic)\.com\/.*/i,new e.CacheFirst({cacheName:"google-fonts-webfonts",plugins:[new e.ExpirationPlugin({maxEntries:4,maxAgeSeconds:31536e3})]}),"GET"),e.registerRoute(/^https:\/\/fonts\.(?:googleapis)\.com\/.*/i,new e.StaleWhileRevalidate({cacheName:"google-fonts-stylesheets",plugins:[new e.ExpirationPlugin({maxEntries:4,maxAgeSeconds:604800})]}),"GET"),e.registerRoute(/\.(?:eot|otf|ttc|ttf|woff|woff2|font.css)$/i,new e.StaleWhileRevalidate({cacheName:"static-font-assets",plugins:[new e.ExpirationPlugin({maxEntries:4,maxAgeSeconds:604800})]}),"GET"),e.registerRoute(/\.(?:jpg|jpeg|gif|png|svg|ico|webp)$/i,new e.StaleWhileRevalidate({cacheName:"static-image-assets",plugins:[new e.ExpirationPlugin({maxEntries:64,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\/_next\/image\?url=.+$/i,new e.StaleWhileRevalidate({cacheName:"next-image",plugins:[new e.ExpirationPlugin({maxEntries:64,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\.(?:mp3|wav|ogg)$/i,new e.CacheFirst({cacheName:"static-audio-assets",plugins:[new e.RangeRequestsPlugin,new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\.(?:mp4)$/i,new e.CacheFirst({cacheName:"static-video-assets",plugins:[new e.RangeRequestsPlugin,new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\.(?:js)$/i,new e.StaleWhileRevalidate({cacheName:"static-js-assets",plugins:[new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\.(?:css|less)$/i,new e.StaleWhileRevalidate({cacheName:"static-style-assets",plugins:[new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\/_next\/data\/.+\/.+\.json$/i,new e.StaleWhileRevalidate({cacheName:"next-data",plugins:[new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\.(?:json|xml|csv)$/i,new e.NetworkFirst({cacheName:"static-data-assets",plugins:[new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET"),e.registerRoute((({url:e})=>{if(!(self.origin===e.origin))return!1;const s=e.pathname;return!s.startsWith("/api/auth/")&&!!s.startsWith("/api/")}),new e.NetworkFirst({cacheName:"apis",networkTimeoutSeconds:10,plugins:[new e.ExpirationPlugin({maxEntries:16,maxAgeSeconds:86400})]}),"GET"),e.registerRoute((({url:e})=>{if(!(self.origin===e.origin))return!1;return!e.pathname.startsWith("/api/")}),new e.NetworkFirst({cacheName:"others",networkTimeoutSeconds:10,plugins:[new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET"),e.registerRoute((({url:e})=>!(self.origin===e.origin)),new e.NetworkFirst({cacheName:"cross-origin",networkTimeoutSeconds:10,plugins:[new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:3600})]}),"GET")})); 2 | -------------------------------------------------------------------------------- /public/workbox-4bf1368b.js: -------------------------------------------------------------------------------- 1 | define(["exports"],(function(t){"use strict";try{self["workbox:core:6.5.1"]&&_()}catch(t){}const e=(t,...e)=>{let s=t;return e.length>0&&(s+=` :: ${JSON.stringify(e)}`),s};class s extends Error{constructor(t,s){super(e(t,s)),this.name=t,this.details=s}}try{self["workbox:routing:6.5.1"]&&_()}catch(t){}const n=t=>t&&"object"==typeof t?t:{handle:t};class r{constructor(t,e,s="GET"){this.handler=n(e),this.match=t,this.method=s}setCatchHandler(t){this.catchHandler=n(t)}}class i extends r{constructor(t,e,s){super((({url:e})=>{const s=t.exec(e.href);if(s&&(e.origin===location.origin||0===s.index))return s.slice(1)}),e,s)}}class a{constructor(){this.t=new Map,this.i=new Map}get routes(){return this.t}addFetchListener(){self.addEventListener("fetch",(t=>{const{request:e}=t,s=this.handleRequest({request:e,event:t});s&&t.respondWith(s)}))}addCacheListener(){self.addEventListener("message",(t=>{if(t.data&&"CACHE_URLS"===t.data.type){const{payload:e}=t.data,s=Promise.all(e.urlsToCache.map((e=>{"string"==typeof e&&(e=[e]);const s=new Request(...e);return this.handleRequest({request:s,event:t})})));t.waitUntil(s),t.ports&&t.ports[0]&&s.then((()=>t.ports[0].postMessage(!0)))}}))}handleRequest({request:t,event:e}){const s=new URL(t.url,location.href);if(!s.protocol.startsWith("http"))return;const n=s.origin===location.origin,{params:r,route:i}=this.findMatchingRoute({event:e,request:t,sameOrigin:n,url:s});let a=i&&i.handler;const o=t.method;if(!a&&this.i.has(o)&&(a=this.i.get(o)),!a)return;let c;try{c=a.handle({url:s,request:t,event:e,params:r})}catch(t){c=Promise.reject(t)}const h=i&&i.catchHandler;return c instanceof Promise&&(this.o||h)&&(c=c.catch((async n=>{if(h)try{return await h.handle({url:s,request:t,event:e,params:r})}catch(t){t instanceof Error&&(n=t)}if(this.o)return this.o.handle({url:s,request:t,event:e});throw n}))),c}findMatchingRoute({url:t,sameOrigin:e,request:s,event:n}){const r=this.t.get(s.method)||[];for(const i of r){let r;const a=i.match({url:t,sameOrigin:e,request:s,event:n});if(a)return r=a,(Array.isArray(r)&&0===r.length||a.constructor===Object&&0===Object.keys(a).length||"boolean"==typeof a)&&(r=void 0),{route:i,params:r}}return{}}setDefaultHandler(t,e="GET"){this.i.set(e,n(t))}setCatchHandler(t){this.o=n(t)}registerRoute(t){this.t.has(t.method)||this.t.set(t.method,[]),this.t.get(t.method).push(t)}unregisterRoute(t){if(!this.t.has(t.method))throw new s("unregister-route-but-not-found-with-method",{method:t.method});const e=this.t.get(t.method).indexOf(t);if(!(e>-1))throw new s("unregister-route-route-not-registered");this.t.get(t.method).splice(e,1)}}let o;const c=()=>(o||(o=new a,o.addFetchListener(),o.addCacheListener()),o);function h(t,e,n){let a;if("string"==typeof t){const s=new URL(t,location.href);a=new r((({url:t})=>t.href===s.href),e,n)}else if(t instanceof RegExp)a=new i(t,e,n);else if("function"==typeof t)a=new r(t,e,n);else{if(!(t instanceof r))throw new s("unsupported-route-type",{moduleName:"workbox-routing",funcName:"registerRoute",paramName:"capture"});a=t}return c().registerRoute(a),a}try{self["workbox:strategies:6.5.1"]&&_()}catch(t){}const u={cacheWillUpdate:async({response:t})=>200===t.status||0===t.status?t:null},l={googleAnalytics:"googleAnalytics",precache:"precache-v2",prefix:"workbox",runtime:"runtime",suffix:"undefined"!=typeof registration?registration.scope:""},f=t=>[l.prefix,t,l.suffix].filter((t=>t&&t.length>0)).join("-"),w=t=>t||f(l.precache),d=t=>t||f(l.runtime);function p(t,e){const s=new URL(t);for(const t of e)s.searchParams.delete(t);return s.href}class y{constructor(){this.promise=new Promise(((t,e)=>{this.resolve=t,this.reject=e}))}}const g=new Set;function m(t){return"string"==typeof t?new Request(t):t}class R{constructor(t,e){this.h={},Object.assign(this,e),this.event=e.event,this.u=t,this.l=new y,this.p=[],this.g=[...t.plugins],this.m=new Map;for(const t of this.g)this.m.set(t,{});this.event.waitUntil(this.l.promise)}async fetch(t){const{event:e}=this;let n=m(t);if("navigate"===n.mode&&e instanceof FetchEvent&&e.preloadResponse){const t=await e.preloadResponse;if(t)return t}const r=this.hasCallback("fetchDidFail")?n.clone():null;try{for(const t of this.iterateCallbacks("requestWillFetch"))n=await t({request:n.clone(),event:e})}catch(t){if(t instanceof Error)throw new s("plugin-error-request-will-fetch",{thrownErrorMessage:t.message})}const i=n.clone();try{let t;t=await fetch(n,"navigate"===n.mode?void 0:this.u.fetchOptions);for(const s of this.iterateCallbacks("fetchDidSucceed"))t=await s({event:e,request:i,response:t});return t}catch(t){throw r&&await this.runCallbacks("fetchDidFail",{error:t,event:e,originalRequest:r.clone(),request:i.clone()}),t}}async fetchAndCachePut(t){const e=await this.fetch(t),s=e.clone();return this.waitUntil(this.cachePut(t,s)),e}async cacheMatch(t){const e=m(t);let s;const{cacheName:n,matchOptions:r}=this.u,i=await this.getCacheKey(e,"read"),a=Object.assign(Object.assign({},r),{cacheName:n});s=await caches.match(i,a);for(const t of this.iterateCallbacks("cachedResponseWillBeUsed"))s=await t({cacheName:n,matchOptions:r,cachedResponse:s,request:i,event:this.event})||void 0;return s}async cachePut(t,e){const n=m(t);var r;await(r=0,new Promise((t=>setTimeout(t,r))));const i=await this.getCacheKey(n,"write");if(!e)throw new s("cache-put-with-no-response",{url:(a=i.url,new URL(String(a),location.href).href.replace(new RegExp(`^${location.origin}`),""))});var a;const o=await this.R(e);if(!o)return!1;const{cacheName:c,matchOptions:h}=this.u,u=await self.caches.open(c),l=this.hasCallback("cacheDidUpdate"),f=l?await async function(t,e,s,n){const r=p(e.url,s);if(e.url===r)return t.match(e,n);const i=Object.assign(Object.assign({},n),{ignoreSearch:!0}),a=await t.keys(e,i);for(const e of a)if(r===p(e.url,s))return t.match(e,n)}(u,i.clone(),["__WB_REVISION__"],h):null;try{await u.put(i,l?o.clone():o)}catch(t){if(t instanceof Error)throw"QuotaExceededError"===t.name&&await async function(){for(const t of g)await t()}(),t}for(const t of this.iterateCallbacks("cacheDidUpdate"))await t({cacheName:c,oldResponse:f,newResponse:o.clone(),request:i,event:this.event});return!0}async getCacheKey(t,e){const s=`${t.url} | ${e}`;if(!this.h[s]){let n=t;for(const t of this.iterateCallbacks("cacheKeyWillBeUsed"))n=m(await t({mode:e,request:n,event:this.event,params:this.params}));this.h[s]=n}return this.h[s]}hasCallback(t){for(const e of this.u.plugins)if(t in e)return!0;return!1}async runCallbacks(t,e){for(const s of this.iterateCallbacks(t))await s(e)}*iterateCallbacks(t){for(const e of this.u.plugins)if("function"==typeof e[t]){const s=this.m.get(e),n=n=>{const r=Object.assign(Object.assign({},n),{state:s});return e[t](r)};yield n}}waitUntil(t){return this.p.push(t),t}async doneWaiting(){let t;for(;t=this.p.shift();)await t}destroy(){this.l.resolve(null)}async R(t){let e=t,s=!1;for(const t of this.iterateCallbacks("cacheWillUpdate"))if(e=await t({request:this.request,response:e,event:this.event})||void 0,s=!0,!e)break;return s||e&&200!==e.status&&(e=void 0),e}}class v{constructor(t={}){this.cacheName=d(t.cacheName),this.plugins=t.plugins||[],this.fetchOptions=t.fetchOptions,this.matchOptions=t.matchOptions}handle(t){const[e]=this.handleAll(t);return e}handleAll(t){t instanceof FetchEvent&&(t={event:t,request:t.request});const e=t.event,s="string"==typeof t.request?new Request(t.request):t.request,n="params"in t?t.params:void 0,r=new R(this,{event:e,request:s,params:n}),i=this.v(r,s,e);return[i,this.q(i,r,s,e)]}async v(t,e,n){let r;await t.runCallbacks("handlerWillStart",{event:n,request:e});try{if(r=await this.D(e,t),!r||"error"===r.type)throw new s("no-response",{url:e.url})}catch(s){if(s instanceof Error)for(const i of t.iterateCallbacks("handlerDidError"))if(r=await i({error:s,event:n,request:e}),r)break;if(!r)throw s}for(const s of t.iterateCallbacks("handlerWillRespond"))r=await s({event:n,request:e,response:r});return r}async q(t,e,s,n){let r,i;try{r=await t}catch(i){}try{await e.runCallbacks("handlerDidRespond",{event:n,request:s,response:r}),await e.doneWaiting()}catch(t){t instanceof Error&&(i=t)}if(await e.runCallbacks("handlerDidComplete",{event:n,request:s,response:r,error:i}),e.destroy(),i)throw i}}function b(t){t.then((()=>{}))}function q(){return(q=Object.assign?Object.assign.bind():function(t){for(var e=1;e(t[e]=s,!0),has:(t,e)=>t instanceof IDBTransaction&&("done"===e||"store"===e)||e in t};function O(t){return t!==IDBDatabase.prototype.transaction||"objectStoreNames"in IDBTransaction.prototype?(U||(U=[IDBCursor.prototype.advance,IDBCursor.prototype.continue,IDBCursor.prototype.continuePrimaryKey])).includes(t)?function(...e){return t.apply(B(this),e),k(x.get(this))}:function(...e){return k(t.apply(B(this),e))}:function(e,...s){const n=t.call(B(this),e,...s);return I.set(n,e.sort?e.sort():[e]),k(n)}}function T(t){return"function"==typeof t?O(t):(t instanceof IDBTransaction&&function(t){if(L.has(t))return;const e=new Promise(((e,s)=>{const n=()=>{t.removeEventListener("complete",r),t.removeEventListener("error",i),t.removeEventListener("abort",i)},r=()=>{e(),n()},i=()=>{s(t.error||new DOMException("AbortError","AbortError")),n()};t.addEventListener("complete",r),t.addEventListener("error",i),t.addEventListener("abort",i)}));L.set(t,e)}(t),e=t,(D||(D=[IDBDatabase,IDBObjectStore,IDBIndex,IDBCursor,IDBTransaction])).some((t=>e instanceof t))?new Proxy(t,N):t);var e}function k(t){if(t instanceof IDBRequest)return function(t){const e=new Promise(((e,s)=>{const n=()=>{t.removeEventListener("success",r),t.removeEventListener("error",i)},r=()=>{e(k(t.result)),n()},i=()=>{s(t.error),n()};t.addEventListener("success",r),t.addEventListener("error",i)}));return e.then((e=>{e instanceof IDBCursor&&x.set(e,t)})).catch((()=>{})),E.set(e,t),e}(t);if(C.has(t))return C.get(t);const e=T(t);return e!==t&&(C.set(t,e),E.set(e,t)),e}const B=t=>E.get(t);const P=["get","getKey","getAll","getAllKeys","count"],M=["put","add","delete","clear"],W=new Map;function j(t,e){if(!(t instanceof IDBDatabase)||e in t||"string"!=typeof e)return;if(W.get(e))return W.get(e);const s=e.replace(/FromIndex$/,""),n=e!==s,r=M.includes(s);if(!(s in(n?IDBIndex:IDBObjectStore).prototype)||!r&&!P.includes(s))return;const i=async function(t,...e){const i=this.transaction(t,r?"readwrite":"readonly");let a=i.store;return n&&(a=a.index(e.shift())),(await Promise.all([a[s](...e),r&&i.done]))[0]};return W.set(e,i),i}N=(t=>q({},t,{get:(e,s,n)=>j(e,s)||t.get(e,s,n),has:(e,s)=>!!j(e,s)||t.has(e,s)}))(N);try{self["workbox:expiration:6.5.1"]&&_()}catch(t){}const S="cache-entries",K=t=>{const e=new URL(t,location.href);return e.hash="",e.href};class A{constructor(t){this.U=null,this._=t}L(t){const e=t.createObjectStore(S,{keyPath:"id"});e.createIndex("cacheName","cacheName",{unique:!1}),e.createIndex("timestamp","timestamp",{unique:!1})}I(t){this.L(t),this._&&function(t,{blocked:e}={}){const s=indexedDB.deleteDatabase(t);e&&s.addEventListener("blocked",(()=>e())),k(s).then((()=>{}))}(this._)}async setTimestamp(t,e){const s={url:t=K(t),timestamp:e,cacheName:this._,id:this.C(t)},n=(await this.getDb()).transaction(S,"readwrite",{durability:"relaxed"});await n.store.put(s),await n.done}async getTimestamp(t){const e=await this.getDb(),s=await e.get(S,this.C(t));return null==s?void 0:s.timestamp}async expireEntries(t,e){const s=await this.getDb();let n=await s.transaction(S).store.index("timestamp").openCursor(null,"prev");const r=[];let i=0;for(;n;){const s=n.value;s.cacheName===this._&&(t&&s.timestamp=e?r.push(n.value):i++),n=await n.continue()}const a=[];for(const t of r)await s.delete(S,t.id),a.push(t.url);return a}C(t){return this._+"|"+K(t)}async getDb(){return this.U||(this.U=await function(t,e,{blocked:s,upgrade:n,blocking:r,terminated:i}={}){const a=indexedDB.open(t,e),o=k(a);return n&&a.addEventListener("upgradeneeded",(t=>{n(k(a.result),t.oldVersion,t.newVersion,k(a.transaction))})),s&&a.addEventListener("blocked",(()=>s())),o.then((t=>{i&&t.addEventListener("close",(()=>i())),r&&t.addEventListener("versionchange",(()=>r()))})).catch((()=>{})),o}("workbox-expiration",1,{upgrade:this.I.bind(this)})),this.U}}class F{constructor(t,e={}){this.N=!1,this.O=!1,this.T=e.maxEntries,this.k=e.maxAgeSeconds,this.B=e.matchOptions,this._=t,this.P=new A(t)}async expireEntries(){if(this.N)return void(this.O=!0);this.N=!0;const t=this.k?Date.now()-1e3*this.k:0,e=await this.P.expireEntries(t,this.T),s=await self.caches.open(this._);for(const t of e)await s.delete(t,this.B);this.N=!1,this.O&&(this.O=!1,b(this.expireEntries()))}async updateTimestamp(t){await this.P.setTimestamp(t,Date.now())}async isURLExpired(t){if(this.k){const e=await this.P.getTimestamp(t),s=Date.now()-1e3*this.k;return void 0===e||er||e&&e<0)throw new s("range-not-satisfiable",{size:r,end:n,start:e});let i,a;return void 0!==e&&void 0!==n?(i=e,a=n+1):void 0!==e&&void 0===n?(i=e,a=r):void 0!==n&&void 0===e&&(i=r-n,a=r),{start:i,end:a}}(i,r.start,r.end),o=i.slice(a.start,a.end),c=o.size,h=new Response(o,{status:206,statusText:"Partial Content",headers:e.headers});return h.headers.set("Content-Length",String(c)),h.headers.set("Content-Range",`bytes ${a.start}-${a.end-1}/${i.size}`),h}catch(t){return new Response("",{status:416,statusText:"Range Not Satisfiable"})}}function $(t,e){const s=e();return t.waitUntil(s),s}try{self["workbox:precaching:6.5.1"]&&_()}catch(t){}function z(t){if(!t)throw new s("add-to-cache-list-unexpected-type",{entry:t});if("string"==typeof t){const e=new URL(t,location.href);return{cacheKey:e.href,url:e.href}}const{revision:e,url:n}=t;if(!n)throw new s("add-to-cache-list-unexpected-type",{entry:t});if(!e){const t=new URL(n,location.href);return{cacheKey:t.href,url:t.href}}const r=new URL(n,location.href),i=new URL(n,location.href);return r.searchParams.set("__WB_REVISION__",e),{cacheKey:r.href,url:i.href}}class G{constructor(){this.updatedURLs=[],this.notUpdatedURLs=[],this.handlerWillStart=async({request:t,state:e})=>{e&&(e.originalRequest=t)},this.cachedResponseWillBeUsed=async({event:t,state:e,cachedResponse:s})=>{if("install"===t.type&&e&&e.originalRequest&&e.originalRequest instanceof Request){const t=e.originalRequest.url;s?this.notUpdatedURLs.push(t):this.updatedURLs.push(t)}return s}}}class V{constructor({precacheController:t}){this.cacheKeyWillBeUsed=async({request:t,params:e})=>{const s=(null==e?void 0:e.cacheKey)||this.M.getCacheKeyForURL(t.url);return s?new Request(s,{headers:t.headers}):t},this.M=t}}let J,Q;async function X(t,e){let n=null;if(t.url){n=new URL(t.url).origin}if(n!==self.location.origin)throw new s("cross-origin-copy-response",{origin:n});const r=t.clone(),i={headers:new Headers(r.headers),status:r.status,statusText:r.statusText},a=e?e(i):i,o=function(){if(void 0===J){const t=new Response("");if("body"in t)try{new Response(t.body),J=!0}catch(t){J=!1}J=!1}return J}()?r.body:await r.blob();return new Response(o,a)}class Y extends v{constructor(t={}){t.cacheName=w(t.cacheName),super(t),this.W=!1!==t.fallbackToNetwork,this.plugins.push(Y.copyRedirectedCacheableResponsesPlugin)}async D(t,e){const s=await e.cacheMatch(t);return s||(e.event&&"install"===e.event.type?await this.j(t,e):await this.S(t,e))}async S(t,e){let n;const r=e.params||{};if(!this.W)throw new s("missing-precache-entry",{cacheName:this.cacheName,url:t.url});{const s=r.integrity,i=t.integrity,a=!i||i===s;n=await e.fetch(new Request(t,{integrity:i||s})),s&&a&&(this.K(),await e.cachePut(t,n.clone()))}return n}async j(t,e){this.K();const n=await e.fetch(t);if(!await e.cachePut(t,n.clone()))throw new s("bad-precaching-response",{url:t.url,status:n.status});return n}K(){let t=null,e=0;for(const[s,n]of this.plugins.entries())n!==Y.copyRedirectedCacheableResponsesPlugin&&(n===Y.defaultPrecacheCacheabilityPlugin&&(t=s),n.cacheWillUpdate&&e++);0===e?this.plugins.push(Y.defaultPrecacheCacheabilityPlugin):e>1&&null!==t&&this.plugins.splice(t,1)}}Y.defaultPrecacheCacheabilityPlugin={cacheWillUpdate:async({response:t})=>!t||t.status>=400?null:t},Y.copyRedirectedCacheableResponsesPlugin={cacheWillUpdate:async({response:t})=>t.redirected?await X(t):t};class Z{constructor({cacheName:t,plugins:e=[],fallbackToNetwork:s=!0}={}){this.A=new Map,this.F=new Map,this.H=new Map,this.u=new Y({cacheName:w(t),plugins:[...e,new V({precacheController:this})],fallbackToNetwork:s}),this.install=this.install.bind(this),this.activate=this.activate.bind(this)}get strategy(){return this.u}precache(t){this.addToCacheList(t),this.$||(self.addEventListener("install",this.install),self.addEventListener("activate",this.activate),this.$=!0)}addToCacheList(t){const e=[];for(const n of t){"string"==typeof n?e.push(n):n&&void 0===n.revision&&e.push(n.url);const{cacheKey:t,url:r}=z(n),i="string"!=typeof n&&n.revision?"reload":"default";if(this.A.has(r)&&this.A.get(r)!==t)throw new s("add-to-cache-list-conflicting-entries",{firstEntry:this.A.get(r),secondEntry:t});if("string"!=typeof n&&n.integrity){if(this.H.has(t)&&this.H.get(t)!==n.integrity)throw new s("add-to-cache-list-conflicting-integrities",{url:r});this.H.set(t,n.integrity)}if(this.A.set(r,t),this.F.set(r,i),e.length>0){const t=`Workbox is precaching URLs without revision info: ${e.join(", ")}\nThis is generally NOT safe. Learn more at https://bit.ly/wb-precache`;console.warn(t)}}}install(t){return $(t,(async()=>{const e=new G;this.strategy.plugins.push(e);for(const[e,s]of this.A){const n=this.H.get(s),r=this.F.get(e),i=new Request(e,{integrity:n,cache:r,credentials:"same-origin"});await Promise.all(this.strategy.handleAll({params:{cacheKey:s},request:i,event:t}))}const{updatedURLs:s,notUpdatedURLs:n}=e;return{updatedURLs:s,notUpdatedURLs:n}}))}activate(t){return $(t,(async()=>{const t=await self.caches.open(this.strategy.cacheName),e=await t.keys(),s=new Set(this.A.values()),n=[];for(const r of e)s.has(r.url)||(await t.delete(r),n.push(r.url));return{deletedURLs:n}}))}getURLsToCacheKeys(){return this.A}getCachedURLs(){return[...this.A.keys()]}getCacheKeyForURL(t){const e=new URL(t,location.href);return this.A.get(e.href)}getIntegrityForCacheKey(t){return this.H.get(t)}async matchPrecache(t){const e=t instanceof Request?t.url:t,s=this.getCacheKeyForURL(e);if(s){return(await self.caches.open(this.strategy.cacheName)).match(s)}}createHandlerBoundToURL(t){const e=this.getCacheKeyForURL(t);if(!e)throw new s("non-precached-url",{url:t});return s=>(s.request=new Request(t),s.params=Object.assign({cacheKey:e},s.params),this.strategy.handle(s))}}const tt=()=>(Q||(Q=new Z),Q);class et extends r{constructor(t,e){super((({request:s})=>{const n=t.getURLsToCacheKeys();for(const r of function*(t,{ignoreURLParametersMatching:e=[/^utm_/,/^fbclid$/],directoryIndex:s="index.html",cleanURLs:n=!0,urlManipulation:r}={}){const i=new URL(t,location.href);i.hash="",yield i.href;const a=function(t,e=[]){for(const s of[...t.searchParams.keys()])e.some((t=>t.test(s)))&&t.searchParams.delete(s);return t}(i,e);if(yield a.href,s&&a.pathname.endsWith("/")){const t=new URL(a.href);t.pathname+=s,yield t.href}if(n){const t=new URL(a.href);t.pathname+=".html",yield t.href}if(r){const t=r({url:i});for(const e of t)yield e.href}}(s.url,e)){const e=n.get(r);if(e){return{cacheKey:e,integrity:t.getIntegrityForCacheKey(e)}}}}),t.strategy)}}t.CacheFirst=class extends v{async D(t,e){let n,r=await e.cacheMatch(t);if(!r)try{r=await e.fetchAndCachePut(t)}catch(t){t instanceof Error&&(n=t)}if(!r)throw new s("no-response",{url:t.url,error:n});return r}},t.ExpirationPlugin=class{constructor(t={}){this.cachedResponseWillBeUsed=async({event:t,request:e,cacheName:s,cachedResponse:n})=>{if(!n)return null;const r=this.G(n),i=this.V(s);b(i.expireEntries());const a=i.updateTimestamp(e.url);if(t)try{t.waitUntil(a)}catch(t){}return r?n:null},this.cacheDidUpdate=async({cacheName:t,request:e})=>{const s=this.V(t);await s.updateTimestamp(e.url),await s.expireEntries()},this.J=t,this.k=t.maxAgeSeconds,this.X=new Map,t.purgeOnQuotaError&&function(t){g.add(t)}((()=>this.deleteCacheAndMetadata()))}V(t){if(t===d())throw new s("expire-custom-caches-only");let e=this.X.get(t);return e||(e=new F(t,this.J),this.X.set(t,e)),e}G(t){if(!this.k)return!0;const e=this.Y(t);if(null===e)return!0;return e>=Date.now()-1e3*this.k}Y(t){if(!t.headers.has("date"))return null;const e=t.headers.get("date"),s=new Date(e).getTime();return isNaN(s)?null:s}async deleteCacheAndMetadata(){for(const[t,e]of this.X)await self.caches.delete(t),await e.delete();this.X=new Map}},t.NetworkFirst=class extends v{constructor(t={}){super(t),this.plugins.some((t=>"cacheWillUpdate"in t))||this.plugins.unshift(u),this.Z=t.networkTimeoutSeconds||0}async D(t,e){const n=[],r=[];let i;if(this.Z){const{id:s,promise:a}=this.tt({request:t,logs:n,handler:e});i=s,r.push(a)}const a=this.et({timeoutId:i,request:t,logs:n,handler:e});r.push(a);const o=await e.waitUntil((async()=>await e.waitUntil(Promise.race(r))||await a)());if(!o)throw new s("no-response",{url:t.url});return o}tt({request:t,logs:e,handler:s}){let n;return{promise:new Promise((e=>{n=setTimeout((async()=>{e(await s.cacheMatch(t))}),1e3*this.Z)})),id:n}}async et({timeoutId:t,request:e,logs:s,handler:n}){let r,i;try{i=await n.fetchAndCachePut(e)}catch(t){t instanceof Error&&(r=t)}return t&&clearTimeout(t),!r&&i||(i=await n.cacheMatch(e)),i}},t.RangeRequestsPlugin=class{constructor(){this.cachedResponseWillBeUsed=async({request:t,cachedResponse:e})=>e&&t.headers.has("range")?await H(t,e):e}},t.StaleWhileRevalidate=class extends v{constructor(t={}){super(t),this.plugins.some((t=>"cacheWillUpdate"in t))||this.plugins.unshift(u)}async D(t,e){const n=e.fetchAndCachePut(t).catch((()=>{}));e.waitUntil(n);let r,i=await e.cacheMatch(t);if(i);else try{i=await n}catch(t){t instanceof Error&&(r=t)}if(!i)throw new s("no-response",{url:t.url,error:r});return i}},t.cleanupOutdatedCaches=function(){self.addEventListener("activate",(t=>{const e=w();t.waitUntil((async(t,e="-precache-")=>{const s=(await self.caches.keys()).filter((s=>s.includes(e)&&s.includes(self.registration.scope)&&s!==t));return await Promise.all(s.map((t=>self.caches.delete(t)))),s})(e).then((t=>{})))}))},t.clientsClaim=function(){self.addEventListener("activate",(()=>self.clients.claim()))},t.precacheAndRoute=function(t,e){!function(t){tt().precache(t)}(t),function(t){const e=tt();h(new et(e,t))}(e)},t.registerRoute=h})); 2 | -------------------------------------------------------------------------------- /src/@types/environment.d.ts: -------------------------------------------------------------------------------- 1 | declare global { 2 | namespace NodeJS { 3 | interface ProcessEnv { 4 | NEXT_PUBLIC_BASE_URL: string; 5 | NEXT_PUBLIC_USER_TYPE: string; 6 | NEXT_PUBLIC_USER_TOKEN_EXPIRES_IN: string; 7 | NEXT_PUBLIC_USER_TOKEN: string; 8 | NEXT_NODE_ENV: string; 9 | } 10 | } 11 | } 12 | 13 | export {}; 14 | -------------------------------------------------------------------------------- /src/@types/styled-components.d.ts: -------------------------------------------------------------------------------- 1 | import theme from 'styles/theme'; 2 | 3 | type Theme = typeof theme; 4 | 5 | declare module 'styled-components' { 6 | // eslint-disable-next-line @typescript-eslint/no-empty-interface 7 | export interface DefaultTheme extends Theme {} 8 | } 9 | -------------------------------------------------------------------------------- /src/components/Main/Main.spec.tsx: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react'; 2 | 3 | import Main from './Main'; 4 | 5 | describe('
', () => { 6 | it('should render the component', () => { 7 | const { container } = render( 8 |
9 |

Next Example

10 |
11 | ); 12 | 13 | expect( 14 | screen.getByRole('heading', { name: /next example/i }) 15 | ).toBeInTheDocument(); 16 | 17 | expect(container).toMatchSnapshot(); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /src/components/Main/Main.stories.tsx: -------------------------------------------------------------------------------- 1 | import { Story, Meta } from '@storybook/react/types-6-0'; 2 | 3 | import Main from './Main'; 4 | 5 | export default { 6 | title: 'Main', 7 | component: Main 8 | } as Meta; 9 | 10 | export const Basic: Story = (args) => ( 11 |
12 |

Next Example

13 |
14 | ); 15 | -------------------------------------------------------------------------------- /src/components/Main/Main.tsx: -------------------------------------------------------------------------------- 1 | import * as S from './styles'; 2 | 3 | type MainProps = { 4 | children: React.ReactNode; 5 | }; 6 | 7 | const Main = ({ children }: MainProps) => ( 8 | 9 | {children} 10 | 11 | ); 12 | 13 | export default Main; 14 | -------------------------------------------------------------------------------- /src/components/Main/__snapshots__/Main.spec.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`
should render the component 1`] = ` 4 | .c0 { 5 | display: -webkit-box; 6 | display: -webkit-flex; 7 | display: -ms-flexbox; 8 | display: flex; 9 | width: 100%; 10 | height: 100vh; 11 | } 12 | 13 | .c1 { 14 | width: 100%; 15 | max-width: 130rem; 16 | display: -webkit-box; 17 | display: -webkit-flex; 18 | display: -ms-flexbox; 19 | display: flex; 20 | -webkit-flex-direction: column; 21 | -ms-flex-direction: column; 22 | flex-direction: column; 23 | -webkit-align-items: center; 24 | -webkit-box-align: center; 25 | -ms-flex-align: center; 26 | align-items: center; 27 | -webkit-box-pack: center; 28 | -webkit-justify-content: center; 29 | -ms-flex-pack: center; 30 | justify-content: center; 31 | } 32 | 33 |
34 |
37 |
40 |

41 | Next Example 42 |

43 |
44 |
45 |
46 | `; 47 | -------------------------------------------------------------------------------- /src/components/Main/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Main } from './Main'; 2 | -------------------------------------------------------------------------------- /src/components/Main/styles.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | import theme from 'styles/theme'; 4 | 5 | export const Container = styled.div` 6 | display: flex; 7 | 8 | width: 100%; 9 | height: 100vh; 10 | `; 11 | 12 | export const Content = styled.div` 13 | width: 100%; 14 | max-width: ${theme.grid.container}; 15 | 16 | display: flex; 17 | flex-direction: column; 18 | align-items: center; 19 | justify-content: center; 20 | `; 21 | -------------------------------------------------------------------------------- /src/constants/environment-variables.ts: -------------------------------------------------------------------------------- 1 | import packageJson from '../../package.json'; 2 | 3 | const { version } = packageJson; 4 | 5 | const uri: { [key: string]: string } = { 6 | development: 'https://jsonplaceholder.typicode.com/todos', 7 | production: 'https://jsonplaceholder.typicode.com', 8 | test: 'https://' 9 | }; 10 | 11 | const NODE_ENV = process.env.NODE_ENV; 12 | 13 | export { uri, version, NODE_ENV }; 14 | -------------------------------------------------------------------------------- /src/constants/is-running-on-server.ts: -------------------------------------------------------------------------------- 1 | export const isRunningOnServer = typeof window === 'undefined' ? true : false; 2 | -------------------------------------------------------------------------------- /src/constants/times.ts: -------------------------------------------------------------------------------- 1 | export const ONE_SECOND_IN_MILLISECONDS = 1000; 2 | export const ONE_MINUTE_IN_SECONDS = 60; 3 | export const ONE_HOUR_IN_MINUTES = 60; 4 | export const ONE_DAY_IN_HOURS = 24; 5 | 6 | /** 7 | * i.e.: 1 day in milliseconds = 86.400.000 8 | */ 9 | export const ONE_DAY_IN_MILLISECONDS = 10 | ONE_SECOND_IN_MILLISECONDS * 11 | ONE_MINUTE_IN_SECONDS * 12 | ONE_HOUR_IN_MINUTES * 13 | ONE_DAY_IN_HOURS; 14 | 15 | export const ONE_HOUR_IN_MILLISECONDS = 16 | ONE_SECOND_IN_MILLISECONDS * ONE_MINUTE_IN_SECONDS * ONE_HOUR_IN_MINUTES; 17 | 18 | export const ONE_MINUTE_IN_MILLISECONDS = 19 | (ONE_SECOND_IN_MILLISECONDS / ONE_MINUTE_IN_SECONDS) % ONE_HOUR_IN_MINUTES; 20 | 21 | /** 22 | * i.e.: 12/31/2023 23 | */ 24 | export const LAST_DAY_OF_YEAR = `12/31/${new Date().getFullYear()}`; 25 | -------------------------------------------------------------------------------- /src/errors/BadRequestError.ts: -------------------------------------------------------------------------------- 1 | import BaseError, { ErrorType } from './BaseError'; 2 | 3 | export default class BadRequestError extends BaseError { 4 | constructor({ 5 | status = 400, 6 | message, 7 | name = 'BadRequestError', 8 | type = 'bad_request', 9 | errors = [] 10 | }: ErrorType) { 11 | super({ message }); 12 | 13 | this.name = name; 14 | this.status = status; 15 | this.type = type; 16 | this.errors = errors; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/errors/BaseError.ts: -------------------------------------------------------------------------------- 1 | export interface ErrorType { 2 | status?: number; 3 | name?: string; 4 | type?: string; 5 | message?: any; // eslint-disable-line @typescript-eslint/no-explicit-any 6 | errors?: any[]; // eslint-disable-line @typescript-eslint/no-explicit-any 7 | stack?: string; 8 | } 9 | 10 | export default class BaseError extends Error { 11 | status: number; 12 | type: string; 13 | errors: any[]; // eslint-disable-line @typescript-eslint/no-explicit-any 14 | 15 | constructor({ 16 | status = 500, 17 | name = 'BaseError', 18 | message, 19 | type = 'base_error', 20 | errors = [], 21 | stack = '' 22 | }: ErrorType) { 23 | super(message); 24 | 25 | this.stack = stack; 26 | this.name = name; 27 | this.status = status; 28 | this.type = type; 29 | this.errors = errors; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/errors/InternalError.ts: -------------------------------------------------------------------------------- 1 | import BaseError, { ErrorType } from './BaseError'; 2 | 3 | export default class InternalError extends BaseError { 4 | constructor({ 5 | status = 500, 6 | name = 'InternalError', 7 | message, 8 | type = 'internal', 9 | errors = [] 10 | }: ErrorType) { 11 | super({ message }); 12 | 13 | this.name = name; 14 | this.status = status; 15 | this.type = type; 16 | this.errors = errors; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/errors/NotFoundError.ts: -------------------------------------------------------------------------------- 1 | import BaseError, { ErrorType } from './BaseError'; 2 | 3 | export default class NotFoundError extends BaseError { 4 | constructor({ 5 | status = 404, 6 | name = 'NotFoundError', 7 | message, 8 | type = 'not_found', 9 | errors = [] 10 | }: ErrorType) { 11 | super({ message }); 12 | 13 | this.name = name; 14 | this.status = status; 15 | this.type = type; 16 | this.errors = errors; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/functions/check-is-numeric/index.spec.ts: -------------------------------------------------------------------------------- 1 | import { checkIsNumeric } from '.'; 2 | 3 | describe('checkIsNumeric', () => { 4 | it('should return true when a string passed contains numbers', () => { 5 | const stringParams = '123'; 6 | 7 | expect(checkIsNumeric(stringParams)).toEqual(true); 8 | }); 9 | 10 | it('should return false when a string passed not contains numbers', () => { 11 | const stringParams = 'asasasas'; 12 | 13 | expect(checkIsNumeric(stringParams)).toEqual(false); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /src/functions/check-is-numeric/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * @param value string 4 | * @returns boolean 5 | */ 6 | export const checkIsNumeric = (value: string): boolean => { 7 | return /^-?\d+$/.test(value); 8 | }; 9 | -------------------------------------------------------------------------------- /src/functions/check-object-is-empty/index.spec.ts: -------------------------------------------------------------------------------- 1 | import { checkObjectIsEmpty } from '.'; 2 | 3 | describe('checkObjectIsEmpty', () => { 4 | it('should return true when object is empty', () => { 5 | const object = {}; 6 | 7 | expect(checkObjectIsEmpty(object)).toEqual(true); 8 | }); 9 | 10 | it('should return false when object is not empty', () => { 11 | const object = { 12 | name: 'name' 13 | }; 14 | 15 | expect(checkObjectIsEmpty(object)).toEqual(false); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /src/functions/check-object-is-empty/index.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @typescript-eslint/ban-types 2 | export const checkObjectIsEmpty = (object: object) => { 3 | return Object.keys(object).length === 0; 4 | }; 5 | -------------------------------------------------------------------------------- /src/functions/check-user-authenticated/index.spec.ts: -------------------------------------------------------------------------------- 1 | import { APP_KEY } from 'utils/localStorage'; 2 | 3 | import { checkUserAuthenticated } from '.'; 4 | 5 | beforeEach(() => { 6 | window.localStorage.clear(); 7 | }); 8 | 9 | describe('checkUserAuthenticated', () => { 10 | it('should return true when user is authenticated', () => { 11 | window.localStorage.setItem( 12 | `${APP_KEY}_${process.env.NEXT_PUBLIC_USER_TOKEN}`, 13 | JSON.stringify({ token: 'token' }) 14 | ); 15 | 16 | expect(checkUserAuthenticated()).toEqual(true); 17 | }); 18 | 19 | it('should return false when user is not authenticated', () => { 20 | expect(checkUserAuthenticated()).toEqual(false); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /src/functions/check-user-authenticated/index.ts: -------------------------------------------------------------------------------- 1 | import { getStorageItem } from 'utils'; 2 | 3 | export const checkUserAuthenticated = () => { 4 | const userToken = getStorageItem(process.env.NEXT_PUBLIC_USER_TOKEN); 5 | 6 | return !!userToken; 7 | }; 8 | -------------------------------------------------------------------------------- /src/functions/index.ts: -------------------------------------------------------------------------------- 1 | export * from './check-object-is-empty'; 2 | export * from './check-is-numeric/index.spec'; 3 | export * from './check-user-authenticated'; 4 | -------------------------------------------------------------------------------- /src/middlewares/api.middleware.ts: -------------------------------------------------------------------------------- 1 | import type { NextApiRequest, NextApiResponse } from 'next'; 2 | import { createRouter } from 'next-connect'; 3 | 4 | import cors from 'cors'; 5 | import cache from 'middlewares/cache.middleware'; 6 | import onError from 'middlewares/errorHandler.middleware'; 7 | import logger from 'middlewares/logger.middleware'; 8 | 9 | const corsDefaultConfiguration = { 10 | origin: '*', 11 | methods: 'GET,HEAD,PUT,PATCH,POST,DELETE', 12 | preflightContinue: false, 13 | optionsSuccessStatus: 204 14 | }; 15 | 16 | const cacheDefaultConfiguration = 86400; 17 | 18 | const onNoMatch = (request: NextApiRequest, response: NextApiResponse) => { 19 | return response.status(404).json({ 20 | message: 'Page not found.', 21 | type: 'not_found', 22 | name: 'NotFoundError' 23 | }); 24 | }; 25 | 26 | export const handler = { 27 | onError, 28 | onNoMatch 29 | }; 30 | 31 | export default ( 32 | options: { 33 | cors?: cors.CorsOptions; 34 | cache?: number; 35 | } = {} 36 | ) => { 37 | const corsOptions = options.cors || {}; 38 | const cacheOptions = options.cache || cacheDefaultConfiguration; 39 | 40 | const configurations = { 41 | cors: { 42 | ...corsDefaultConfiguration, 43 | ...corsOptions 44 | }, 45 | cache: cacheOptions 46 | }; 47 | 48 | const nc = createRouter(); 49 | return nc 50 | .use(cors(configurations.cors)) 51 | .use(logger) 52 | .use(cache(configurations.cache)); 53 | }; 54 | -------------------------------------------------------------------------------- /src/middlewares/cache.middleware.ts: -------------------------------------------------------------------------------- 1 | // max-age especifica quanto tempo o browser deve manter o valor em cache, em segundos. 2 | // s-maxage é uma header lida pelo servidor proxy (neste caso, Vercel). 3 | // stale-while-revalidate indica que o conteúdo da cache pode ser servido como "stale" e revalidado no background 4 | // 5 | // Por que os valores abaixo? 6 | // 7 | // 1. O cache da Now é muito rápido e permite respostas em cerca de 10ms. O valor de 8 | // um dia (86400 segundos) é suficiente para garantir performance e também que as 9 | // respostas estejam relativamente sincronizadas caso o governo decida atualizar os CEPs. 10 | // 2. O cache da Now é invalidado toda vez que um novo deploy é feito, garantindo que 11 | // todas as novas requisições sejam servidas pela implementação mais recente da API. 12 | // 3. Não há browser caching pois este tipo de API normalmente é utilizada uma vez só 13 | // por um usuário. Se fizessemos caching, os valores ficariam lá guardados no browser 14 | // sem necessidade, só ocupando espaço em disco. A história seria diferente se a API 15 | // servisse fotos dos usuários, por exemplo. Além disso teríamos problemas com 16 | // stale/out-of-date cache caso alterássemos a implementação da API. 17 | import { NextApiRequest, NextApiResponse } from 'next'; 18 | 19 | export default (time: number) => 20 | (request: NextApiRequest, response: NextApiResponse, next: () => void) => { 21 | const CACHE_CONTROL_HEADER_VALUE = `max-age=0, s-maxage=${time}, stale-while-revalidate, public`; 22 | 23 | response.setHeader('Cache-Control', CACHE_CONTROL_HEADER_VALUE); 24 | 25 | next(); 26 | }; 27 | -------------------------------------------------------------------------------- /src/middlewares/errorHandler.middleware.ts: -------------------------------------------------------------------------------- 1 | import { NextApiRequest, NextApiResponse } from 'next'; 2 | 3 | import BaseError from 'errors/BaseError'; 4 | 5 | export default function errorHandler( 6 | error: any, // eslint-disable-line @typescript-eslint/no-explicit-any 7 | request: NextApiRequest, 8 | response: NextApiResponse 9 | ) { 10 | console.log({ 11 | url: request.url, 12 | ...error 13 | }); 14 | 15 | if (error instanceof BaseError) { 16 | const errorResponse = { 17 | message: error.message, 18 | type: error.type, 19 | name: error.name, 20 | errors: error.errors 21 | }; 22 | 23 | if (error.errors.length !== 0) { 24 | errorResponse.errors = error.errors; 25 | } 26 | 27 | return response.status(error.status).json(errorResponse); 28 | } 29 | 30 | return response.status(500).json(error); 31 | } 32 | -------------------------------------------------------------------------------- /src/middlewares/logger.middleware.ts: -------------------------------------------------------------------------------- 1 | import { NextApiRequest, NextApiResponse } from 'next'; 2 | 3 | export default function logger( 4 | request: NextApiRequest, 5 | response: NextApiResponse, 6 | next: () => void 7 | ) { 8 | const clientIp = 9 | request.headers['x-forwarded-for'] || request.socket.remoteAddress; 10 | 11 | console.log({ 12 | url: request.url, 13 | clientIp 14 | }); 15 | 16 | return next(); 17 | } 18 | -------------------------------------------------------------------------------- /src/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import { AppProps } from 'next/app'; 2 | import Head from 'next/head'; 3 | 4 | import { ThemeProvider } from 'styled-components'; 5 | 6 | import GlobalStyles from 'styles/global'; 7 | import theme from 'styles/theme'; 8 | 9 | function App({ Component, pageProps }: AppProps) { 10 | return ( 11 | 12 | 13 | Project 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | ); 23 | } 24 | 25 | export default App; 26 | -------------------------------------------------------------------------------- /src/pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import Document, { 2 | Html, 3 | Head, 4 | Main, 5 | NextScript, 6 | DocumentContext 7 | } from 'next/document'; 8 | 9 | import { ServerStyleSheet } from 'styled-components'; 10 | 11 | export default class MyDocument extends Document { 12 | static async getInitialProps(ctx: DocumentContext) { 13 | const sheet = new ServerStyleSheet(); 14 | const originalRenderPage = ctx.renderPage; 15 | 16 | try { 17 | ctx.renderPage = () => 18 | originalRenderPage({ 19 | enhanceApp: (App) => (props) => 20 | sheet.collectStyles() 21 | }); 22 | 23 | const initialProps = await Document.getInitialProps(ctx); 24 | return { 25 | ...initialProps, 26 | styles: [ 27 | <> 28 | {initialProps.styles} 29 | {sheet.getStyleElement()} 30 | 31 | ] 32 | }; 33 | } finally { 34 | sheet.seal(); 35 | } 36 | } 37 | 38 | render() { 39 | return ( 40 | 41 | 42 | 43 |
44 | 45 | 46 | 47 | ); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/pages/about.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link'; 2 | 3 | import { Main } from 'components/Main'; 4 | 5 | export default function AboutPage() { 6 | return ( 7 |
8 |

About Page

9 | 10 | Dashboard 11 |
12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /src/pages/api/hello.ts: -------------------------------------------------------------------------------- 1 | import { NextApiRequest, NextApiResponse } from 'next/types'; 2 | 3 | import app, { handler } from 'middlewares/api.middleware'; 4 | 5 | async function get(req: NextApiRequest, res: NextApiResponse) { 6 | try { 7 | res.status(200).json({ 8 | message: 'Hello World' 9 | }); 10 | } catch (error: unknown) { 11 | console.log(error); 12 | res.status(400).json({ error: error }); 13 | } 14 | } 15 | 16 | export default app().get(get).handler(handler); 17 | -------------------------------------------------------------------------------- /src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import Dashboard from 'templates/Dashboard'; 2 | 3 | export default function Home() { 4 | return ( 5 | <> 6 | 7 | 8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /src/services/api.ts: -------------------------------------------------------------------------------- 1 | import axios, { AxiosInstance, AxiosRequestConfig } from 'axios'; 2 | 3 | import { NODE_ENV, uri } from 'constants/environment-variables'; 4 | 5 | const axiosiInstance = axios.create({ 6 | baseURL: uri[NODE_ENV] 7 | }); 8 | 9 | const api = (axios: AxiosInstance) => { 10 | return { 11 | get: function (url: string, config: AxiosRequestConfig = {}) { 12 | return axios.get(url, config); 13 | }, 14 | put: function ( 15 | url: string, 16 | body: unknown, 17 | config: AxiosRequestConfig = {} 18 | ) { 19 | return axios.put(url, body, config); 20 | }, 21 | post: function ( 22 | url: string, 23 | body: unknown, 24 | config: AxiosRequestConfig = {} 25 | ) { 26 | return axios.post(url, body, config); 27 | }, 28 | delete: function (url: string, config: AxiosRequestConfig = {}) { 29 | return axios.delete(url, config); 30 | } 31 | }; 32 | }; 33 | 34 | export default api(axiosiInstance); 35 | -------------------------------------------------------------------------------- /src/styles/global.ts: -------------------------------------------------------------------------------- 1 | import { createGlobalStyle, css } from 'styled-components'; 2 | 3 | const GlobalStyles = createGlobalStyle` 4 | @font-face { 5 | font-family: 'Montserrat'; 6 | font-style: normal; 7 | font-weight: 300; 8 | font-display: swap; 9 | src: local(''), 10 | url('/fonts/montserrat-v15-latin-300.woff2') format('woff2'); 11 | } 12 | 13 | @font-face { 14 | font-family: 'Montserrat'; 15 | font-style: normal; 16 | font-weight: 400; 17 | font-display: swap; 18 | src: local(''), 19 | url('/fonts/montserrat-v15-latin-regular.woff2') format('woff2'); 20 | } 21 | 22 | @font-face { 23 | font-family: 'Montserrat'; 24 | font-style: normal; 25 | font-weight: 600; 26 | font-display: swap; 27 | src: local(''), 28 | url('/fonts/montserrat-v15-latin-600.woff2') format('woff2'); 29 | } 30 | 31 | * { 32 | margin: 0; 33 | padding: 0; 34 | box-sizing: border-box; 35 | outline: none; 36 | -webkit-font-smoothing: antialiased; 37 | -moz-osx-font-smoothing: grayscale; 38 | } 39 | 40 | ${({ theme }) => css` 41 | html { 42 | font-size: 62.5%; 43 | } 44 | 45 | body { 46 | font-size: ${theme.font.sizes.medium}; 47 | background-color: ${theme.colors.gray_50}; 48 | } 49 | 50 | body, 51 | input, 52 | textarea, 53 | button { 54 | font-family: ${theme.font.family}; 55 | } 56 | `} 57 | 58 | button { 59 | cursor: pointer; 60 | } 61 | 62 | a { 63 | color: inherit; 64 | text-decoration: none; 65 | } 66 | `; 67 | 68 | export default GlobalStyles; 69 | -------------------------------------------------------------------------------- /src/styles/theme.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | grid: { 3 | container: '130rem', 4 | gutter: '3.2rem' 5 | }, 6 | border: { 7 | radius: '0.4rem' 8 | }, 9 | box: { 10 | shadow: '0 8px 8px rgba(0, 0, 0, 0.08)' 11 | }, 12 | font: { 13 | family: 14 | "Montserrat, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif", 15 | light: 300, 16 | normal: 400, 17 | bold: 600, 18 | sizes: { 19 | xxxxsmall: '0.8rem', 20 | xxxsmall: '1.2rem', 21 | xxsmall: '1.4rem', 22 | xsmall: '1.6rem', 23 | small: '2rem', 24 | medium: '2.4rem', 25 | large: '3.2rem', 26 | xlarge: '4rem', 27 | xxlarge: '4.8rem', 28 | xxxlarge: '5.6rem', 29 | huge: '6.4rem' 30 | } 31 | }, 32 | colors: { 33 | white: '#fff', 34 | gray_50: '#f0efeb', 35 | gray_300: '#343a40', 36 | yellow_300: '#F1C40F', 37 | purple_100: '#7F57DF', 38 | purple_300: '#5855E9', 39 | red_300: '#FF6666', 40 | black: '#0D0D0D' 41 | }, 42 | spacings: { 43 | xxxsmall: '0.8rem', 44 | xxsmall: '1rem', 45 | xsmall: '1.6rem', 46 | small: '2.4rem', 47 | medium: '3.2rem', 48 | large: '4.0rem', 49 | xlarge: '4.8rem', 50 | xxlarge: '5.6rem' 51 | }, 52 | layers: { 53 | base: 10, 54 | menu: 20, 55 | overlay: 30, 56 | modal: 40, 57 | alwaysOnTop: 50 58 | }, 59 | transition: { 60 | default: '0.3s ease-in-out', 61 | fast: '0.1s ease-in-out' 62 | } 63 | } as const; 64 | -------------------------------------------------------------------------------- /src/templates/Dashboard/index.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link'; 2 | 3 | import { Main } from 'components/Main/'; 4 | 5 | import * as S from './styles'; 6 | 7 | const Dashboard = () => { 8 | return ( 9 |
10 | 11 | Link: About 12 | 13 |
14 | ); 15 | }; 16 | 17 | export default Dashboard; 18 | -------------------------------------------------------------------------------- /src/templates/Dashboard/styles.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const Container = styled.div``; 4 | -------------------------------------------------------------------------------- /src/utils/delay/index.ts: -------------------------------------------------------------------------------- 1 | export const delay = (milliseconds: number, fn: () => void) => { 2 | setTimeout(() => { 3 | fn(); 4 | }, milliseconds); 5 | }; 6 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './localStorage'; 2 | export * from './delay'; 3 | export * from './tests/__mocks__'; 4 | export { renderWithTheme } from './tests/__config__/helpers'; 5 | -------------------------------------------------------------------------------- /src/utils/localStorage/index.spec.ts: -------------------------------------------------------------------------------- 1 | import { getStorageItem, setStorageItem, APP_KEY } from '.'; 2 | 3 | beforeEach(() => { 4 | window.localStorage.clear(); 5 | }); 6 | 7 | describe('getStorageItem()', () => { 8 | it('should return the item from localStorage', () => { 9 | window.localStorage.setItem( 10 | `${APP_KEY}_cartItems`, 11 | JSON.stringify(['1', '2']) 12 | ); 13 | 14 | expect(getStorageItem('cartItems')).toStrictEqual(['1', '2']); 15 | }); 16 | }); 17 | 18 | describe('setStorageItem()', () => { 19 | it('should add the item to localStorage', () => { 20 | setStorageItem('cartItems', ['1', '2']); 21 | 22 | expect(window.localStorage.getItem(`${APP_KEY}_cartItems`)).toStrictEqual( 23 | JSON.stringify(['1', '2']) 24 | ); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /src/utils/localStorage/index.ts: -------------------------------------------------------------------------------- 1 | export const APP_KEY = 'APPKEY'; 2 | 3 | export function getStorageItem(key: string) { 4 | if (typeof window === 'undefined') return; 5 | 6 | const data = window.localStorage.getItem(`${APP_KEY}_${key}`); 7 | 8 | return JSON.parse(data!); 9 | } 10 | 11 | export function setStorageItem(key: string, value: unknown) { 12 | if (typeof window === 'undefined') return; 13 | 14 | const data = JSON.stringify(value); 15 | 16 | return window.localStorage.setItem(`${APP_KEY}_${key}`, data); 17 | } 18 | -------------------------------------------------------------------------------- /src/utils/tests/__config__/helpers/index.tsx: -------------------------------------------------------------------------------- 1 | import { render, RenderResult } from '@testing-library/react'; 2 | import { ThemeProvider } from 'styled-components'; 3 | 4 | import theme from 'styles/theme'; 5 | 6 | export const renderWithTheme = (children: React.ReactNode): RenderResult => 7 | render({children}); 8 | -------------------------------------------------------------------------------- /src/utils/tests/__mocks__/generateArray/index.ts: -------------------------------------------------------------------------------- 1 | import { faker } from '@faker-js/faker'; 2 | 3 | function generateArray() { 4 | return faker.random 5 | .word() 6 | .split('') 7 | .map(() => ({ 8 | id: faker.datatype.uuid(), 9 | name: faker.random.word() 10 | })); 11 | } 12 | 13 | export default generateArray; 14 | -------------------------------------------------------------------------------- /src/utils/tests/__mocks__/generateRandomDate/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Returns always a Date greather than the current date 3 | * @returns Date 4 | */ 5 | function generateRandomDate(): Date { 6 | const currentDate = Date.now(); 7 | const timestamp = Math.floor(Math.random() * currentDate); 8 | 9 | const randomDateGenerated = new Date(timestamp + currentDate); 10 | 11 | return randomDateGenerated; 12 | } 13 | 14 | export default generateRandomDate; 15 | -------------------------------------------------------------------------------- /src/utils/tests/__mocks__/generateRandomNumberFromInterval/index.ts: -------------------------------------------------------------------------------- 1 | type MaxNumberProps = number | undefined | null; 2 | type MinNumberProps = number | undefined | null; 3 | 4 | /** 5 | * I.E: Call function without parameters 6 | * `randomNumber(null, null)` 7 | * 8 | * @param min n value 9 | * @param max must be greater than min 10 | * @returns number 11 | */ 12 | function randomNumberFromInterval( 13 | min: MinNumberProps, 14 | max: MaxNumberProps 15 | ): number { 16 | const maxNumber = max ?? 1; 17 | const minNumber = min ?? 0; 18 | 19 | const number = Math.floor( 20 | Math.random() * (maxNumber - minNumber + 1) + minNumber 21 | ); 22 | 23 | return number; 24 | } 25 | 26 | export default randomNumberFromInterval; 27 | -------------------------------------------------------------------------------- /src/utils/tests/__mocks__/index.ts: -------------------------------------------------------------------------------- 1 | export { default as generateArray } from './generateArray'; 2 | export { default as generateRandomDate } from './generateRandomDate'; 3 | export { default as generateRandomNumberFromInterval } from './generateRandomNumberFromInterval'; 4 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "src", 4 | "target": "es5", 5 | "lib": [ 6 | "dom", 7 | "dom.iterable", 8 | "esnext" 9 | ], 10 | "allowJs": true, 11 | "skipLibCheck": true, 12 | "strict": true, 13 | "forceConsistentCasingInFileNames": true, 14 | "noEmit": true, 15 | "esModuleInterop": true, 16 | "module": "esnext", 17 | "moduleResolution": "node", 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "jsx": "preserve", 21 | "incremental": true 22 | }, 23 | "include": [ 24 | "next-env.d.ts", 25 | "**/*.ts", 26 | "**/*.tsx" 27 | ], 28 | "exclude": [ 29 | "node_modules", 30 | "cypress" 31 | ] 32 | } 33 | --------------------------------------------------------------------------------