├── .all-contributorsrc ├── .env.local.example ├── .env.test.example ├── .eslintignore ├── .eslintrc.js ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── config.yml │ ├── feature_request.md │ └── question.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── ci.yml │ └── cypress.yml ├── .gitignore ├── .husky ├── .gitignore └── commit-msg ├── .npmrc ├── .storybook ├── main.js └── preview.js ├── .tool-versions ├── .vscode ├── extensions.json ├── launch.json └── settings.json ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── __tests__ └── setupEnv.ts ├── assets └── screenshot.png ├── babel.config.js ├── build.gradle.kts ├── commitlint.config.js ├── cypress.json ├── cypress ├── .gitignore ├── README.md ├── fixtures │ └── .gitkeep ├── integration │ └── app │ │ └── index.spec.ts ├── plugins │ └── index.ts ├── support │ ├── commands.ts │ ├── console.ts │ ├── index.d.ts │ └── index.ts └── tsconfig.json ├── docker-compose.yml ├── docker ├── Dockerfile ├── docker-entrypoint.sh └── process.yml ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── jest.config.js ├── jest.setup.ts ├── next-env.d.ts ├── next-i18next.config.js ├── next.config.js ├── nginx └── nginx.conf ├── package.json ├── postcss.config.js ├── prettier.config.js ├── public ├── favicon.ico ├── images │ └── .gitkeep └── locales │ ├── de │ └── common.json │ └── en │ └── common.json ├── scripts ├── copyfiles.js └── setup.js ├── src ├── components │ └── page │ │ ├── Page.tsx │ │ └── index.ts ├── hooks │ └── useChangeLanguage.ts ├── layouts │ └── AppLayout.tsx ├── pages │ ├── 404.tsx │ ├── _app.tsx │ ├── _document.tsx │ ├── _error.tsx │ └── index.tsx ├── services │ └── jokes.ts ├── store.ts ├── styles │ ├── Scrollbar.scss │ └── index.scss └── types │ └── next.d.ts ├── stylelint.config.js ├── tailwind.config.js ├── tsconfig.json ├── tsconfig.test.json └── yarn.lock /.all-contributorsrc: -------------------------------------------------------------------------------- 1 | { 2 | "projectName": "nextjs-template", 3 | "projectOwner": "natterstefan", 4 | "repoType": "github", 5 | "repoHost": "https://github.com", 6 | "files": [ 7 | "README.md" 8 | ], 9 | "imageSize": 100, 10 | "commit": false, 11 | "commitConvention": "eslint", 12 | "contributors": [ 13 | { 14 | "login": "barnabasJ", 15 | "name": "Barnabas Jovanovics", 16 | "avatar_url": "https://avatars.githubusercontent.com/u/11669837?v=4", 17 | "profile": "https://github.com/barnabasJ", 18 | "contributions": [ 19 | "ideas" 20 | ] 21 | }, 22 | { 23 | "login": "natterstefan", 24 | "name": "Stefan Natter", 25 | "avatar_url": "https://avatars.githubusercontent.com/u/1043668?v=4", 26 | "profile": "https://natterstefan.me/", 27 | "contributions": [ 28 | "ideas" 29 | ] 30 | } 31 | ], 32 | "contributorsPerLine": 7 33 | } 34 | -------------------------------------------------------------------------------- /.env.local.example: -------------------------------------------------------------------------------- 1 | # SILENCE IS GOLDEN -------------------------------------------------------------------------------- /.env.test.example: -------------------------------------------------------------------------------- 1 | # SILENCE IS GOLDEN -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | build 2 | gradle 3 | node_modules 4 | tmp 5 | version.ts 6 | 7 | # generated files 8 | /graphql/schema.graphql 9 | /graphql/schema.json 10 | __generated__ 11 | 12 | # storybook 13 | storybook-static 14 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: 'eslint-config-ns-ts', 3 | rules: { 4 | /** 5 | * This rule was disabled because of NextJS' Link API. 6 | * 7 | * @see https://github.com/vercel/next.js/issues/5533#issuecomment-573287110 8 | * @see https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/issues/402#issuecomment-368305051 9 | */ 10 | 'jsx-a11y/anchor-is-valid': 0, 11 | }, 12 | overrides: [ 13 | { 14 | files: [ 15 | './src/**/__stories__/*.stories.ts', 16 | './src/**/__stories__/*.stories.tsx', 17 | ], 18 | rules: { 19 | 'global-require': 0, 20 | 'import/no-extraneous-dependencies': 0, 21 | 'max-classes-per-file': 0, 22 | 'no-console': 0, 23 | }, 24 | }, 25 | /** 26 | * exclude jest plugin in cypress files, because test are written 27 | * differently and `expect` is typed different. 28 | */ 29 | { 30 | files: [ 31 | './cypress/**/*.ts', 32 | './cypress/**/*.tsx', 33 | './src/**/__cypress__/*.tsx', 34 | ], 35 | extends: ['plugin:cypress/recommended'], 36 | rules: { 37 | 'jest/expect-expect': 0, 38 | 'jest/valid-expect': 0, 39 | 'jest/valid-expect-in-promise': 0, 40 | }, 41 | }, 42 | ], 43 | } 44 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # Thank you 2 | 3 | github: [natterstefan] 4 | patreon: natterstefan 5 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug Report 🐞 3 | about: Something isn't working as expected? Here is the right place to report. 4 | --- 5 | 6 | 13 | 14 | # Bug Report 15 | 16 | ## Relevant information 17 | 18 | 19 | 20 | ### Your Environment 21 | 22 | - Browser: **\_** 23 | - Browser version: **\_** 24 | - OS: **\_** 25 | - Package name: **\_** 26 | - Package version: **\_** 27 | 28 | #### Steps to reproduce 29 | 30 | 1. Step 1 31 | 2. Step 2 32 | 3. Step 3 33 | 34 | #### Observed Results 35 | 36 | - What happened? This could be a description, log output, etc. 37 | 38 | #### Expected Results 39 | 40 | - What did you expect to happen? 41 | 42 | #### Relevant Code (optional) 43 | 44 | ```js 45 | // TODO(you): code here to reproduce the problem 46 | ``` 47 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Twitter 4 | url: https://twitter.com/natterstefan 5 | about: Get in touch with me on Twitter 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature Request 💡 3 | about: Suggest a new idea. 4 | --- 5 | 6 | 13 | 14 | # Feature Request 15 | 16 | Brief explanation of the feature you have in mind. 17 | 18 | ## Basic example 19 | 20 | If you want you can include a basic code example. Omit this section if it's 21 | not applicable. 22 | 23 | ## Motivation 24 | 25 | Why are you suggesting this? What is the use case for it and what is the 26 | expected outcome? 27 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Question 🤔 3 | about: Usage question or discussion. 4 | --- 5 | 6 | 13 | 14 | # Question 15 | 16 | ## Relevant information 17 | 18 | Provide as much useful information as you can. 19 | 20 | ### Your Environment 21 | 22 | - Browser: **\_** 23 | - Browser version: **\_** 24 | - OS: **\_** 25 | - Package name: **\_** 26 | - Package version: **\_** 27 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 7 | 8 | # TASK 9 | 10 | ## What I did 11 | 12 | ## How to test 13 | 14 | 21 | 22 | ## Screenshots 23 | 24 | 28 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Node CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | if: "! contains(github.event.head_commit.message, '[skip ci]')" 9 | steps: 10 | - uses: actions/checkout@v2 11 | with: 12 | fetch-depth: 1 13 | - uses: actions/setup-node@v2 14 | with: 15 | node-version: 14 16 | - uses: actions/cache@v2 17 | with: 18 | path: node_modules 19 | key: natterstefan-rtk-query-example-${{ hashFiles('**/yarn.lock') }} 20 | - run: yarn 21 | - run: yarn lint 22 | - run: yarn test 23 | -------------------------------------------------------------------------------- /.github/workflows/cypress.yml: -------------------------------------------------------------------------------- 1 | name: Cypress 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | if: "! contains(github.event.head_commit.message, '[skip ci]')" 12 | steps: 13 | - uses: actions/checkout@v2 14 | with: 15 | fetch-depth: 1 16 | - uses: actions/setup-node@v2 17 | with: 18 | node-version: 14 19 | - uses: actions/cache@v2 20 | with: 21 | path: node_modules 22 | key: natterstefan-rtk-query-example-${{ hashFiles('**/yarn.lock') }} 23 | - run: yarn 24 | - run: yarn test:cypress:ci 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | /.pnp 44 | .pnp.js 45 | 46 | # TypeScript v1 declaration files 47 | typings/ 48 | 49 | # TypeScript cache 50 | *.tsbuildinfo 51 | 52 | # Optional npm cache directory 53 | .npm 54 | 55 | # Optional eslint and stylelint cache 56 | .eslintcache 57 | .stylelintcache 58 | 59 | # Optional jest cache 60 | .jest-cache 61 | 62 | # Microbundle cache 63 | .rpt2_cache/ 64 | .rts2_cache_cjs/ 65 | .rts2_cache_es/ 66 | .rts2_cache_umd/ 67 | 68 | # Optional REPL history 69 | .node_repl_history 70 | 71 | # Output of 'npm pack' 72 | *.tgz 73 | 74 | # Yarn Integrity file 75 | .yarn-integrity 76 | 77 | # dotenv environment variables file 78 | .env 79 | .env.test 80 | .env.local 81 | .env.development.local 82 | .env.test.local 83 | .env.production.local 84 | 85 | # parcel-bundler cache (https://parceljs.org/) 86 | .cache 87 | 88 | # Next.js build output 89 | .next 90 | /out/ 91 | 92 | # Nuxt.js build / generate output 93 | .nuxt 94 | dist 95 | 96 | # Gatsby files 97 | .cache/ 98 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 99 | # https://nextjs.org/blog/next-9-1#public-directory-support 100 | # public 101 | 102 | # vuepress build output 103 | .vuepress/dist 104 | 105 | # Serverless directories 106 | .serverless/ 107 | 108 | # FuseBox cache 109 | .fusebox/ 110 | 111 | # DynamoDB Local files 112 | .dynamodb/ 113 | 114 | # TernJS port file 115 | .tern-port 116 | 117 | # vercel 118 | .vercel 119 | 120 | # production 121 | /build 122 | 123 | # misc 124 | .DS_Store 125 | *.pem 126 | tmp 127 | 128 | # build-files 129 | /version.js 130 | /version.ts 131 | 132 | # Gradle 133 | .gradle 134 | 135 | # storybook 136 | storybook-static -------------------------------------------------------------------------------- /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | yarn commitlint --edit "$1" 5 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | # Enable when you fork this repository and use it for your apps. 2 | # save-exact=true -------------------------------------------------------------------------------- /.storybook/main.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | module.exports = { 4 | /** 5 | * Storybook uses Webpack@4 by default, but we require Webpack@5 already. 6 | * @see https://storybook.js.org/blog/storybook-for-webpack-5/ 7 | * @see https://gist.github.com/shilman/8856ea1786dcd247139b47b270912324#upgrade 8 | */ 9 | core: { 10 | builder: 'webpack5', 11 | }, 12 | stories: ['../src/**/*.stories.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'], 13 | addons: [ 14 | '@storybook/addon-links', 15 | '@storybook/addon-essentials', 16 | { 17 | /** 18 | * NOTE: fix Storybook issue with PostCSS@8 19 | * @see https://github.com/storybookjs/storybook/issues/12668#issuecomment-773958085 20 | */ 21 | name: '@storybook/addon-postcss', 22 | options: { 23 | postcssLoaderOptions: { 24 | implementation: require('postcss'), 25 | }, 26 | }, 27 | }, 28 | ], 29 | webpackFinal: config => { 30 | /** 31 | * Add *.scss support 32 | * @see https://nebulab.it/blog/nextjs-tailwind-storybook/ 33 | */ 34 | config.module.rules.push({ 35 | test: /\.scss$/, 36 | use: ['style-loader', 'css-loader', 'postcss-loader', 'sass-loader'], 37 | }) 38 | 39 | /** 40 | * Add support for alias-imports 41 | * @see https://github.com/storybookjs/storybook/issues/11989#issuecomment-715524391 42 | */ 43 | config.resolve.alias = { 44 | ...config.resolve?.alias, 45 | '@': [path.resolve(__dirname, '../src/'), path.resolve(__dirname, '../')], 46 | } 47 | return config 48 | }, 49 | } 50 | -------------------------------------------------------------------------------- /.storybook/preview.js: -------------------------------------------------------------------------------- 1 | import { withNextRouter } from 'storybook-addon-next-router' 2 | 3 | import '../src/styles/index.scss' 4 | 5 | // @see https://www.npmjs.com/package/storybook-addon-next-router 6 | export const decorators = [ 7 | withNextRouter({ 8 | path: '/', // defaults to `/` 9 | asPath: '/', // defaults to `/` 10 | query: {}, // defaults to `{}` 11 | push() {}, // defaults to using addon actions integration, can override any method in the router 12 | }), 13 | ] 14 | 15 | export const parameters = { 16 | actions: { argTypesRegex: '^on[A-Z].*' }, 17 | controls: { 18 | matchers: { 19 | color: /(background|color)$/i, 20 | date: /Date$/, 21 | }, 22 | }, 23 | } 24 | -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | nodejs 14.15.4 2 | yarn 1.22.10 -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "apollographql.vscode-apollo", 4 | "bradlc.vscode-tailwindcss", 5 | "dbaeumer.vscode-eslint", 6 | "eamodio.gitlens", 7 | "esbenp.prettier-vscode", 8 | "firsttris.vscode-jest-runner", 9 | "heybourn.headwind", 10 | "johnpapa.vscode-peacock", 11 | "pflannery.vscode-versionlens", 12 | "stylelint.vscode-stylelint", 13 | "syler.sass-indented", 14 | "wayou.vscode-todo-highlight", 15 | "ziyasal.vscode-open-in-github", 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "type": "node", 6 | "request": "launch", 7 | "name": "Jest: Test Current File", 8 | "program": "${workspaceFolder}/node_modules/.bin/jest", 9 | "args": ["${file}", "--detectOpenHandles"], 10 | "console": "integratedTerminal", 11 | "internalConsoleOptions": "neverOpen", 12 | "disableOptimisticBPs": true, 13 | "windows": { 14 | "program": "${workspaceFolder}/node_modules/jest/bin/jest" 15 | } 16 | }, 17 | { 18 | "type": "node", 19 | "request": "launch", 20 | "name": "Jest: Test Current File in Watch mode", 21 | "program": "${workspaceFolder}/node_modules/.bin/jest", 22 | "args": ["${file}", "--detectOpenHandles", "--watch"], 23 | "console": "integratedTerminal", 24 | "internalConsoleOptions": "neverOpen", 25 | "disableOptimisticBPs": true, 26 | "windows": { 27 | "program": "${workspaceFolder}/node_modules/jest/bin/jest" 28 | } 29 | }, 30 | { 31 | "type": "node", 32 | "request": "launch", 33 | "name": "Jest: Run all Tests", 34 | "program": "${workspaceFolder}/node_modules/.bin/jest", 35 | "args": ["--runInBand", "--detectOpenHandles"], 36 | "console": "integratedTerminal", 37 | "internalConsoleOptions": "neverOpen", 38 | "disableOptimisticBPs": true, 39 | "windows": { 40 | "program": "${workspaceFolder}/node_modules/jest/bin/jest" 41 | } 42 | }, 43 | { 44 | "type": "node", 45 | "request": "launch", 46 | "name": "Jest: Debug all Tests", 47 | "runtimeArgs": [ 48 | "--inspect-brk", 49 | "${workspaceRoot}/node_modules/.bin/jest", 50 | "--runInBand", 51 | "--detectOpenHandles" 52 | ], 53 | "console": "integratedTerminal", 54 | "internalConsoleOptions": "neverOpen", 55 | "port": 9229 56 | }, 57 | { 58 | "type": "node", 59 | "request": "attach", 60 | "name": "Attach by Process ID", 61 | "processId": "${command:PickProcess}" 62 | } 63 | ] 64 | } 65 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | /** 3 | * Disable CSS validation in VS Code, to allow @tailwind import and the @apply 4 | * syntax in VS Code. 5 | * 6 | * Enable stylelint to ensure CSS is still validated though. 7 | * 8 | * Docs: 9 | * - @see https://stackoverflow.com/a/62093553/1238150 10 | * - @see https://github.com/microsoft/vscode/issues/103163#issuecomment-672272020 11 | * - @see https://www.meidev.co/blog/visual-studio-code-css-linting-with-tailwind/ 12 | */ 13 | "css.validate": false, 14 | "less.validate": false, 15 | "scss.validate": false, 16 | "stylelint.enable": true, 17 | /** 18 | * LINTING & FORMATTING 19 | */ 20 | "editor.formatOnSave": true, 21 | "editor.codeActionsOnSave": { 22 | "source.fixAll.eslint": true, 23 | }, 24 | "eslint.validate": [ 25 | "graphql", 26 | "javascript", 27 | "javascriptreact", 28 | "typescript", 29 | "typescriptreact" 30 | ], 31 | "eslint.options": { 32 | "extentions": [ 33 | ".d.ts", 34 | ".gql", 35 | ".graphql", 36 | ".js", 37 | ".jsx", 38 | ".ts", 39 | ".tsx" 40 | ] 41 | }, 42 | /** 43 | * EDITOR UX 44 | */ 45 | "editor.rulers": [ 46 | 80, 47 | ], 48 | "editor.tabSize": 2, 49 | "editor.insertSpaces": true, 50 | "files.eol": "\n", 51 | /** 52 | * PLUGINS SETUP 53 | */ 54 | "todohighlight.keywords": [ 55 | // inspired by: https://github.com/wayou/vscode-todo-highlight/issues/93#issuecomment-390368341 56 | { 57 | "text": "NOTE:", 58 | "color": "#ecf0f1", 59 | "border": "1px solid #2980b9", 60 | "borderRadius": "4px", 61 | "backgroundColor": "#3498db", 62 | }, 63 | { 64 | "text": "HACK:", 65 | "color": "#ecf0f1", 66 | "border": "1px solid #8e44ad", 67 | "borderRadius": "4px", 68 | "backgroundColor": "#9b59b6", 69 | }, 70 | { 71 | "text": "XXX", 72 | "color": "#ecf0f1", 73 | "border": "1px solid #f39c12", 74 | "borderRadius": "4px", 75 | "backgroundColor": "#f1c40f", 76 | }, 77 | { 78 | "text": "FIXME:", 79 | "color": "#ecf0f1", 80 | "border": "1px solid #f39c12", 81 | "borderRadius": "4px", 82 | "backgroundColor": "#f1c40f", 83 | }, 84 | { 85 | "text": "ATTENTION:", 86 | "color": "#ecf0f1", 87 | "border": "1px solid #c0392b", 88 | "borderRadius": "4px", 89 | "backgroundColor": "#e74c3c", 90 | }, 91 | { 92 | "text": "BUG:", 93 | "color": "#ecf0f1", 94 | "border": "1px solid #c0392b", 95 | "borderRadius": "4px", 96 | "backgroundColor": "#e74c3c", 97 | } 98 | ], 99 | /** 100 | * TYPESCRIPT SETTINGS 101 | */ 102 | "typescript.implementationsCodeLens.enabled": true, 103 | "typescript.referencesCodeLens.enabled": true, 104 | "typescript.tsdk": "./node_modules/typescript/lib", 105 | /** 106 | * Peacock for Visual Studio Code 107 | * 108 | * Change the color of the workspace to quickly identify template repositories. 109 | * @see https://www.peacockcode.dev/guide/#overview 110 | */ 111 | "workbench.colorCustomizations": { 112 | "activityBar.activeBackground": "#2f7c47", 113 | "activityBar.activeBorder": "#422c74", 114 | "activityBar.background": "#2f7c47", 115 | "activityBar.foreground": "#e7e7e7", 116 | "activityBar.inactiveForeground": "#e7e7e799", 117 | "activityBarBadge.background": "#422c74", 118 | "activityBarBadge.foreground": "#e7e7e7", 119 | "statusBar.background": "#215732", 120 | "statusBar.foreground": "#e7e7e7", 121 | "statusBarItem.hoverBackground": "#2f7c47", 122 | "titleBar.activeBackground": "#215732", 123 | "titleBar.activeForeground": "#e7e7e7", 124 | "titleBar.inactiveBackground": "#21573299", 125 | "titleBar.inactiveForeground": "#e7e7e799" 126 | }, 127 | "peacock.color": "#215732", 128 | } -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | https://natterstefan.me. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Stefan Natter 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # nextjs-rtk-query-example 2 | 3 | [![Node CI](https://github.com/natterstefan/nextjs-rtk-query-example/actions/workflows/ci.yml/badge.svg)](https://github.com/natterstefan/nextjs-rtk-query-example/actions/workflows/ci.yml) 4 | [![GitHub issues](https://img.shields.io/github/issues/natterstefan/nextjs-rtk-query-example)](https://github.com/natterstefan/nextjs-rtk-query-example/issues) 5 | [![GitHub stars](https://img.shields.io/github/stars/natterstefan/nextjs-rtk-query-example)](https://github.com/natterstefan/nextjs-rtk-query-example/stargazers) 6 | [![GitHub license](https://img.shields.io/github/license/natterstefan/nextjs-rtk-query-example)](https://github.com/natterstefan/nextjs-rtk-query-example/blob/main/LICENSE) 7 | [![nextjs](https://img.shields.io/badge/nextjs-built%20with%20typescript-informational.svg?logo=typescript&logoWidth=20)](https://github.com/natterstefan/nextjs-rtk-query-example) 8 | 9 | ![[natterstefan/nextjs-template](https://github.com/natterstefan/nextjs-rtk-query-example)](./assets/screenshot.png) 10 | 11 | Next.js with rtk-query example. 12 | 13 | ## Installation & Setup 14 | 15 | ```bash 16 | yarn 17 | ``` 18 | 19 | Once the task is completed you can start the app with `yarn dev`. 20 | 21 | ## Start 22 | 23 | ```bash 24 | # development mode 25 | yarn dev 26 | 27 | # production mode 28 | yarn build 29 | yarn start 30 | ``` 31 | 32 | ## Development 33 | 34 | ### Tests & Code Style 35 | 36 | ```bash 37 | yarn test 38 | yarn lint 39 | ``` 40 | 41 | ## Deployment 42 | 43 | ### Deploy your own 44 | 45 | Deploy `nextjs-rtk-query-example` using [Vercel](https://vercel.com): 46 | 47 | [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/import/project?template=https://github.com/natterstefan/nextjs-rtk-query-example) 48 | 49 | ### Docker 50 | 51 | `nextjs-rtk-query-example` also provides a Docker Image (see [docker](./docker)) and 52 | docker-compose setup. 53 | 54 | #### Build Docker Image 55 | 56 | ```bash 57 | ./gradlew buildDockerImage 58 | ``` 59 | 60 | #### Run Docker Image 61 | 62 | ```bash 63 | ./gradlew runDockerBuild 64 | ``` 65 | 66 | Or when you want to run it in the background: 67 | 68 | ```bash 69 | docker-compose --env-file ./.env.local up -d 70 | ``` 71 | 72 | ## License 73 | 74 | [MIT](./LICENSE) 75 | 76 | ## Contributors ✨ 77 | 78 | Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)): 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 |

Barnabas Jovanovics

🤔

Stefan Natter

🤔
89 | 90 | 91 | 92 | 93 | 94 | 95 | This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome! 96 | 97 | --- 98 | 99 | _Based on the template._ 100 | -------------------------------------------------------------------------------- /__tests__/setupEnv.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies */ 2 | /** 3 | * FIX: '.env.test' being loaded properly 4 | * @see https://github.com/vercel/next.js/issues/17903#issuecomment-733994841 5 | */ 6 | 7 | import { resolve } from 'path' 8 | 9 | import { loadEnvConfig } from '@next/env' 10 | 11 | export default async () => { 12 | const pwd = process.env.PWD || resolve(__dirname, '..') 13 | loadEnvConfig(pwd) 14 | } 15 | -------------------------------------------------------------------------------- /assets/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/natterstefan/nextjs-rtk-query-example/8bd367f7057279f6aa56bd078583db1c7da24183/assets/screenshot.png -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ['next/babel'], 3 | } 4 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | base 3 | id("com.lovelysystems.gradle") version ("1.3.2") 4 | } 5 | 6 | lovely { 7 | gitProject() 8 | dockerProject("natterstefan/nextjs-template") 9 | } 10 | 11 | subprojects { 12 | apply { 13 | plugin("com.lovelysystems.gradle") 14 | } 15 | } 16 | 17 | val writeVersion by tasks.creating { 18 | val versionFile = file("version.js") 19 | versionFile.writeText("module.exports = '${project.version.toString()}';") 20 | outputs.file(versionFile) 21 | } 22 | 23 | val runDockerBuild by tasks.creating { 24 | group = "Development" 25 | description = "Run Docker Image" 26 | dependsOn("buildDockerImage") 27 | doLast { 28 | exec { 29 | commandLine( 30 | "docker-compose", 31 | "--env-file", 32 | ".env.local", 33 | "-f", 34 | "docker-compose.yml", 35 | "up" 36 | ) 37 | } 38 | } 39 | } 40 | 41 | val dev by tasks.creating { 42 | group = "Development" 43 | description = "Initialize Repository" 44 | dependsOn("writeVersion") 45 | doLast { 46 | exec { 47 | commandLine( 48 | "yarn" 49 | ) 50 | } 51 | } 52 | } 53 | 54 | with(lovely.dockerFiles) { 55 | // and copy all the output into the docker build folder 56 | from(tasks["writeVersion"].outputs) 57 | 58 | // packages 59 | from(".") { 60 | // files required to properly build and run the app in the docker image 61 | include( 62 | "public/**", 63 | "src/**", 64 | ".npmrc", 65 | "babel.config.js", 66 | "next-i18next.config.js", 67 | "next.config.js", 68 | "package.json", 69 | "postcss.config.js", 70 | "tailwind.config.js", 71 | "tsconfig.json", 72 | "yarn.lock" 73 | ) 74 | 75 | // exclude any config, test or generated files 76 | exclude( 77 | "**/__generated__", 78 | "**/*.log", 79 | "**/*.md", 80 | "**/.env", 81 | "**/.env.*", 82 | "**/.eslintcache", 83 | "**/.eslintrc.js", 84 | "**/.prettierignore", 85 | "**/.remarkrc.js", 86 | "**/coverage/**", 87 | "**/jest.config.js", 88 | "**/jest.setup.ts", 89 | "**/node_modules/**", 90 | "**/prettier.config.js", 91 | "**/stats.json", 92 | "**/tmp" 93 | ) 94 | } 95 | 96 | // Dockerfile 97 | from("docker") 98 | } 99 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { extends: ['@commitlint/config-conventional'] } 2 | -------------------------------------------------------------------------------- /cypress.json: -------------------------------------------------------------------------------- 1 | { 2 | "baseUrl": "http://localhost:3000" 3 | } 4 | -------------------------------------------------------------------------------- /cypress/.gitignore: -------------------------------------------------------------------------------- 1 | screenshots 2 | videos -------------------------------------------------------------------------------- /cypress/README.md: -------------------------------------------------------------------------------- 1 | # Cypress 2 | 3 | ## Links 4 | 5 | - [Testing Your App](https://docs.cypress.io/guides/getting-started/testing-your-app) 6 | - [Best Practices](https://docs.cypress.io/guides/references/best-practices) 7 | - [Cypress - TypeScript Deep Dive](https://basarat.gitbook.io/typescript/intro-1/cypress) 8 | - [IDE Integration](https://docs.cypress.io/guides/tooling/IDE-integration#Visual-Studio-Code) 9 | -------------------------------------------------------------------------------- /cypress/fixtures/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/natterstefan/nextjs-rtk-query-example/8bd367f7057279f6aa56bd078583db1c7da24183/cypress/fixtures/.gitkeep -------------------------------------------------------------------------------- /cypress/integration/app/index.spec.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | context('Headline', () => { 4 | beforeEach(() => { 5 | cy.visit('http://localhost:3000') 6 | }) 7 | 8 | it('should contain a link to the English Homepage', () => { 9 | cy.getBySel('title').contains('Random Programming Jokes') 10 | }) 11 | }) 12 | -------------------------------------------------------------------------------- /cypress/plugins/index.ts: -------------------------------------------------------------------------------- 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 | /** 13 | * This function is called when a project is opened or re-opened (e.g. due to 14 | * the project's config changing) 15 | */ 16 | const plugin: Cypress.PluginConfig = (_on, config) => { 17 | // `on` is used to hook into various events Cypress emits 18 | // `config` is the resolved Cypress config 19 | return config 20 | } 21 | 22 | export default plugin 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 | /** 28 | * inspired by 29 | * - @see https://github.com/cypress-io/cypress-realworld-app/blob/7be40079dd77c8fdb19021ded768b099c1441315/cypress/support/commands.ts#L35-L41 30 | * 31 | * TODO: replace with composition commands https://medium.com/@NicholasBoll/cypress-io-scaling-e2e-testing-with-custom-commands-6b72b902aab 32 | */ 33 | Cypress.Commands.add('getBySel', (selector, ...args) => { 34 | return cy.get(`[data-cy=${selector}]`, ...args) 35 | }) 36 | 37 | Cypress.Commands.add('getBySelLike', (selector, ...args) => { 38 | return cy.get(`[data-cy*=${selector}]`, ...args) 39 | }) 40 | -------------------------------------------------------------------------------- /cypress/support/console.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Spy on both error and warn console.logs to detect if an error has been 3 | * written to the console 4 | * 5 | * inspired by 6 | * - @see https://stackoverflow.com/a/63794694/1238150 7 | * 8 | * Docs 9 | * - @see https://docs.cypress.io/faq/questions/using-cypress-faq#How-do-I-spy-on-console-log 10 | * - @see https://github.com/cypress-io/cypress-example-recipes/blob/46900c4c5085f8d2c6094cd644ca842b36315de2/examples/stubbing-spying__window/cypress/integration/spy-before-load.js#L4-L13 11 | */ 12 | Cypress.on('window:before:load', win => { 13 | cy.spy(win.console, 'error') 14 | cy.spy(win.console, 'warn') 15 | }) 16 | -------------------------------------------------------------------------------- /cypress/support/index.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/naming-convention */ 2 | // load type definitions that come with Cypress module 3 | /// 4 | 5 | /** 6 | * TODO: remove and use solution proposed in commands.ts instead 7 | */ 8 | declare namespace Cypress { 9 | interface Chainable { 10 | /** 11 | * Select and add product to Cart 12 | * @example 13 | * cy.selectProduct(productName, size , color) 14 | */ 15 | /** 16 | * Custom command to select DOM element by data-cy attribute. 17 | * @example cy.getBySel('greeting') 18 | */ 19 | getBySel(value: string): Chainable 20 | getBySelLike(value: string): Chainable 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /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' 17 | import './console' 18 | -------------------------------------------------------------------------------- /cypress/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "target": "es5", 5 | "lib": [ 6 | "es5", 7 | "dom" 8 | ], 9 | "types": [ 10 | "cypress", 11 | "node" 12 | ], 13 | "isolatedModules": false 14 | }, 15 | "include": [ 16 | "./**/*.ts", 17 | "./**/*.tsx", 18 | "./**/*.d.ts" 19 | ] 20 | } -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | # 2 | # Next.js Docker Setup with Nginx 3 | # 4 | # inspired by 5 | # @see https://steveholgado.com/nginx-for-nextjs/#adding-pm2-to-our-dockerfile 6 | # 7 | version: "3.8" 8 | 9 | services: 10 | app: 11 | image: natterstefan/nextjs-template:dev 12 | networks: 13 | - nextjs 14 | env_file: 15 | - .env.local 16 | environment: 17 | NODE_ENV: ${NODE_ENV:-production} 18 | nginx: 19 | image: nginx:alpine 20 | volumes: 21 | - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro 22 | ports: 23 | - 8080:80 24 | networks: 25 | - nextjs 26 | 27 | networks: 28 | nextjs: {} 29 | -------------------------------------------------------------------------------- /docker/Dockerfile: -------------------------------------------------------------------------------- 1 | # inspired by https://github.com/vercel/next.js/blob/canary/docs/deployment.md#docker-image 2 | # TODO: deactive https://nextjs.org/docs/api-reference/next.config.js/compression when using nginx 3 | ARG node_version=14.15.4 4 | ARG node_image=node:${node_version}-alpine 5 | ARG pm2_version=4.5.1 6 | 7 | FROM $node_image as devDep 8 | WORKDIR /app/ 9 | COPY .npmrc package.json yarn.lock ./ 10 | RUN yarn install --frozen-lockfile --no-progress 11 | 12 | FROM $node_image as deps 13 | WORKDIR /app/ 14 | COPY .npmrc package.json yarn.lock ./ 15 | RUN yarn install --frozen-lockfile --no-progress --production=true 16 | 17 | # STAGE BUILDER 18 | FROM $node_image as builder 19 | # https://nextjs.org/telemetry 20 | ENV NEXT_TELEMETRY_DISABLED=1 21 | 22 | WORKDIR /app/ 23 | 24 | # package.json and dependency handling 25 | COPY --from=devDep /app/node_modules ./node_modules 26 | COPY package.json ./ 27 | 28 | # files for building the app 29 | COPY babel.config.js tsconfig.json ./ 30 | COPY next-i18next.config.js ./ 31 | COPY next.config.js postcss.config.js tailwind.config.js ./ 32 | 33 | # source code of the app 34 | COPY public ./public/ 35 | COPY src ./src/ 36 | 37 | # build the bundles (codegen -> ts -> js -> bundled) 38 | RUN yarn build 39 | 40 | # STAGE APP 41 | FROM $node_image 42 | LABEL "maintainer"="natterstefan" 43 | 44 | ENV NEXT_TELEMETRY_DISABLED=1 45 | ENV NODE_ENV=production 46 | 47 | WORKDIR /app/ 48 | 49 | # set timezone 50 | RUN apk --no-cache add tzdata 51 | RUN ln -sf /usr/share/zoneinfo/Europe/Vienna /etc/localtime 52 | 53 | RUN npm install pm2@${pm2_version} -g 54 | COPY docker-entrypoint.sh process.yml ./ 55 | 56 | # copy files 57 | COPY --from=deps /app/node_modules ./node_modules 58 | COPY --from=builder /app/package.json ./ 59 | COPY --from=builder /app/next-i18next.config.js ./ 60 | COPY --from=builder /app/next.config.js ./ 61 | COPY --from=builder /app/public ./public 62 | COPY --from=builder /app/.next ./.next 63 | 64 | RUN addgroup -g 1001 -S nodejs 65 | RUN adduser -S nextjs -u 1001 66 | RUN chown -R nextjs:nodejs /app/.next 67 | USER nextjs 68 | 69 | EXPOSE 3000 70 | 71 | ENTRYPOINT ["./docker-entrypoint.sh"] -------------------------------------------------------------------------------- /docker/docker-entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # @see https://pm2.keymetrics.io/docs/usage/docker-pm2-nodejs/ 4 | exec pm2-runtime ./process.yml -------------------------------------------------------------------------------- /docker/process.yml: -------------------------------------------------------------------------------- 1 | # Docs 2 | # @see https://pm2.keymetrics.io/docs/usage/docker-pm2-nodejs/ 3 | # 4 | # ATTENTION: cluster mode does not work 5 | # 6 | # @see https://github.com/vercel/next.js/discussions/10675#discussioncomment-54193 7 | # and discussions above this comment, especially https://nttr.st/3sQDgF8! 8 | # 9 | # NOTE: pm2 start would work, but we can't use it (https://stackoverflow.com/a/60603179/1238150) 10 | apps: 11 | - name: "app" 12 | script: "node_modules/.bin/next" 13 | args: "start" 14 | cwd: "./" 15 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/natterstefan/nextjs-rtk-query-example/8bd367f7057279f6aa56bd078583db1c7da24183/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.6.1-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | # Determine the Java command to use to start the JVM. 86 | if [ -n "$JAVA_HOME" ] ; then 87 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 88 | # IBM's JDK on AIX uses strange locations for the executables 89 | JAVACMD="$JAVA_HOME/jre/sh/java" 90 | else 91 | JAVACMD="$JAVA_HOME/bin/java" 92 | fi 93 | if [ ! -x "$JAVACMD" ] ; then 94 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 95 | 96 | Please set the JAVA_HOME variable in your environment to match the 97 | location of your Java installation." 98 | fi 99 | else 100 | JAVACMD="java" 101 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 102 | 103 | Please set the JAVA_HOME variable in your environment to match the 104 | location of your Java installation." 105 | fi 106 | 107 | # Increase the maximum file descriptors if we can. 108 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 109 | MAX_FD_LIMIT=`ulimit -H -n` 110 | if [ $? -eq 0 ] ; then 111 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 112 | MAX_FD="$MAX_FD_LIMIT" 113 | fi 114 | ulimit -n $MAX_FD 115 | if [ $? -ne 0 ] ; then 116 | warn "Could not set maximum file descriptor limit: $MAX_FD" 117 | fi 118 | else 119 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 120 | fi 121 | fi 122 | 123 | # For Darwin, add options to specify how the application appears in the dock 124 | if $darwin; then 125 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 126 | fi 127 | 128 | # For Cygwin or MSYS, switch paths to Windows format before running java 129 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 130 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 131 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 132 | JAVACMD=`cygpath --unix "$JAVACMD"` 133 | 134 | # We build the pattern for arguments to be converted via cygpath 135 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 136 | SEP="" 137 | for dir in $ROOTDIRSRAW ; do 138 | ROOTDIRS="$ROOTDIRS$SEP$dir" 139 | SEP="|" 140 | done 141 | OURCYGPATTERN="(^($ROOTDIRS))" 142 | # Add a user-defined pattern to the cygpath arguments 143 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 144 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 145 | fi 146 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 147 | i=0 148 | for arg in "$@" ; do 149 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 150 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 151 | 152 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 153 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 154 | else 155 | eval `echo args$i`="\"$arg\"" 156 | fi 157 | i=$((i+1)) 158 | done 159 | case $i in 160 | (0) set -- ;; 161 | (1) set -- "$args0" ;; 162 | (2) set -- "$args0" "$args1" ;; 163 | (3) set -- "$args0" "$args1" "$args2" ;; 164 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 165 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 166 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 167 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 168 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 169 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 170 | esac 171 | fi 172 | 173 | # Escape application args 174 | save () { 175 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 176 | echo " " 177 | } 178 | APP_ARGS=$(save "$@") 179 | 180 | # Collect all arguments for the java command, following the shell quoting and substitution rules 181 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 182 | 183 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 184 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 185 | cd "$(dirname "$0")" 186 | fi 187 | 188 | exec "$JAVACMD" "$@" 189 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 33 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 34 | 35 | @rem Find java.exe 36 | if defined JAVA_HOME goto findJavaFromJavaHome 37 | 38 | set JAVA_EXE=java.exe 39 | %JAVA_EXE% -version >NUL 2>&1 40 | if "%ERRORLEVEL%" == "0" goto init 41 | 42 | echo. 43 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 44 | echo. 45 | echo Please set the JAVA_HOME variable in your environment to match the 46 | echo location of your Java installation. 47 | 48 | goto fail 49 | 50 | :findJavaFromJavaHome 51 | set JAVA_HOME=%JAVA_HOME:"=% 52 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 53 | 54 | if exist "%JAVA_EXE%" goto init 55 | 56 | echo. 57 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 58 | echo. 59 | echo Please set the JAVA_HOME variable in your environment to match the 60 | echo location of your Java installation. 61 | 62 | goto fail 63 | 64 | :init 65 | @rem Get command-line arguments, handling Windows variants 66 | 67 | if not "%OS%" == "Windows_NT" goto win9xME_args 68 | 69 | :win9xME_args 70 | @rem Slurp the command line arguments. 71 | set CMD_LINE_ARGS= 72 | set _SKIP=2 73 | 74 | :win9xME_args_slurp 75 | if "x%~1" == "x" goto execute 76 | 77 | set CMD_LINE_ARGS=%* 78 | 79 | :execute 80 | @rem Setup the command line 81 | 82 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 83 | 84 | @rem Execute Gradle 85 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 86 | 87 | :end 88 | @rem End local scope for the variables with windows NT shell 89 | if "%ERRORLEVEL%"=="0" goto mainEnd 90 | 91 | :fail 92 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 93 | rem the _cmd.exe /c_ return code! 94 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 95 | exit /b 1 96 | 97 | :mainEnd 98 | if "%OS%"=="Windows_NT" endlocal 99 | 100 | :omega 101 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'jest-preset-ns/presets/react', 3 | globals: { 4 | // @see https://github.com/natterstefan/jest-preset-ns/issues/4#issuecomment-765416205 5 | 'ts-jest': { 6 | tsconfig: 'tsconfig.test.json', 7 | }, 8 | }, 9 | globalSetup: '/__tests__/setupEnv.ts', 10 | moduleNameMapper: { 11 | '@/(.*)': ['/$1', '/src/$1'], 12 | }, 13 | setupFilesAfterEnv: ['/jest.setup.ts'], 14 | testMatch: ['/**/__tests__/*.test.ts', '/**/__tests__/*.test.tsx'], 15 | } 16 | -------------------------------------------------------------------------------- /jest.setup.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/ban-ts-comment */ 2 | /* eslint-disable import/no-extraneous-dependencies */ 3 | import 'jest-preset-ns/presets/react/jest-setup.js' 4 | 5 | import { setConfig } from 'next/config' 6 | import { PHASE_DEVELOPMENT_SERVER } from 'next/constants' 7 | import i18n from 'i18next' 8 | import { initReactI18next } from 'react-i18next' 9 | import { NextRouter } from 'next/router' 10 | 11 | // @ts-ignore 12 | import NextConfig from './next.config' 13 | 14 | const { publicRuntimeConfig, serverRuntimeConfig } = NextConfig( 15 | PHASE_DEVELOPMENT_SERVER, 16 | {}, 17 | ) 18 | 19 | /** 20 | * Enables usage of "publicRuntimeConfig" within tests. 21 | * @see https://github.com/vercel/next.js/issues/4024#issuecomment-386016077 22 | */ 23 | setConfig({ publicRuntimeConfig, serverRuntimeConfig }) 24 | 25 | /** 26 | * Setup i18n in tests 27 | * 28 | * @see https://github.com/i18next/react-i18next/blob/552ed79036c28f282afe7c6ccb525b82b76e71d5/example/test-jest/src/setupTests.js#L4-L23 29 | * @see https://github.com/isaachinman/next-i18next/issues/377#issuecomment-700516821 30 | */ 31 | i18n.use(initReactI18next).init({ 32 | lng: 'en', 33 | fallbackLng: 'en', 34 | 35 | // have a common namespace used around the full app 36 | ns: ['translations'], 37 | defaultNS: 'translations', 38 | 39 | // debug: true, 40 | 41 | interpolation: { 42 | escapeValue: false, // not needed for react!! 43 | }, 44 | 45 | resources: { en: { translations: {} } }, 46 | }) 47 | 48 | /** 49 | * mockRouter mocks the initial router state of Next.js 50 | * @see https://github.com/vercel/next.js/issues/7479#issuecomment-752418517 51 | * 52 | * FTR: other solutions like this one did not work: 53 | * @see https://github.com/vercel/next.js/issues/7479#issuecomment-533657701 54 | */ 55 | const mockRouter: NextRouter = { 56 | basePath: '/', 57 | pathname: '/', 58 | route: '/', 59 | query: {}, 60 | asPath: '/', 61 | push: jest.fn(() => Promise.resolve(true)), 62 | replace: jest.fn(() => Promise.resolve(true)), 63 | reload: jest.fn(() => Promise.resolve(true)), 64 | prefetch: jest.fn(() => Promise.resolve()), 65 | back: jest.fn(() => Promise.resolve(true)), 66 | beforePopState: jest.fn(() => Promise.resolve(true)), 67 | isFallback: false, 68 | isLocaleDomain: false, 69 | isPreview: false, 70 | events: { 71 | on: jest.fn(), 72 | off: jest.fn(), 73 | emit: jest.fn(), 74 | }, 75 | isReady: true, 76 | } 77 | 78 | // eslint-disable-next-line @typescript-eslint/no-var-requires 79 | const useRouter = jest.spyOn(require('next/router'), 'useRouter') 80 | 81 | /** 82 | * mockNextUseRouter and mockNextUseRouterOnce must be used in tests where one 83 | * needs to use Router features of Next.js. This can be either done for all 84 | * tests (with `beforeAll`) or on a per test basis. 85 | * 86 | * @see https://github.com/vercel/next.js/issues/7479#issuecomment-587145429 87 | */ 88 | export function mockNextUseRouter(props?: Partial) { 89 | useRouter.mockImplementation(() => ({ 90 | ...mockRouter, 91 | ...props, 92 | })) 93 | } 94 | 95 | export function mockNextUseRouterOnce(props?: Partial) { 96 | useRouter.mockImplementationOnce(() => ({ 97 | ...mockRouter, 98 | ...props, 99 | })) 100 | } 101 | -------------------------------------------------------------------------------- /next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | type NextConfig = { 5 | publicRuntimeConfig: { 6 | NODE_ENV: string 7 | } 8 | serverRuntimeConfig: Record 9 | } 10 | 11 | declare module 'next/config' { 12 | export declare function setConfig(configValue: any): void 13 | export default function getConfig(): NextConfig 14 | } 15 | -------------------------------------------------------------------------------- /next-i18next.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Docs: 3 | * @see https://github.com/isaachinman/next-i18next 4 | * @see https://nextjs.org/docs/advanced-features/i18n-routing 5 | */ 6 | module.exports = { 7 | i18n: { 8 | // ATTENTION: The default locale does not have a prefix. 9 | defaultLocale: 'en', 10 | locales: ['en', 'de'], 11 | }, 12 | } 13 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies */ 2 | /* eslint-disable @typescript-eslint/no-var-requires */ 3 | const withPlugins = require('next-compose-plugins') 4 | 5 | const { i18n } = require('./next-i18next.config') 6 | 7 | /** 8 | * // @type {import('next').Config} 9 | * TODO: make type above work 10 | * - @see https://github.com/vercel/next.js/issues/8044#issuecomment-525054841 11 | * - @see https://github.com/vercel/next.js/pull/13429#issuecomment-634621879 12 | * – It should be availabe already: https://github.com/vercel/next.js/blob/canary/packages/next/next-server/server/config-shared.ts#L12 13 | */ 14 | const nextConfig = { 15 | poweredByHeader: false, 16 | /** 17 | * Opt-in to webpack 5 support 18 | * @see https://github.com/vercel/next.js/issues/21679#issuecomment-771941447 19 | */ 20 | future: { 21 | webpack5: true, 22 | }, 23 | i18n, 24 | publicRuntimeConfig: { 25 | NODE_ENV: process.env.NODE_ENV, 26 | }, 27 | } 28 | 29 | module.exports = withPlugins([], nextConfig) 30 | -------------------------------------------------------------------------------- /nginx/nginx.conf: -------------------------------------------------------------------------------- 1 | # inspired by https://github.com/steveholgado/nextjs-docker-pm2-nginx/blob/12eacfd745be8007087e2945df82ab99eba5999a/nginx/default.conf 2 | events {} 3 | 4 | http { 5 | # Cache zone 6 | proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=STATIC:10m inactive=7d use_temp_path=off; 7 | 8 | upstream app { 9 | server app:3000; 10 | } 11 | 12 | server { 13 | listen 80 default_server; 14 | 15 | server_name _; 16 | server_tokens off; 17 | 18 | gzip on; 19 | gzip_proxied any; 20 | gzip_comp_level 4; 21 | gzip_types text/css application/javascript image/svg+xml; 22 | 23 | proxy_http_version 1.1; 24 | proxy_set_header Upgrade $http_upgrade; 25 | proxy_set_header Connection 'upgrade'; 26 | proxy_set_header Host $host; 27 | proxy_cache_bypass $http_upgrade; 28 | 29 | # BUILT ASSETS (E.G. JS BUNDLES) 30 | # Browser cache - max cache headers from Next.js as build id in url 31 | # Server cache - valid forever (cleared after cache "inactive" period) 32 | location /_next/static { 33 | proxy_cache STATIC; 34 | proxy_pass http://app; 35 | } 36 | 37 | # STATIC ASSETS (E.G. IMAGES) 38 | # Browser cache - "no-cache" headers from Next.js as no build id in url 39 | # Server cache - refresh regularly in case of changes 40 | location /static { 41 | proxy_cache STATIC; 42 | proxy_ignore_headers Cache-Control; 43 | proxy_cache_valid 60m; 44 | proxy_pass http://app; 45 | } 46 | 47 | # DYNAMIC ASSETS - NO CACHE 48 | location / { 49 | proxy_pass http://app; 50 | } 51 | } 52 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nextjs-template", 3 | "author": "Stefan Natter ", 4 | "version": "0.1.0", 5 | "license": "MIT", 6 | "private": true, 7 | "repository": { 8 | "type": "git", 9 | "url": "git+https://github.com/natterstefan/nextjs-template.git" 10 | }, 11 | "bugs": { 12 | "url": "https://github.com/natterstefan/nextjs-template/issues" 13 | }, 14 | "scripts": { 15 | "build-storybook": "build-storybook", 16 | "build": "next build", 17 | "cypress:open": "cypress open", 18 | "cypress:run": "cypress run", 19 | "dev": "next", 20 | "lint": "yarn lint:ts && yarn lint:css", 21 | "lint:ts": "tsc && eslint --cache \"./**/*.{ts,tsx}\"", 22 | "lint:css": "stylelint --cache './**/*.{ts,tsx,scss}'", 23 | "prepare": "is-ci || husky install", 24 | "postinstall": "node scripts/setup", 25 | "start": "next start", 26 | "storybook": "start-storybook -p 6006", 27 | "test": "jest --passWithNoTests", 28 | "test:cypress": "start-server-and-test dev 3000 cypress:open", 29 | "test:cypress:ci": "start-server-and-test dev 3000 cypress:run" 30 | }, 31 | "dependencies": { 32 | "@reduxjs/toolkit": "^1.5.1", 33 | "@rtk-incubator/rtk-query": "^0.3.0", 34 | "date-fns": "^2.21.0", 35 | "next": "^10.1.3", 36 | "next-compose-plugins": "^2.2.1", 37 | "next-i18next": "^8.1.3", 38 | "react": "^16.14.0", 39 | "react-cookie": "^4.0.3", 40 | "react-dom": "^16.14.0", 41 | "react-redux": "^7.2.3" 42 | }, 43 | "devDependencies": { 44 | "@babel/core": "^7.13.15", 45 | "@commitlint/cli": "^12.1.1", 46 | "@commitlint/config-conventional": "^12.1.1", 47 | "@storybook/addon-actions": "^6.2.8", 48 | "@storybook/addon-essentials": "^6.2.8", 49 | "@storybook/addon-links": "^6.2.8", 50 | "@storybook/addon-postcss": "^2.0.0", 51 | "@storybook/addons": "^6.2.8", 52 | "@storybook/builder-webpack5": "^6.2.8", 53 | "@storybook/react": "^6.2.8", 54 | "@types/enzyme": "^3.10.8", 55 | "@types/node": "^14.14.37", 56 | "@types/react": "^17.0.3", 57 | "@types/react-dom": "^17.0.3", 58 | "all-contributors-cli": "^6.20.0", 59 | "autoprefixer": "^10.2.5", 60 | "babel-loader": "^8.2.2", 61 | "css-loader": "^5.2.1", 62 | "cypress": "^6.9.1", 63 | "enzyme": "^3.11.0", 64 | "eslint": "^7.24.0", 65 | "eslint-config-airbnb": "^18.2.1", 66 | "eslint-config-ns-ts": "^1.6.1", 67 | "eslint-config-prettier": "^8.2.0", 68 | "eslint-plugin-cypress": "^2.11.2", 69 | "eslint-plugin-import": "^2.22.1", 70 | "eslint-plugin-jest": "^24.3.5", 71 | "eslint-plugin-jsx-a11y": "^6.4.1", 72 | "eslint-plugin-prettier": "^3.3.1", 73 | "eslint-plugin-react": "^7.23.2", 74 | "eslint-plugin-react-hooks": "^4.2.0", 75 | "husky": "^6.0.0", 76 | "is-ci": "^3.0.0", 77 | "jest": "^26.6.3", 78 | "jest-preset-ns": "^0.2.0", 79 | "postcss": "^8.2.10", 80 | "postcss-flexbugs-fixes": "^5.0.2", 81 | "postcss-import": "^14.0.1", 82 | "postcss-loader": "^5.2.0", 83 | "postcss-preset-env": "^6.7.0", 84 | "prettier": "^2.2.1", 85 | "sass": "^1.32.8", 86 | "sass-loader": "^11.0.1", 87 | "start-server-and-test": "^1.12.1", 88 | "storybook-addon-next-router": "^2.0.4", 89 | "style-loader": "^2.0.0", 90 | "stylelint": "^13.12.0", 91 | "stylelint-config-recommended": "^4.0.0", 92 | "tailwindcss": "^2.1.1", 93 | "typescript": "^4.2.4", 94 | "webpack": "^5.33.2" 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [ 3 | 'postcss-import', 4 | 'tailwindcss', 5 | 'postcss-flexbugs-fixes', 6 | [ 7 | 'postcss-preset-env', 8 | { 9 | autoprefixer: { 10 | flexbox: 'no-2009', 11 | }, 12 | stage: 3, 13 | features: { 14 | 'custom-properties': false, 15 | }, 16 | }, 17 | ], 18 | ], 19 | } 20 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies */ 2 | module.exports = require('eslint-config-ns-ts/prettier.config') 3 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/natterstefan/nextjs-rtk-query-example/8bd367f7057279f6aa56bd078583db1c7da24183/public/favicon.ico -------------------------------------------------------------------------------- /public/images/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/natterstefan/nextjs-rtk-query-example/8bd367f7057279f6aa56bd078583db1c7da24183/public/images/.gitkeep -------------------------------------------------------------------------------- /public/locales/de/common.json: -------------------------------------------------------------------------------- 1 | { 2 | "headline": "Next.js Vorlage", 3 | "home_DE": "Home Deutsch", 4 | "home_EN": "Home Englisch" 5 | } 6 | -------------------------------------------------------------------------------- /public/locales/en/common.json: -------------------------------------------------------------------------------- 1 | { 2 | "headline": "Next.js Template", 3 | "home_DE": "Home German", 4 | "home_EN": "Home English" 5 | } 6 | -------------------------------------------------------------------------------- /scripts/copyfiles.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | /* eslint-disable @typescript-eslint/no-var-requires */ 3 | 4 | /** 5 | * PREARE .env.* based on the given .env.*.example files 6 | */ 7 | const fs = require('fs') 8 | const path = require('path') 9 | 10 | module.exports = () => { 11 | const exampleFiles = [ 12 | path.join(__dirname, '../.env.local.example'), 13 | path.join(__dirname, '../.env.test.example'), 14 | ] 15 | const configFiles = [ 16 | path.join(__dirname, '../.env.local'), 17 | path.join(__dirname, '../.env.test'), 18 | ] 19 | 20 | configFiles.forEach((configFile, i) => { 21 | if (fs.existsSync(configFile)) { 22 | console.log(`${configFile} exists already.`) 23 | } else { 24 | fs.copyFile(exampleFiles[i], configFile, err => { 25 | if (err) { 26 | throw err 27 | } 28 | 29 | console.log(`${configFile} copied`) 30 | }) 31 | } 32 | }) 33 | } 34 | -------------------------------------------------------------------------------- /scripts/setup.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @typescript-eslint/no-var-requires 2 | const copyfiles = require('./copyfiles') 3 | 4 | copyfiles() 5 | -------------------------------------------------------------------------------- /src/components/page/Page.tsx: -------------------------------------------------------------------------------- 1 | import React, { FunctionComponent } from 'react' 2 | 3 | const Content: FunctionComponent = ({ children }) => { 4 | return
{children}
5 | } 6 | 7 | export function Page({ children }: React.PropsWithChildren) { 8 | return
{children}
9 | } 10 | 11 | Page.Content = Content 12 | -------------------------------------------------------------------------------- /src/components/page/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Page' 2 | -------------------------------------------------------------------------------- /src/hooks/useChangeLanguage.ts: -------------------------------------------------------------------------------- 1 | import { useRouter } from 'next/router' 2 | import { useEffect } from 'react' 3 | import { useCookies } from 'react-cookie' 4 | import addDays from 'date-fns/addDays' 5 | 6 | /** 7 | * useChangeLocation observes the current `locale` of the page and compares it 8 | * wit the the `defaultLocale`. If they do not match it updates the user's 9 | * cookies which will used by Next.js to determine the user's language. 10 | * 11 | * Docs 12 | * - @see https://nextjs.org/docs/advanced-features/i18n-routing#leveraging-the-next_locale-cookie 13 | * - @see https://www.infoxicator.com/how-i-translated-my-next-js-blog 14 | * - @see https://github.com/vinissimus/next-translate#10-how-to-save-the-user-defined-language 15 | * 16 | * Other Links 17 | * - @see https://dev.to/debosthefirst/how-to-use-cookies-for-persisting-users-in-nextjs-4617 18 | */ 19 | export const useChangeLocation = () => { 20 | const [cookie, setCookie] = useCookies(['NEXT_LOCALE']) 21 | const { locale, defaultLocale } = useRouter() 22 | 23 | useEffect(() => { 24 | if (cookie.NEXT_LOCALE !== locale) { 25 | const expires = addDays(new Date(), 100) 26 | setCookie('NEXT_LOCALE', locale, { path: '/', expires }) 27 | } 28 | }, [locale, defaultLocale, cookie.NEXT_LOCALE, setCookie]) 29 | } 30 | -------------------------------------------------------------------------------- /src/layouts/AppLayout.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from 'react' 2 | 3 | import { useChangeLocation } from '@/hooks/useChangeLanguage' 4 | 5 | export const AppLayout: FC = ({ children }) => { 6 | useChangeLocation() 7 | 8 | return ( 9 |
10 |
11 |
16 | {children} 17 |
18 |
19 |
20 | ) 21 | } 22 | -------------------------------------------------------------------------------- /src/pages/404.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { NextPage } from 'next' 3 | import Head from 'next/head' 4 | 5 | import { Page } from '@/components/page' 6 | 7 | const NotFoundPage: NextPage = () => { 8 | return ( 9 | <> 10 | 11 | 404 - Not Found 12 | 13 | 14 | 15 |
16 |
17 |
18 |
19 | 32 |
33 |
34 |

Error

35 |
36 |

Not found!

37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 | 45 | ) 46 | } 47 | 48 | export default NotFoundPage 49 | -------------------------------------------------------------------------------- /src/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactNode } from 'react' 2 | import Head from 'next/head' 3 | import { AppProps } from 'next/app' 4 | import { CookiesProvider } from 'react-cookie' 5 | import { appWithTranslation } from 'next-i18next' 6 | import { Provider } from 'react-redux' 7 | 8 | import { store } from '../store' 9 | 10 | import '../styles/index.scss' 11 | 12 | /** 13 | * Next.js Docs 14 | * @see https://nextjs.org/docs/advanced-features/custom-app 15 | * 16 | * Layout pattern is inspired by 17 | * @see https://adamwathan.me/2019/10/17/persistent-layout-patterns-in-nextjs/ 18 | */ 19 | function App({ Component, pageProps }: AppProps) { 20 | const Layout = 21 | (Component as any).layout || 22 | (({ children }: { children: ReactNode }) => <>{children}) 23 | 24 | return ( 25 | <> 26 | 27 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | ) 41 | } 42 | 43 | export default appWithTranslation(App) 44 | -------------------------------------------------------------------------------- /src/pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Document, { Html, Head, Main, NextScript } from 'next/document' 3 | 4 | /** 5 | * @see https://nextjs.org/docs/advanced-features/custom-document 6 | */ 7 | export default class MyDocument extends Document { 8 | render() { 9 | return ( 10 | 11 | 12 | 13 |
14 | 15 | 16 | 17 | ) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/pages/_error.tsx: -------------------------------------------------------------------------------- 1 | import { ServerResponse } from 'http' 2 | 3 | import React from 'react' 4 | import { NextPage } from 'next' 5 | 6 | import { Page } from '@/components/page' 7 | 8 | type ErrorProps = { 9 | statusCode?: ServerResponse['statusCode'] | null 10 | } 11 | 12 | const ErrorPage: NextPage = ({ statusCode }) => { 13 | return ( 14 | 15 | 16 |
17 |
18 |
19 |
20 | 33 |
34 |
35 |

36 | Error {statusCode} 37 |

38 |
39 |

An error occurred.

40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 | ) 48 | } 49 | 50 | ErrorPage.getInitialProps = async ({ res, err }) => { 51 | let statusCode = null 52 | 53 | if (res) { 54 | ;({ statusCode } = res) 55 | } else if (err) { 56 | ;({ statusCode } = err) 57 | } 58 | 59 | const props: ErrorProps = { 60 | statusCode, 61 | } 62 | 63 | return props 64 | } 65 | 66 | export default ErrorPage 67 | -------------------------------------------------------------------------------- /src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-nested-ternary */ 2 | import React from 'react' 3 | import { GetServerSideProps } from 'next' 4 | import Head from 'next/head' 5 | import { serverSideTranslations } from 'next-i18next/serverSideTranslations' 6 | 7 | import { Page } from '@/components/page' 8 | import { AppLayout } from '@/layouts/AppLayout' 9 | import { NextPageType } from '@/types/next' 10 | 11 | import { useGetRandomJokeQuery } from '../services/jokes' 12 | 13 | /** 14 | * What do you call a group of 8 Hobbits? 15 | * A Hobbyte. 16 | */ 17 | const IndexPage: NextPageType = () => { 18 | const { data, isFetching, refetch } = useGetRandomJokeQuery(undefined, { 19 | pollingInterval: 10000, 20 | }) 21 | 22 | return ( 23 | <> 24 | 25 | {/* eslint-disable-next-line jsx-a11y/accessible-emoji */} 26 | 🤡 Random Programming Jokes 🤡 27 | 28 | 29 | 30 |
31 |
32 |

33 | 34 | 🤡 35 | {' '} 36 | Random Programming Jokes{' '} 37 |

38 |
39 |
40 | {isFetching || !data ? ( 41 | '...' 42 | ) : data.type === 'twopart' ? ( 43 |
44 |

{data.setup}

45 |

{data.delivery}

46 |
47 | ) : ( 48 |
49 |

{data.joke}

50 |
51 | )} 52 |
53 |
54 | 61 |
62 |
63 |
64 |
65 | 66 | ) 67 | } 68 | 69 | IndexPage.layout = AppLayout 70 | 71 | export const getStaticProps: GetServerSideProps = async ({ locale }) => ({ 72 | props: { 73 | ...(locale && (await serverSideTranslations(locale, ['common']))), 74 | }, 75 | }) 76 | 77 | export default IndexPage 78 | -------------------------------------------------------------------------------- /src/services/jokes.ts: -------------------------------------------------------------------------------- 1 | // Need to use the React-specific entry point to import createApi 2 | import { createApi, fetchBaseQuery } from '@rtk-incubator/rtk-query/react' 3 | 4 | type RandomJokeResponse = 5 | | { 6 | type: 'twopart' 7 | setup: string 8 | delivery: string 9 | } 10 | | { 11 | type: 'single' 12 | joke: string 13 | } 14 | 15 | // Define a service using a base URL and expected endpoints 16 | export const jokeApi = createApi({ 17 | reducerPath: 'jokeApi', 18 | baseQuery: fetchBaseQuery({ baseUrl: 'https://v2.jokeapi.dev/' }), 19 | endpoints: builder => ({ 20 | getRandomJoke: builder.query({ 21 | query: () => 22 | `joke/Programming?blacklistFlags=explicit,nsfw,political,racist,religious,sexist`, 23 | }), 24 | }), 25 | }) 26 | 27 | // Export hooks for usage in functional components, which are 28 | // auto-generated based on the defined endpoints 29 | export const { useGetRandomJokeQuery } = jokeApi 30 | -------------------------------------------------------------------------------- /src/store.ts: -------------------------------------------------------------------------------- 1 | import { configureStore } from '@reduxjs/toolkit' 2 | import { setupListeners } from '@rtk-incubator/rtk-query' 3 | 4 | import { jokeApi } from './services/jokes' 5 | 6 | export const store = configureStore({ 7 | reducer: { 8 | // Add the generated reducer as a specific top-level slice 9 | [jokeApi.reducerPath]: jokeApi.reducer, 10 | }, 11 | // Adding the api middleware enables caching, invalidation, polling, 12 | // and other useful features of `rtk-query`. 13 | middleware: getDefaultMiddleware => 14 | getDefaultMiddleware().concat(jokeApi.middleware), 15 | }) 16 | 17 | // optional, but required for refetchOnFocus/refetchOnReconnect behaviors 18 | // see `setupListeners` docs - takes an optional callback as the 2nd arg for customization 19 | setupListeners(store.dispatch) 20 | -------------------------------------------------------------------------------- /src/styles/Scrollbar.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * inspired by: 3 | * @see https://www.digitalocean.com/community/tutorials/css-scrollbars 4 | */ 5 | 6 | /* Firefox */ 7 | * { 8 | scrollbar-width: thin; 9 | scrollbar-color: #9ba1ae #ced2d8; 10 | } 11 | 12 | /* width (vertical bar) and height (horizontal bar) */ 13 | *::-webkit-scrollbar { 14 | width: 8px; /* width of the entire scrollbar */ 15 | height: 8px; /* height of the entire scrollbar */ 16 | } 17 | 18 | /* Track */ 19 | *::-webkit-scrollbar-track { 20 | @apply bg-gray-300; /* color of the tracking area */ 21 | } 22 | 23 | /* Handle */ 24 | *::-webkit-scrollbar-thumb { 25 | @apply bg-gray-400; /* color of the scroll thumb */ 26 | @apply border-gray-300 border border-solid; /* creates padding around scroll thumb */ 27 | @apply rounded-xl; /* roundness of the scroll thumb */ 28 | 29 | &:hover { 30 | @apply bg-gray-500; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/styles/index.scss: -------------------------------------------------------------------------------- 1 | @import 'tailwindcss/base'; 2 | @import 'tailwindcss/components'; 3 | @import 'tailwindcss/utilities'; 4 | 5 | @import './Scrollbar.scss'; 6 | -------------------------------------------------------------------------------- /src/types/next.d.ts: -------------------------------------------------------------------------------- 1 | import { ReactNode } from 'react' 2 | import { NextPage } from 'next' 3 | 4 | declare type NextPageType = NextPage & { layout: ReactNode } 5 | -------------------------------------------------------------------------------- /stylelint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | ignoreFiles: [ 3 | '**/__generated__/**/*', 4 | '**/__snapshots__/**/*', 5 | '**/build', 6 | '**/coverage/**/*', 7 | '**/node_modules', 8 | '**/storybook-static', 9 | '**/tmp', 10 | ], 11 | extends: ['stylelint-config-recommended'], 12 | rules: { 13 | 'at-rule-no-unknown': [ 14 | true, 15 | { 16 | ignoreAtRules: [ 17 | 'tailwind', 18 | 'apply', 19 | 'variants', 20 | 'responsive', 21 | 'screen', 22 | ], 23 | }, 24 | ], 25 | }, 26 | } 27 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | mode: 'jit', 3 | purge: ['./src/**/*.{ts,tsx,scss}'], 4 | } 5 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "esnext", 5 | "lib": ["dom", "dom.iterable", "esnext"], 6 | "skipLibCheck": true, 7 | "allowJs": false, 8 | "jsx": "preserve", 9 | "removeComments": true, 10 | "baseUrl": ".", 11 | "paths": { 12 | "@/*": ["src/*", "*"], 13 | }, 14 | "noEmit": true, 15 | "importHelpers": true, 16 | "isolatedModules": true, 17 | "strict": true, 18 | "noImplicitAny": true, 19 | "strictNullChecks": true, 20 | "noUnusedLocals": true, 21 | "noUnusedParameters": true, 22 | "noImplicitReturns": true, 23 | "moduleResolution": "node", 24 | "resolveJsonModule": true, 25 | "allowSyntheticDefaultImports": true, 26 | "forceConsistentCasingInFileNames": true, 27 | "esModuleInterop": true, 28 | }, 29 | /** either exclude cypress _or_ only include src/ files to ensure that Cypress' 30 | * and jest's `expect` types don't interfere with each other. 31 | * 32 | * inspired by: 33 | * @see https://github.com/cypress-io/cypress-and-jest-typescript-example/blob/734a38e2451f6cf0341190959a24a4ca40b8ecb7/tsconfig.json 34 | */ 35 | "include": ["next-env.d.ts", "src/**/*.ts", "src/**/*.tsx", "src/**/*.d.ts"], 36 | "exclude": ["build", "node_modules", "storybook-static", "tmp"] 37 | } 38 | -------------------------------------------------------------------------------- /tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "jsx": "react" 5 | } 6 | } --------------------------------------------------------------------------------