├── .env ├── .eslintignore ├── .eslintrc.json ├── .github ├── FUNDING.yml └── workflows │ ├── CI.yml │ ├── checkly.yml │ ├── crowdin.yml │ ├── release.yml │ └── update-deps.yml ├── .gitignore ├── .husky ├── commit-msg └── pre-commit ├── .storybook ├── main.ts └── preview.ts ├── .vscode ├── extensions.json ├── i18n-ally-custom-framework.yml ├── launch.json ├── settings.json └── tasks.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── checkly.config.ts ├── codecov.yml ├── commitlint.config.ts ├── crowdin.yml ├── drizzle.config.ts ├── lint-staged.config.js ├── migrations ├── 0000_init_db.sql └── meta │ ├── 0000_snapshot.json │ └── _journal.json ├── next-env.d.ts ├── next.config.mjs ├── package-lock.json ├── package.json ├── playwright.config.ts ├── postcss.config.js ├── public ├── apple-touch-icon.png ├── assets │ └── images │ │ ├── better-stack-dark.png │ │ ├── better-stack-white.png │ │ ├── checkly-logo-dark.png │ │ ├── checkly-logo-light.png │ │ ├── clerk-logo-dark.png │ │ ├── codecov-dark.svg │ │ ├── codecov-white.svg │ │ ├── crowdin-dark.png │ │ ├── crowdin-white.png │ │ ├── nextjs-starter-banner.png │ │ ├── nextlessjs.png │ │ ├── sentry-dark.png │ │ ├── sentry-white.png │ │ ├── turso-dark.png │ │ └── turso-light.png ├── favicon-16x16.png ├── favicon-32x32.png └── favicon.ico ├── sentry.client.config.ts ├── sentry.edge.config.ts ├── sentry.server.config.ts ├── src ├── app │ ├── [locale] │ │ ├── (auth) │ │ │ ├── (center) │ │ │ │ ├── layout.tsx │ │ │ │ ├── sign-in │ │ │ │ │ └── [[...sign-in]] │ │ │ │ │ │ └── page.tsx │ │ │ │ └── sign-up │ │ │ │ │ └── [[...sign-up]] │ │ │ │ │ └── page.tsx │ │ │ ├── dashboard │ │ │ │ ├── layout.tsx │ │ │ │ ├── page.tsx │ │ │ │ └── user-profile │ │ │ │ │ └── [[...user-profile]] │ │ │ │ │ └── page.tsx │ │ │ └── layout.tsx │ │ ├── (unauth) │ │ │ ├── about │ │ │ │ ├── page.test.tsx │ │ │ │ └── page.tsx │ │ │ ├── api │ │ │ │ └── guestbook │ │ │ │ │ └── route.ts │ │ │ ├── guestbook │ │ │ │ └── page.tsx │ │ │ ├── layout.tsx │ │ │ ├── page.test.tsx │ │ │ ├── page.tsx │ │ │ └── portfolio │ │ │ │ ├── [slug] │ │ │ │ └── page.tsx │ │ │ │ └── page.tsx │ │ └── layout.tsx │ ├── global-error.tsx │ ├── robots.ts │ └── sitemap.ts ├── components │ ├── AddGuestbookForm.tsx │ ├── DeleteGuestbookEntry.tsx │ ├── DemoBadge.tsx │ ├── EditableGuestbookEntry.tsx │ ├── GuestbookForm.tsx │ ├── GuestbookList.tsx │ ├── Hello.tsx │ ├── LocaleSwitcher.tsx │ ├── LogOutButton.tsx │ └── Sponsors.tsx ├── libs │ ├── DB.ts │ ├── Env.ts │ ├── Logger.ts │ ├── i18n.ts │ └── i18nNavigation.ts ├── locales │ ├── en.json │ └── fr.json ├── middleware.ts ├── models │ └── Schema.ts ├── styles │ └── global.css ├── templates │ ├── BaseTemplate.stories.tsx │ ├── BaseTemplate.test.tsx │ └── BaseTemplate.tsx ├── types │ └── global.d.ts ├── utils │ ├── AppConfig.ts │ ├── Helpers.test.ts │ ├── Helpers.ts │ └── dist │ │ └── AppConfig.js └── validations │ └── GuestbookValidation.ts ├── tailwind.config.ts ├── tests ├── e2e │ ├── Guestbook.spec.ts │ ├── Navigation.spec.ts │ └── Sanity.check.spec.ts └── integration │ └── Guestbook.spec.ts ├── tsconfig.json ├── vitest-setup.ts └── vitest.config.mts /.env: -------------------------------------------------------------------------------- 1 | # FIXME: Configure environment variables for your project 2 | 3 | # For security reason, don't push secret key in your git repo. 4 | # Append .local to the environment files to prevent your secret key from being commited to Git. 5 | 6 | # Hosting 7 | # Replace by your domain name, only for production 8 | # NEXT_PUBLIC_APP_URL=https://example.com 9 | 10 | # Database 11 | # Using an incorrect DATABASE_URL value, Next.js build will timeout and you will get the following error: "because it took more than 60 seconds" 12 | # DATABASE_URL=libsql://[RANDOM-CHARS]-[DB-NAME]-[ORG-NAME].turso.io 13 | DATABASE_URL=file:next-js-boilerplate.db 14 | 15 | # Clerk authentication 16 | NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_cHJvZm91bmQtYmx1ZWdpbGwtMC5jbGVyay5hY2NvdW50cy5kZXYk 17 | 18 | NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in 19 | 20 | ######## [BEGIN] SENSITIVE DATA ######## For security reason, don't update the following variables (secret key) directly in this file. 21 | ######## Please create a new file named `.env.local`, all environment files ending with `.local` won't be tracked by Git. 22 | ######## After creating the file, you can add the following variables. 23 | CLERK_SECRET_KEY=sk_test_19ZQnHVcfboDNoxeTuVBRwGNorgLXEgzQwIm5Gbstj 24 | 25 | DATABASE_AUTH_TOKEN=your_database_auth_token 26 | 27 | # LOGTAIL_SOURCE_TOKEN= 28 | ######## [END] SENSITIVE DATA 29 | 30 | # CROWDIN_PROJECT_ID=yayu-form 31 | # CROWDIN_PERSONAL_TOKEN=091f789109e4dda61679a42a946cab775c314af908dda4d3b53f976b786626b0366c8eada9bb72a1 -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | out 3 | !.storybook 4 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | // Configuration for JavaScript files 3 | "extends": [ 4 | "airbnb-base", 5 | "next/core-web-vitals", // Needed to avoid warning in next.js build: 'The Next.js plugin was not detected in your ESLint configuration' 6 | "plugin:prettier/recommended" 7 | ], 8 | "rules": { 9 | "prettier/prettier": [ 10 | "error", 11 | { 12 | "singleQuote": true, 13 | "endOfLine": "auto" 14 | } 15 | ] // Avoid conflict rule between Prettier and Airbnb Eslint 16 | }, 17 | "overrides": [ 18 | // Configuration for TypeScript files 19 | { 20 | "files": ["**/*.ts", "**/*.tsx", "**/*.mts"], 21 | "plugins": [ 22 | "@typescript-eslint", 23 | "unused-imports", 24 | "tailwindcss", 25 | "simple-import-sort" 26 | ], 27 | "extends": [ 28 | "plugin:tailwindcss/recommended", 29 | "airbnb", 30 | "airbnb-typescript", 31 | "next/core-web-vitals", 32 | "plugin:prettier/recommended" 33 | ], 34 | "parser": "@typescript-eslint/parser", 35 | "parserOptions": { 36 | "project": "./tsconfig.json" 37 | }, 38 | "rules": { 39 | "prettier/prettier": [ 40 | "error", 41 | { 42 | "singleQuote": true, 43 | "endOfLine": "auto" 44 | } 45 | ], // Avoid conflict rule between Prettier and Airbnb Eslint 46 | "import/extensions": "off", // Avoid missing file extension errors, TypeScript already provides a similar feature 47 | "react/function-component-definition": "off", // Disable Airbnb's specific function type 48 | "react/destructuring-assignment": "off", // Vscode doesn't support automatically destructuring, it's a pain to add a new variable 49 | "react/require-default-props": "off", // Allow non-defined react props as undefined 50 | "react/jsx-props-no-spreading": "off", // _app.tsx uses spread operator and also, react-hook-form 51 | "@typescript-eslint/comma-dangle": "off", // Avoid conflict rule between Eslint and Prettier 52 | "@typescript-eslint/consistent-type-imports": "error", // Ensure `import type` is used when it's necessary 53 | "no-restricted-syntax": [ 54 | "error", 55 | "ForInStatement", 56 | "LabeledStatement", 57 | "WithStatement" 58 | ], // Overrides Airbnb configuration and enable no-restricted-syntax 59 | "import/prefer-default-export": "off", // Named export is easier to refactor automatically 60 | "simple-import-sort/imports": "error", // Import configuration for `eslint-plugin-simple-import-sort` 61 | "simple-import-sort/exports": "error", // Export configuration for `eslint-plugin-simple-import-sort` 62 | "import/order": "off", // Avoid conflict rule between `eslint-plugin-import` and `eslint-plugin-simple-import-sort` 63 | "@typescript-eslint/no-unused-vars": "off", 64 | "unused-imports/no-unused-imports": "error", 65 | "unused-imports/no-unused-vars": [ 66 | "error", 67 | { "argsIgnorePattern": "^_" } 68 | ] 69 | } 70 | }, 71 | // Configuration for testing 72 | { 73 | "files": ["**/*.test.ts", "**/*.test.tsx"], 74 | "plugins": ["vitest", "jest-formatting", "testing-library", "jest-dom"], 75 | "extends": [ 76 | "plugin:vitest/recommended", 77 | "plugin:jest-formatting/recommended", 78 | "plugin:testing-library/react", 79 | "plugin:jest-dom/recommended" 80 | ] 81 | }, 82 | // Configuration for e2e testing (Playwright) 83 | { 84 | "files": ["**/*.spec.ts"], 85 | "extends": ["plugin:playwright/recommended"] 86 | }, 87 | // Configuration for Storybook 88 | { 89 | "files": ["*.stories.*"], 90 | "extends": ["plugin:storybook/recommended"], 91 | "rules": { 92 | "import/no-extraneous-dependencies": [ 93 | "error", 94 | { 95 | "devDependencies": true 96 | } 97 | ] 98 | } 99 | } 100 | ] 101 | } 102 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: ixartz 2 | custom: 3 | ["https://donate.stripe.com/7sI5m5146ehfddm7tj", "https://nextlessjs.com"] 4 | -------------------------------------------------------------------------------- /.github/workflows/CI.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | jobs: 10 | build: 11 | strategy: 12 | matrix: 13 | node-version: [20.x, 22.x] 14 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 15 | 16 | name: Build with ${{ matrix.node-version }} 17 | runs-on: ubuntu-latest 18 | 19 | steps: 20 | - uses: actions/checkout@v3 21 | - name: Use Node.js ${{ matrix.node-version }} 22 | uses: actions/setup-node@v3 23 | with: 24 | node-version: ${{ matrix.node-version }} 25 | cache: "npm" 26 | - run: npm ci 27 | - run: npm run build 28 | 29 | test: 30 | strategy: 31 | matrix: 32 | node-version: [20.x] 33 | 34 | name: Run all tests 35 | runs-on: ubuntu-latest 36 | 37 | steps: 38 | - uses: actions/checkout@v3 39 | with: 40 | fetch-depth: 0 # Retrieve Git history, needed to verify commits 41 | - name: Use Node.js ${{ matrix.node-version }} 42 | uses: actions/setup-node@v3 43 | with: 44 | node-version: ${{ matrix.node-version }} 45 | cache: "npm" 46 | - run: npm ci 47 | 48 | - name: Set SENTRY_AUTH_TOKEN env if secret exists 49 | run: | 50 | if [[ -n "${{ secrets.SENTRY_AUTH_TOKEN }}" ]]; then 51 | echo "SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }}" >> $GITHUB_ENV 52 | fi 53 | 54 | - name: Build Next.js for E2E tests 55 | run: npm run build 56 | 57 | - if: github.event_name == 'pull_request' 58 | name: Validate all commits from PR 59 | run: npx commitlint --from ${{ github.event.pull_request.base.sha }} --to ${{ github.event.pull_request.head.sha }} --verbose 60 | 61 | - name: Linter 62 | run: npm run lint 63 | 64 | - name: Type checking 65 | run: npm run check-types 66 | 67 | - name: Run unit tests 68 | run: npm run test 69 | 70 | - name: Upload coverage reports to Codecov 71 | uses: codecov/codecov-action@v3 72 | env: 73 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 74 | 75 | - name: Install Playwright (used for Storybook and E2E tests) 76 | run: npx playwright install --with-deps 77 | 78 | - name: Run storybook tests 79 | run: npm run test-storybook:ci 80 | 81 | - name: Run E2E tests 82 | run: npx percy exec -- npm run test:e2e 83 | env: 84 | PERCY_TOKEN: ${{ secrets.PERCY_TOKEN }} 85 | 86 | - uses: actions/upload-artifact@v3 87 | if: always() 88 | with: 89 | name: test-results 90 | path: test-results/ 91 | retention-days: 7 92 | -------------------------------------------------------------------------------- /.github/workflows/checkly.yml: -------------------------------------------------------------------------------- 1 | name: Checkly 2 | 3 | on: [deployment_status] 4 | 5 | env: 6 | CHECKLY_API_KEY: ${{ secrets.CHECKLY_API_KEY }} 7 | CHECKLY_ACCOUNT_ID: ${{ secrets.CHECKLY_ACCOUNT_ID }} 8 | ENVIRONMENT_URL: ${{ github.event.deployment_status.environment_url }} 9 | CHECKLY_TEST_ENVIRONMENT: ${{ github.event.deployment_status.environment }} 10 | 11 | jobs: 12 | test-e2e: 13 | strategy: 14 | matrix: 15 | node-version: [20.x] 16 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 17 | 18 | # Only run when the deployment was successful 19 | if: github.event.deployment_status.state == 'success' 20 | 21 | name: Test E2E on Checkly 22 | runs-on: ubuntu-latest 23 | timeout-minutes: 10 24 | 25 | steps: 26 | - uses: actions/checkout@v3 27 | with: 28 | ref: "${{ github.event.deployment_status.deployment.ref }}" 29 | fetch-depth: 0 30 | 31 | - name: Set branch name # workaround to detect branch name in "deployment_status" actions 32 | run: echo "CHECKLY_TEST_REPO_BRANCH=$(git show -s --pretty=%D HEAD | tr -s ',' '\n' | sed 's/^ //' | grep -e 'origin/' | head -1 | sed 's/\origin\///g')" >> $GITHUB_ENV 33 | 34 | - uses: actions/setup-node@v3 35 | with: 36 | node-version: ${{ matrix.node-version }} 37 | cache: "npm" 38 | 39 | - name: Restore or cache node_modules 40 | id: cache-node-modules 41 | uses: actions/cache@v3 42 | with: 43 | path: node_modules 44 | key: node-modules-${{ hashFiles('package-lock.json') }} 45 | 46 | - name: Install dependencies 47 | if: steps.cache-node-modules.outputs.cache-hit != 'true' 48 | run: npm ci 49 | 50 | - name: Run checks # run the checks passing in the ENVIRONMENT_URL and recording a test session. 51 | id: run-checks 52 | run: npx checkly test -e ENVIRONMENT_URL=${{ env.ENVIRONMENT_URL }} --reporter=github --record 53 | 54 | - name: Create summary # export the markdown report to the job summary. 55 | id: create-summary 56 | run: cat checkly-github-report.md > $GITHUB_STEP_SUMMARY 57 | 58 | - name: Deploy checks # if the test run was successful and we are on Production, deploy the checks 59 | id: deploy-checks 60 | if: steps.run-checks.outcome == 'success' && github.event.deployment_status.environment == 'Production' 61 | run: npx checkly deploy --force 62 | -------------------------------------------------------------------------------- /.github/workflows/crowdin.yml: -------------------------------------------------------------------------------- 1 | name: Crowdin Action 2 | 3 | on: 4 | push: 5 | branches: [ main ] # Run on push to the main branch 6 | schedule: 7 | - cron: "0 5 * * *" # Run every day at 5am 8 | workflow_dispatch: # Run manually 9 | 10 | jobs: 11 | synchronize-with-crowdin: 12 | name: Synchronize with Crowdin 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v3 17 | 18 | - name: crowdin action 19 | uses: crowdin/github-action@v1 20 | with: 21 | upload_sources: true 22 | upload_translations: true 23 | download_translations: true 24 | localization_branch_name: l10n_crowdin_translations 25 | create_pull_request: true 26 | pull_request_title: 'New Crowdin Translations' 27 | pull_request_body: 'New Crowdin translations by [Crowdin GH Action](https://github.com/crowdin/github-action)' 28 | pull_request_base_branch_name: 'main' 29 | commit_message: 'chore: new Crowdin translations by GitHub Action' 30 | env: 31 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 32 | CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }} 33 | CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }} 34 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | workflow_run: 5 | workflows: ["CI"] 6 | types: 7 | - completed 8 | branches: 9 | - main 10 | 11 | jobs: 12 | release: 13 | strategy: 14 | matrix: 15 | node-version: [20.x] 16 | 17 | name: Create a new release 18 | runs-on: ubuntu-latest 19 | 20 | permissions: 21 | contents: write # to be able to publish a GitHub release 22 | issues: write # to be able to comment on released issues 23 | pull-requests: write # to be able to comment on released pull requests 24 | 25 | steps: 26 | - uses: actions/checkout@v3 27 | with: 28 | fetch-depth: 0 29 | - name: Use Node.js ${{ matrix.node-version }} 30 | uses: actions/setup-node@v3 31 | with: 32 | node-version: ${{ matrix.node-version }} 33 | cache: "npm" 34 | - run: HUSKY=0 npm ci 35 | 36 | - name: Release 37 | env: 38 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 39 | run: npx semantic-release 40 | -------------------------------------------------------------------------------- /.github/workflows/update-deps.yml: -------------------------------------------------------------------------------- 1 | name: Update dependencies 2 | 3 | on: 4 | workflow_dispatch: 5 | schedule: 6 | - cron: "0 0 1 * *" 7 | 8 | jobs: 9 | update: 10 | strategy: 11 | matrix: 12 | node-version: [20.x] 13 | 14 | name: Update all dependencies 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v3 19 | - name: Use Node.js ${{ matrix.node-version }} 20 | uses: actions/setup-node@v3 21 | with: 22 | node-version: ${{ matrix.node-version }} 23 | cache: "npm" 24 | - run: npm ci 25 | 26 | - run: npx npm-check-updates -u # Update dependencies 27 | - run: rm -Rf node_modules package-lock.json 28 | - run: npm install 29 | - name: Create Pull Request 30 | uses: peter-evans/create-pull-request@v4 31 | with: 32 | commit-message: "build: update dependencies to the latest version" 33 | title: Update dependencies to the latest version 34 | -------------------------------------------------------------------------------- /.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 | # Database 9 | *.db 10 | 11 | # testing 12 | /coverage 13 | 14 | # storybook 15 | storybook-static 16 | *storybook.log 17 | 18 | # playwright 19 | /test-results/ 20 | /playwright-report/ 21 | /playwright/.cache/ 22 | 23 | # next.js 24 | /.next 25 | /out 26 | 27 | # cache 28 | .swc/ 29 | 30 | # production 31 | /build 32 | 33 | # misc 34 | .DS_Store 35 | *.pem 36 | Thumbs.db 37 | 38 | # debug 39 | npm-debug.log* 40 | pnpm-debug.log* 41 | yarn-debug.log* 42 | yarn-error.log* 43 | 44 | # local env files 45 | .env*.local 46 | 47 | # local folder 48 | local 49 | 50 | # vercel 51 | .vercel 52 | 53 | # Sentry Config File 54 | .sentryclirc 55 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | cd "$(dirname "$0")/.." && npx --no -- commitlint --edit $1 3 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # Disable concurent to run `check-types` after ESLint in lint-staged 3 | cd "$(dirname "$0")/.." && npx lint-staged --concurrent false 4 | -------------------------------------------------------------------------------- /.storybook/main.ts: -------------------------------------------------------------------------------- 1 | import type { StorybookConfig } from '@storybook/nextjs'; 2 | 3 | const config: StorybookConfig = { 4 | stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'], 5 | addons: [ 6 | '@storybook/addon-onboarding', 7 | '@storybook/addon-links', 8 | '@storybook/addon-essentials', 9 | '@storybook/addon-interactions', 10 | ], 11 | framework: { 12 | name: '@storybook/nextjs', 13 | options: {}, 14 | }, 15 | staticDirs: ['../public'], 16 | core: { 17 | disableTelemetry: true, 18 | }, 19 | }; 20 | 21 | export default config; 22 | -------------------------------------------------------------------------------- /.storybook/preview.ts: -------------------------------------------------------------------------------- 1 | import '../src/styles/global.css'; 2 | 3 | import type { Preview } from '@storybook/react'; 4 | 5 | const preview: Preview = { 6 | parameters: { 7 | controls: { 8 | matchers: { 9 | color: /(background|color)$/i, 10 | date: /Date$/i, 11 | }, 12 | }, 13 | nextjs: { 14 | appDirectory: true, 15 | }, 16 | }, 17 | }; 18 | 19 | export default preview; 20 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "dbaeumer.vscode-eslint", 4 | "mikestead.dotenv", 5 | "csstools.postcss", 6 | "bradlc.vscode-tailwindcss", 7 | "vitest.explorer", 8 | "humao.rest-client", 9 | "yoavbls.pretty-ts-errors", 10 | "ms-playwright.playwright", 11 | "github.vscode-github-actions", 12 | "lokalise.i18n-ally" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /.vscode/i18n-ally-custom-framework.yml: -------------------------------------------------------------------------------- 1 | languageIds: 2 | - javascript 3 | - typescript 4 | - javascriptreact 5 | - typescriptreact 6 | 7 | usageMatchRegex: 8 | - "[^\\w\\d]t\\(['\"`]({key})['\"`]" 9 | 10 | scopeRangeRegex: "(?:useTranslations\\(|getTranslations\\(|namespace:)\\s*['\"`](.*?)['\"`]" 11 | 12 | monopoly: true 13 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Next.js: debug full stack", 9 | "type": "node-terminal", 10 | "request": "launch", 11 | "command": "npm run dev", 12 | "serverReadyAction": { 13 | "pattern": "- Local:.+(https?://.+)", 14 | "uriFormat": "%s", 15 | "action": "debugWithChrome" 16 | } 17 | } 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.tabSize": 2, 3 | "editor.detectIndentation": false, 4 | "search.exclude": { 5 | "package-lock.json": true 6 | }, 7 | "editor.defaultFormatter": "dbaeumer.vscode-eslint", 8 | "editor.formatOnSave": false, 9 | "editor.codeActionsOnSave": [ 10 | "source.addMissingImports", 11 | "source.fixAll.eslint" 12 | ], 13 | "typescript.tsdk": "node_modules/typescript/lib", // Use the workspace version of TypeScript 14 | "typescript.enablePromptUseWorkspaceTsdk": true, // For security reasons it's require that users opt into using the workspace version of typescript 15 | "typescript.preferences.autoImportFileExcludePatterns": [ 16 | // useRouter should be imported from `next/navigation` instead of `next/router` 17 | "next/router.d.ts", 18 | "next/dist/client/router.d.ts", 19 | // give priority for Link to next/link instead of lucide-react 20 | "lucide-react" 21 | ], 22 | "typescript.preferences.preferTypeOnlyAutoImports": true, // Prefer type-only imports 23 | "testing.openTesting": "neverOpen", // Don't open the testing view automatically when running tests 24 | // Multiple language settings for json and jsonc files 25 | "[json][jsonc][yaml]": { 26 | "editor.formatOnSave": true, 27 | "editor.defaultFormatter": "esbenp.prettier-vscode" 28 | }, 29 | "prettier.ignorePath": ".gitignore", // Don't run prettier for files listed in .gitignore 30 | "i18n-ally.localesPaths": ["src/locales"], 31 | "i18n-ally.keystyle": "nested" 32 | } 33 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "label": "Project wide type checking with TypeScript", 8 | "type": "npm", 9 | "script": "check-types", 10 | "problemMatcher": ["$tsc"], 11 | "group": { 12 | "kind": "build", 13 | "isDefault": true 14 | }, 15 | "presentation": { 16 | "clear": true, 17 | "reveal": "never" 18 | } 19 | } 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # [3.52.0](https://github.com/ixartz/Next-js-Boilerplate/compare/v3.51.0...v3.52.0) (2024-05-31) 2 | 3 | 4 | ### Features 5 | 6 | * update Drizzle configuration for Drizzle Kit 0.22 and improve ([5159455](https://github.com/ixartz/Next-js-Boilerplate/commit/5159455ab2cfb569702b33a7e2135ec23f32d598)) 7 | 8 | # [3.51.0](https://github.com/ixartz/Next-js-Boilerplate/compare/v3.50.1...v3.51.0) (2024-05-29) 9 | 10 | 11 | ### Features 12 | 13 | * update to Drizzle kit 0.21 version, no need to have dialect in command line ([62aa678](https://github.com/ixartz/Next-js-Boilerplate/commit/62aa6786117637e6b76c97f6c98f7ca6e8c343b0)) 14 | 15 | ## [3.50.1](https://github.com/ixartz/Next-js-Boilerplate/compare/v3.50.0...v3.50.1) (2024-05-20) 16 | 17 | 18 | ### Bug Fixes 19 | 20 | * add eslint support for .mts file ([cd58d38](https://github.com/ixartz/Next-js-Boilerplate/commit/cd58d3806206e269d712e0976f4101af26275e44)) 21 | 22 | # [3.50.0](https://github.com/ixartz/Next-js-Boilerplate/compare/v3.49.0...v3.50.0) (2024-05-18) 23 | 24 | 25 | ### Features 26 | 27 | * replace Jest by Vitest for better DX ([2504504](https://github.com/ixartz/Next-js-Boilerplate/commit/25045041bb0af1fc4065ccffdb4d4d9b715c5823)) 28 | * update to Storybook v8 ([51b20a6](https://github.com/ixartz/Next-js-Boilerplate/commit/51b20a64f8f7a9780cb4c81b6ec2f0d1ac8779c5)) 29 | 30 | 31 | ### Reverts 32 | 33 | * reuse vitest.config.mts to avoid warning when running the tests ([f923242](https://github.com/ixartz/Next-js-Boilerplate/commit/f9232425d3cca895bcf3b45355dbee2caaedccce)) 34 | 35 | # [3.49.0](https://github.com/ixartz/Next-js-Boilerplate/compare/v3.48.0...v3.49.0) (2024-05-17) 36 | 37 | 38 | ### Features 39 | 40 | * vscode jest open test result view on test fails and add unauthenticatedUrl in clerk middleware ([2a68124](https://github.com/ixartz/Next-js-Boilerplate/commit/2a681244f834b6ea55bcd5cd3105f8b4a9df4a05)) 41 | 42 | # [3.48.0](https://github.com/ixartz/Next-js-Boilerplate/compare/v3.47.0...v3.48.0) (2024-05-09) 43 | 44 | 45 | ### Features 46 | 47 | * add custom configuration for i18n ally VSCode extension ([46f9459](https://github.com/ixartz/Next-js-Boilerplate/commit/46f945963c02eb29efc802fb0f3b1220b10bdf13)) 48 | 49 | # [3.47.0](https://github.com/ixartz/Next-js-Boilerplate/compare/v3.46.0...v3.47.0) (2024-05-07) 50 | 51 | 52 | ### Features 53 | 54 | * make dashboard without lang protected route in Clerk ([704466b](https://github.com/ixartz/Next-js-Boilerplate/commit/704466bbab40e366d0c1e17b66d7f5f0e97b902b)) 55 | * run Clerk middleware only needed ([5aeee06](https://github.com/ixartz/Next-js-Boilerplate/commit/5aeee0609bb9abbccf17aa0d2900cffdc7c3a18a)) 56 | * upgrade to Clerk v5 and use Clerk's Core 2 ([c1978f1](https://github.com/ixartz/Next-js-Boilerplate/commit/c1978f181a7c29e443fe407d91dfb9c2ae147f04)) 57 | 58 | 59 | ### Reverts 60 | 61 | * add back process.env.NEXT_PUBLIC_CLERK_SIGN_IN_URL ([f8cb9f4](https://github.com/ixartz/Next-js-Boilerplate/commit/f8cb9f441e08ec4f0e4501e4b42b4923adbc01a1)) 62 | * downgrade React to 18.2 due to testing errors, error raised in Next.js issue [#65161](https://github.com/ixartz/Next-js-Boilerplate/issues/65161) ([1815eb3](https://github.com/ixartz/Next-js-Boilerplate/commit/1815eb3670f53b4d949a06505e8ef3afd4ab0ee5)) 63 | 64 | # [3.46.0](https://github.com/ixartz/Next-js-Boilerplate/compare/v3.45.0...v3.46.0) (2024-04-13) 65 | 66 | 67 | ### Features 68 | 69 | * new turso logo ([3e781fc](https://github.com/ixartz/Next-js-Boilerplate/commit/3e781fc75201a7271a3a640a0b665adb1560add6)) 70 | * use new Turso tagline ([601ba6b](https://github.com/ixartz/Next-js-Boilerplate/commit/601ba6b2a4beb1a0c6779964d2d654bd3553f044)) 71 | 72 | # [3.45.0](https://github.com/ixartz/Next-js-Boilerplate/compare/v3.44.1...v3.45.0) (2024-04-04) 73 | 74 | 75 | ### Features 76 | 77 | * remove next-sitemap and use the native Next.js sitemap/robots.txt ([135a435](https://github.com/ixartz/Next-js-Boilerplate/commit/135a4350bef905d2a38a8901d42e5fa304fb92bc)) 78 | 79 | ## [3.44.1](https://github.com/ixartz/Next-js-Boilerplate/compare/v3.44.0...v3.44.1) (2024-04-03) 80 | 81 | 82 | ### Bug Fixes 83 | 84 | * add Twitter in the index page ([75dfb8b](https://github.com/ixartz/Next-js-Boilerplate/commit/75dfb8bc5ca40446005f8d405add52d09071f62a)) 85 | * use new VSCode Jest configuration ([e92e4e0](https://github.com/ixartz/Next-js-Boilerplate/commit/e92e4e09c636944d85cec38683738520224acebb)) 86 | 87 | # [3.44.0](https://github.com/ixartz/Next-js-Boilerplate/compare/v3.43.0...v3.44.0) (2024-04-02) 88 | 89 | 90 | ### Features 91 | 92 | * run migration only in development and eslint-disable need to be at the top ([db94f31](https://github.com/ixartz/Next-js-Boilerplate/commit/db94f31615cd5ffcc3739ab56572646f7ce1f177)) 93 | 94 | # [3.43.0](https://github.com/ixartz/Next-js-Boilerplate/compare/v3.42.0...v3.43.0) (2024-03-07) 95 | 96 | 97 | ### Features 98 | 99 | * use eslintrc.json and give release.yml permission in GitHub Actions ([a329518](https://github.com/ixartz/Next-js-Boilerplate/commit/a32951811e157696ab915eebd6b71b09f49ccb83)) 100 | 101 | # [3.42.0](https://github.com/ixartz/Next-js-Boilerplate/compare/v3.41.0...v3.42.0) (2024-02-22) 102 | 103 | 104 | ### Features 105 | 106 | * remove import React when it's not needed ([a7082d3](https://github.com/ixartz/Next-js-Boilerplate/commit/a7082d3492d9a426218829f86554b2aeda9da8fd)) 107 | 108 | # [3.41.0](https://github.com/ixartz/Next-js-Boilerplate/compare/v3.40.0...v3.41.0) (2024-02-09) 109 | 110 | 111 | ### Features 112 | 113 | * add target blank for links going outside ([37ba36e](https://github.com/ixartz/Next-js-Boilerplate/commit/37ba36e5e3815d87cf882dc9aaf8b69b5849b49e)) 114 | * make the index page of the boilerplate cleaner ([f3a3f9b](https://github.com/ixartz/Next-js-Boilerplate/commit/f3a3f9b306bfaed85058d59cd15e62db158468ca)) 115 | 116 | # [3.40.0](https://github.com/ixartz/Next-js-Boilerplate/compare/v3.39.0...v3.40.0) (2024-02-07) 117 | 118 | 119 | ### Features 120 | 121 | * add pino.js as Logger ([1d35f43](https://github.com/ixartz/Next-js-Boilerplate/commit/1d35f43efd5e250498d2d30654be672e4e2d91c9)) 122 | 123 | # [3.39.0](https://github.com/ixartz/Next-js-Boilerplate/compare/v3.38.0...v3.39.0) (2024-02-07) 124 | 125 | 126 | ### Features 127 | 128 | * add preferType on VSCode ([a55bc6a](https://github.com/ixartz/Next-js-Boilerplate/commit/a55bc6a4b543c47ec491c5a84806f62c93dc1aa4)) 129 | 130 | # [3.38.0](https://github.com/ixartz/Next-js-Boilerplate/compare/v3.37.0...v3.38.0) (2024-01-19) 131 | 132 | 133 | ### Features 134 | 135 | * update to Next.js 14.1 ([5dab52d](https://github.com/ixartz/Next-js-Boilerplate/commit/5dab52d58648a12b5779f04d642ad4b2010931b0)) 136 | 137 | # [3.37.0](https://github.com/ixartz/Next-js-Boilerplate/compare/v3.36.0...v3.37.0) (2024-01-13) 138 | 139 | 140 | ### Features 141 | 142 | * add environment variables for one click deploy Netlify ([5becdbf](https://github.com/ixartz/Next-js-Boilerplate/commit/5becdbf59f43fdfe893c5b7b62cac1246787a07e)) 143 | 144 | # [3.36.0](https://github.com/ixartz/Next-js-Boilerplate/compare/v3.35.0...v3.36.0) (2024-01-10) 145 | 146 | 147 | ### Features 148 | 149 | * prod environement use the same method to migrate ([f6cfe7f](https://github.com/ixartz/Next-js-Boilerplate/commit/f6cfe7fa7583621c9161aa478f1d958d5c93c083)) 150 | 151 | 152 | ### Reverts 153 | 154 | * add back process.env.NODE_ENV check in README file for migrate ([853f3dc](https://github.com/ixartz/Next-js-Boilerplate/commit/853f3dc4cbade618902b382023fe6a6a8e947082)) 155 | * only run migration in development, if it run in production, it also run during the build ([c94a600](https://github.com/ixartz/Next-js-Boilerplate/commit/c94a6007b20f71fe10b10c76a05659364ee920ff)) 156 | 157 | # [3.35.0](https://github.com/ixartz/Next-js-Boilerplate/compare/v3.34.0...v3.35.0) (2024-01-07) 158 | 159 | 160 | ### Features 161 | 162 | * automatically run migrate in DB instead of running in NPM scripts ([b202686](https://github.com/ixartz/Next-js-Boilerplate/commit/b202686687a41eb38cf92a0451f03b5f0a854a2d)) 163 | * e2e tests run against next start with production code ([a57f724](https://github.com/ixartz/Next-js-Boilerplate/commit/a57f72402c459b75aec65472db7030557974643b)) 164 | * jest fail on console error and warn ([2dd92f2](https://github.com/ixartz/Next-js-Boilerplate/commit/2dd92f2db19df25210f0aa6eb8b9c44136a16ab7)) 165 | 166 | 167 | ### Reverts 168 | 169 | * change related to running playwright with next start ([1a2d0b6](https://github.com/ixartz/Next-js-Boilerplate/commit/1a2d0b6473e6e7b4965c7df353d39645a8688273)) 170 | * change related to running playwright with next start ([e9e0c17](https://github.com/ixartz/Next-js-Boilerplate/commit/e9e0c1790a8e76b51ee8a0b1012cc3492349bd1b)) 171 | 172 | # [3.34.0](https://github.com/ixartz/Next-js-Boilerplate/compare/v3.33.0...v3.34.0) (2024-01-06) 173 | 174 | 175 | ### Features 176 | 177 | * add type definition in Postcss config ([07906ff](https://github.com/ixartz/Next-js-Boilerplate/commit/07906ff20a7c8d2b0c24cc1f33c93b0bc541b9c3)) 178 | * change commitlint config from JS to TS ([6509805](https://github.com/ixartz/Next-js-Boilerplate/commit/650980539eb16c4ef0f5d1ed3e833cdb08faaf86)) 179 | * change jest config extension from js to TypeScript ([1cdea44](https://github.com/ixartz/Next-js-Boilerplate/commit/1cdea44c2a193e9df792dc997f6aa5304e043ff6)) 180 | * change nodeResolution to the new bundler from TypeScript 5.0 ([59282a2](https://github.com/ixartz/Next-js-Boilerplate/commit/59282a2f028a10b841f4af42248e4ecd2c41c080)) 181 | * convert Tailwind config file from JS to TS ([aff3b27](https://github.com/ixartz/Next-js-Boilerplate/commit/aff3b276c6b857570c3ec0b68de3cd5efaaeebbd)) 182 | 183 | # [3.33.0](https://github.com/ixartz/Next-js-Boilerplate/compare/v3.32.1...v3.33.0) (2024-01-03) 184 | 185 | 186 | ### Features 187 | 188 | * enable SWC compiler in Storybook ([5b4c61e](https://github.com/ixartz/Next-js-Boilerplate/commit/5b4c61ea11164b6e5853cefe363d2d433cda374d)) 189 | 190 | ## [3.32.1](https://github.com/ixartz/Next-js-Boilerplate/compare/v3.32.0...v3.32.1) (2023-12-27) 191 | 192 | 193 | ### Bug Fixes 194 | 195 | * typo in en.json file for Portfolio word ([4d42b3d](https://github.com/ixartz/Next-js-Boilerplate/commit/4d42b3d11feeb1134961c0c688a6659b5e88364e)) 196 | 197 | # [3.32.0](https://github.com/ixartz/Next-js-Boilerplate/compare/v3.31.0...v3.32.0) (2023-12-22) 198 | 199 | 200 | ### Features 201 | 202 | * add code coverage reporting with Codecov ([08abd23](https://github.com/ixartz/Next-js-Boilerplate/commit/08abd23acbb5fb770046900901a367d60f18695e)) 203 | 204 | # [3.31.0](https://github.com/ixartz/Next-js-Boilerplate/compare/v3.30.1...v3.31.0) (2023-12-20) 205 | 206 | 207 | ### Features 208 | 209 | * add FIXME tag for Sentry configuration ([2eceef1](https://github.com/ixartz/Next-js-Boilerplate/commit/2eceef14257232c89f625acfe475c1aa7f220e46)) 210 | * add Sentry and launch spotlight.js in dev mode ([a1326ae](https://github.com/ixartz/Next-js-Boilerplate/commit/a1326aebb4ade33dc8a4429e749fb482ed906754)) 211 | * add spotlight ([34086c1](https://github.com/ixartz/Next-js-Boilerplate/commit/34086c1b8636bdc391c31ceed062a1e858d81539)) 212 | * enable Sentry Spotlight only in development mode ([62cc01a](https://github.com/ixartz/Next-js-Boilerplate/commit/62cc01ab2e1ae5594a4b91f931f313a904ff4b7d)) 213 | * ignore technical exception throw by React RSC in Sentry ([4bf9503](https://github.com/ixartz/Next-js-Boilerplate/commit/4bf95038600a28ea3e98e84dabec4df5fd9af609)) 214 | * in global error get locale in params and set in html lang attribute ([c3b4d25](https://github.com/ixartz/Next-js-Boilerplate/commit/c3b4d25d3be6a5ceed48f2d365bd14e44ff9b114)) 215 | 216 | ## [3.30.1](https://github.com/ixartz/Next-js-Boilerplate/compare/v3.30.0...v3.30.1) (2023-12-17) 217 | 218 | 219 | ### Bug Fixes 220 | 221 | * api routes not found after apply intl middleware ([4650a5e](https://github.com/ixartz/Next-js-Boilerplate/commit/4650a5e293716dee7704c6082839aaf94b63e7ad)), closes [#209](https://github.com/ixartz/Next-js-Boilerplate/issues/209) 222 | 223 | # [3.30.0](https://github.com/ixartz/Next-js-Boilerplate/compare/v3.29.0...v3.30.0) (2023-12-12) 224 | 225 | 226 | ### Features 227 | 228 | * add GitHub Actions to sync with Crowdin ([ccc86e9](https://github.com/ixartz/Next-js-Boilerplate/commit/ccc86e9e4df89dadd3214ae167972038f44108a6)) 229 | 230 | # [3.29.0](https://github.com/ixartz/Next-js-Boilerplate/compare/v3.28.0...v3.29.0) (2023-12-08) 231 | 232 | 233 | ### Features 234 | 235 | * add i18n support for client component and typesafety for i18n keys ([2d86247](https://github.com/ixartz/Next-js-Boilerplate/commit/2d862478414c4e6cf06e287acbef50369ef9a119)) 236 | * add i18n support for Dashboard url used in Clerk ([12b89bc](https://github.com/ixartz/Next-js-Boilerplate/commit/12b89bcfa1cae76872fc1504960a5ee417ef5eea)) 237 | * add i18n with Clerk components and remove custom style in global.css file ([5e1af6c](https://github.com/ixartz/Next-js-Boilerplate/commit/5e1af6c9a83cc6988c68fd761bf4945a2e0cdb9c)) 238 | * add i18n with next-intl ([1f43eb2](https://github.com/ixartz/Next-js-Boilerplate/commit/1f43eb247ad8591fef3aa8a34d112dd804eec4c3)) 239 | * add locale switcher UI to change lang ([13b40e3](https://github.com/ixartz/Next-js-Boilerplate/commit/13b40e32d265d341da1cf723c1af36f3ea53e7e1)) 240 | * add metatags in App Router for page migrated from Pages Router ([ce8c277](https://github.com/ixartz/Next-js-Boilerplate/commit/ce8c2770c41abcc3c866d7320de6ef4d8a541715)) 241 | * add support i18n for authMiddleware ([8651d36](https://github.com/ixartz/Next-js-Boilerplate/commit/8651d36279512b0f5e008341916110a8ee6f167a)) 242 | * add tests for page in App Router ([6a722a1](https://github.com/ixartz/Next-js-Boilerplate/commit/6a722a1fec7a236973f794edc6583a245ebb4747)) 243 | * convert all hard coded text and translate in french ([0c3b1b2](https://github.com/ixartz/Next-js-Boilerplate/commit/0c3b1b2f9a8ae5c0d34cb6f3a227a907aca00342)) 244 | * i18n for page metatag ([5e7676d](https://github.com/ixartz/Next-js-Boilerplate/commit/5e7676de0d58238de1d46e662c3c8e6e00bd2c5b)) 245 | * link in BaseTemplate replaced margin with gap ([28b6ff2](https://github.com/ixartz/Next-js-Boilerplate/commit/28b6ff24577b5d4338a7da068e06070c7f50f195)) 246 | * migreate the index page from Page Rotuer to App Router ([fd3e82c](https://github.com/ixartz/Next-js-Boilerplate/commit/fd3e82c2ff837951277a8300fd95f15294b9290a)) 247 | * move messages folder to locales ([305e385](https://github.com/ixartz/Next-js-Boilerplate/commit/305e38504939008ecfbbd3bfb6deaf052e57eae7)) 248 | * remove Page router and migrate about page to App Router ([3965cbf](https://github.com/ixartz/Next-js-Boilerplate/commit/3965cbf89a67a64272b895809a31791ccf383b57)) 249 | * translate text in dashboard layout ([8119f1d](https://github.com/ixartz/Next-js-Boilerplate/commit/8119f1db63853f83710a6cc1f3135b45bc209809)) 250 | 251 | 252 | ### Reverts 253 | 254 | * add back NEXT_PUBLIC_CLERK_SIGN_IN_URL in the previous location ([16ae2ef](https://github.com/ixartz/Next-js-Boilerplate/commit/16ae2ef3a7b2800a3ac4d847bb7afa70743ee805)) 255 | * add back style for a tag link ([c12a7bd](https://github.com/ixartz/Next-js-Boilerplate/commit/c12a7bd400c875a115eefe2a9921db9e36bf644d)) 256 | * use percy/cli 1.27.4 instead of 1.27.5, impossible to upload snapshort with 1.27.5 ([73f8a0b](https://github.com/ixartz/Next-js-Boilerplate/commit/73f8a0b0e9c69f83e5c5a2b51f52159fcc43c654)) 257 | 258 | # [3.28.0](https://github.com/ixartz/Next-js-Boilerplate/compare/v3.27.0...v3.28.0) (2023-11-22) 259 | 260 | 261 | ### Features 262 | 263 | * rename custom SignOutButton to LogOutButton to avoid confusion with Clerk SignOutButton ([183301b](https://github.com/ixartz/Next-js-Boilerplate/commit/183301b5e87bfa4479727c295e83b45b923454a0)) 264 | * use custom SignOutButton to apply custom CSS styles, unified with other nav links ([35094bf](https://github.com/ixartz/Next-js-Boilerplate/commit/35094bf038f0eae6e7e2d77238840c97cc7adabe)) 265 | 266 | # [3.27.0](https://github.com/ixartz/Next-js-Boilerplate/compare/v3.26.0...v3.27.0) (2023-11-20) 267 | 268 | 269 | ### Features 270 | 271 | * add PRODUCTION_URL environment variable and throw error when targetURL doesn't exist ([8134dee](https://github.com/ixartz/Next-js-Boilerplate/commit/8134dee84205e297020851bad4c81cf3906e7dfe)) 272 | * unified e2e tests for Checkly and playwright ([afa53f5](https://github.com/ixartz/Next-js-Boilerplate/commit/afa53f56b51f9a537131ceb046f90ea59c17dd71)) 273 | * use target URl instead of baseURL for checkly ([4fd61ed](https://github.com/ixartz/Next-js-Boilerplate/commit/4fd61edc77e1ef0d457cb829a89545f7dab47210)) 274 | 275 | # [3.26.0](https://github.com/ixartz/Next-js-Boilerplate/compare/v3.25.0...v3.26.0) (2023-11-15) 276 | 277 | 278 | ### Features 279 | 280 | * add a new GitHub Actions file for Checkly ([2109b1c](https://github.com/ixartz/Next-js-Boilerplate/commit/2109b1c75359a9ce89c2c0773fd65e78e1439403)) 281 | * add aria-label to fix jsx-a11y/control-has-associated-label error ([47e4ff4](https://github.com/ixartz/Next-js-Boilerplate/commit/47e4ff4f811b4e2071b9ba31f5c0ad1367b0caba)) 282 | * add email alert channel for checkly ([d1a4380](https://github.com/ixartz/Next-js-Boilerplate/commit/d1a43801d64fa261bdb252cf83dc289742f37294)) 283 | * add email channel in Checkly configuration to send emails when failing ([2019591](https://github.com/ixartz/Next-js-Boilerplate/commit/20195919d8a07f4e3cc0b7884e7d972de2935a94)) 284 | * create checkly config with a random working test ([32255b0](https://github.com/ixartz/Next-js-Boilerplate/commit/32255b0770ec5be84e9fd3321154329c556aedee)) 285 | * remove eslint rule customization in VSCode and use min(1) instead of nonempty (deprecated) ([9982a2d](https://github.com/ixartz/Next-js-Boilerplate/commit/9982a2d94fe7854eefaa754e9f41cf4735a81c86)) 286 | * update package-lock.json to fix CI ([1fff7ef](https://github.com/ixartz/Next-js-Boilerplate/commit/1fff7efe7295a9ee750b9f05af1a670db7bda733)) 287 | 288 | # [3.25.0](https://github.com/ixartz/Next-js-Boilerplate/compare/v3.24.0...v3.25.0) (2023-10-30) 289 | 290 | 291 | ### Features 292 | 293 | * release a new version for Next.js 14 and update README file ([4be2485](https://github.com/ixartz/Next-js-Boilerplate/commit/4be24850b75b9ca896e9e5546b8357745b128398)) 294 | 295 | # [3.24.0](https://github.com/ixartz/Next-js-Boilerplate/compare/v3.23.0...v3.24.0) (2023-10-24) 296 | 297 | 298 | ### Features 299 | 300 | * make guestbook endpoint avaiable to signed out users ([10b4d81](https://github.com/ixartz/Next-js-Boilerplate/commit/10b4d814d477e3475569537b1ef01a86b68c9a43)) 301 | 302 | # [3.23.0](https://github.com/ixartz/Next-js-Boilerplate/compare/v3.22.0...v3.23.0) (2023-10-12) 303 | 304 | 305 | ### Features 306 | 307 | * add playwright extension in VSCode ([956d1a8](https://github.com/ixartz/Next-js-Boilerplate/commit/956d1a8ec70c6a1214c72a115f0378507aa1b436)) 308 | * add playwright plugin in ESLint ([b2486f1](https://github.com/ixartz/Next-js-Boilerplate/commit/b2486f1b1090c458115b873ddc5bffa8ecaf8412)) 309 | * add Playwright: config, first test and dependency ([f054ea2](https://github.com/ixartz/Next-js-Boilerplate/commit/f054ea264bab3376ab7f86b0a0fdc1b6a4e98350)) 310 | * remove all Cypress related files and configurations ([9fe8271](https://github.com/ixartz/Next-js-Boilerplate/commit/9fe8271e667b819910702803f5489e99766fe9ff)) 311 | 312 | 313 | ### Reverts 314 | 315 | * the failing test in Navigation spec ([28996f5](https://github.com/ixartz/Next-js-Boilerplate/commit/28996f59d2f02562761609348000d55776365f7e)) 316 | 317 | # [3.22.0](https://github.com/ixartz/Next-js-Boilerplate/compare/v3.21.0...v3.22.0) (2023-10-02) 318 | 319 | 320 | ### Features 321 | 322 | * remove basePath in Next.js configuration ([7f9a0e6](https://github.com/ixartz/Next-js-Boilerplate/commit/7f9a0e6ed42aec7d9ec500531b7f519dc11a5ec9)) 323 | * remove no-img-element and use Next.js built-in 4 | Next js starter banner 5 |

6 | 7 | 🚀 Boilerplate and Starter for Next.js with App Router support, Tailwind CSS, and TypeScript ⚡️ Prioritizing developer experience first: Next.js, TypeScript, ESLint, Prettier, Husky, Lint-Staged, Jest (replaced by Vitest), Testing Library, Commitlint, VSCode, PostCSS, Tailwind CSS, Authentication with [Clerk](https://clerk.com?utm_source=github&utm_medium=sponsorship&utm_campaign=nextjs-boilerplate), Database with DrizzleORM (SQLite, PostgreSQL, and MySQL) and [Turso](https://turso.tech/?utm_source=nextjsstarterbp), Error Monitoring with [Sentry](https://sentry.io/for/nextjs/?utm_source=github&utm_medium=paid-community&utm_campaign=general-fy25q1-nextjs&utm_content=github-banner-nextjsboilerplate-logo), Logging with Pino.js and Log Management, Monitoring as Code, Storybook, Multi-language (i18n), and more. Ready for Next.js 15. 8 | 9 | Clone this project and use it to create your own [Next.js](https://nextjs.org) project. You can check a [Next js templates demo](https://creativedesignsguru.com/demo/Nextjs-Boilerplate/). 10 | 11 | ## Sponsors 12 | 13 | 14 | 15 | 24 | 33 | 42 | 43 | 44 | 60 | 69 | 78 | 79 | 80 | 89 | 94 | 95 | 96 | 101 | 102 |
16 | 17 | 18 | 19 | 20 | Clerk – Authentication & User Management for Next.js 21 | 22 | 23 | 25 | 26 | 27 | 28 | 29 | Turso - SQLite for Production 30 | 31 | 32 | 34 | 35 | 36 | 37 | 38 | Crowdin 39 | 40 | 41 |
45 | 46 | 47 | 48 | 49 | Sentry 50 | 51 | 52 | 53 | 54 | 55 | 56 | Codecov 57 | 58 | 59 | 61 | 62 | 63 | 64 | 65 | PostHog 66 | 67 | 68 | 70 | 71 | 72 | 73 | 74 | Better Stack 75 | 76 | 77 |
81 | 82 | 83 | 84 | 85 | Checkly 86 | 87 | 88 | 90 | 91 | React SaaS Boilerplate Next.js 92 | 93 |
97 | 98 | Add your logo here 99 | 100 |
103 | 104 | ### Features 105 | 106 | Developer experience first, extremely flexible code structure and only keep what you need: 107 | 108 | - ⚡ [Next.js](https://nextjs.org) with App Router support 109 | - 🔥 Type checking [TypeScript](https://www.typescriptlang.org) 110 | - 💎 Integrate with [Tailwind CSS](https://tailwindcss.com) 111 | - ✅ Strict Mode for TypeScript and React 18 112 | - 🔒 Authentication with [Clerk](https://clerk.com?utm_source=github&utm_medium=sponsorship&utm_campaign=nextjs-boilerplate): Sign up, Sign in, Sign out, Forgot password, Reset password, and more. 113 | - 👤 Passwordless Authentication with Magic Links, Multi-Factor Auth (MFA), Social Auth (Google, Facebook, Twitter, GitHub, Apple, and more), Passwordless login with Passkeys, User Impersonation 114 | - 📦 Type-safe ORM with DrizzleORM, compatible with SQLite, PostgreSQL, and MySQL 115 | - 💽 Global Database with [Turso](https://turso.tech/?utm_source=nextjsstarterbp) 116 | - 🌐 Multi-language (i18n) with [next-intl](https://next-intl-docs.vercel.app/) and [Crowdin](https://l.crowdin.com/next-js) 117 | - ♻️ Type-safe environment variables with T3 Env 118 | - ⌨️ Form handling with React Hook Form 119 | - 🔴 Validation library with Zod 120 | - 📏 Linter with [ESLint](https://eslint.org) (default Next.js, Next.js Core Web Vitals, Tailwind CSS and Airbnb configuration) 121 | - 💖 Code Formatter with [Prettier](https://prettier.io) 122 | - 🦊 Husky for Git Hooks 123 | - 🚫 Lint-staged for running linters on Git staged files 124 | - 🚓 Lint git commit with Commitlint 125 | - 📓 Write standard compliant commit messages with Commitizen 126 | - 🦺 Unit Testing with Vitest and React Testing Library 127 | - 🧪 Integration and E2E Testing with Playwright 128 | - 👷 Run tests on pull request with GitHub Actions 129 | - 🎉 Storybook for UI development 130 | - 🚨 Error Monitoring with [Sentry](https://sentry.io/for/nextjs/?utm_source=github&utm_medium=paid-community&utm_campaign=general-fy25q1-nextjs&utm_content=github-banner-nextjsboilerplate-logo) 131 | - ☂️ Code coverage with [Codecov](https://about.codecov.io/codecov-free-trial/?utm_source=github&utm_medium=paid-community&utm_campaign=general-fy25q1-nextjs&utm_content=github-banner-nextjsboilerplate-logo) 132 | - 📝 Logging with Pino.js and Log Management with [Better Stack](https://betterstack.com/?utm_source=github&utm_medium=sponsorship&utm_campaign=next-js-boilerplate) 133 | - 🖥️ Monitoring as Code with [Checkly](https://www.checklyhq.com/?utm_source=github&utm_medium=sponsorship&utm_campaign=next-js-boilerplate) 134 | - 🎁 Automatic changelog generation with Semantic Release 135 | - 🔍 Visual testing with Percy (Optional) 136 | - 💡 Absolute Imports using `@` prefix 137 | - 🗂 VSCode configuration: Debug, Settings, Tasks and Extensions 138 | - 🤖 SEO metadata, JSON-LD and Open Graph tags 139 | - 🗺️ Sitemap.xml and robots.txt 140 | - ⌘ Database exploration with Drizzle Studio and CLI migration tool with Drizzle Kit 141 | - ⚙️ [Bundler Analyzer](https://www.npmjs.com/package/@next/bundle-analyzer) 142 | - 🌈 Include a FREE minimalist theme 143 | - 💯 Maximize lighthouse score 144 | 145 | Built-in feature from Next.js: 146 | 147 | - ☕ Minify HTML & CSS 148 | - 💨 Live reload 149 | - ✅ Cache busting 150 | 151 | ### Philosophy 152 | 153 | - Nothing is hidden from you, so you have the freedom to make the necessary adjustments to fit your needs and preferences. 154 | - Easy to customize 155 | - Minimal code 156 | - SEO-friendly 157 | - 🚀 Production-ready 158 | 159 | ### Requirements 160 | 161 | - Node.js 20+ and npm 162 | 163 | ### Getting started 164 | 165 | Run the following command on your local environment: 166 | 167 | ```shell 168 | git clone --depth=1 https://github.com/ixartz/Next-js-Boilerplate.git my-project-name 169 | cd my-project-name 170 | npm install 171 | ``` 172 | 173 | Then, you can run the project locally in development mode with live reload by executing: 174 | 175 | ```shell 176 | npm run dev 177 | ``` 178 | 179 | Open http://localhost:3000 with your favorite browser to see your project. 180 | 181 | ### Set up authentication 182 | 183 | Create a Clerk account at [Clerk.com](https://clerk.com?utm_source=github&utm_medium=sponsorship&utm_campaign=nextjs-boilerplate) and create a new application in Clerk Dashboard. Then, copy `NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY` and `CLERK_SECRET_KEY` into `.env.local` file (not tracked by Git): 184 | 185 | ```shell 186 | NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=your_clerk_pub_key 187 | CLERK_SECRET_KEY=your_clerk_secret_key 188 | ``` 189 | 190 | Now, you have a fully working authentication system with Next.js: Sign up, Sign in, Sign out, Forgot password, Reset password, Update profile, Update password, Update email, Delete account, and more. 191 | 192 | ### Set up remote database 193 | 194 | The project uses DrizzleORM, a type-safe ORM compatible with SQLite, PostgreSQL, and MySQL databases. By default, the project is set up to work seamlessly with libSQL, and for production purposes, it's integrated with [Turso](https://turso.tech/?utm_source=nextjsstarterbp). The Next.js Boilerplate also enables a smooth transition to an alternative database provider if your project requires it. 195 | 196 | First, you need to create a Turso account at [Turso.tech](https://turso.tech/?utm_source=nextjsstarterbp) and install the Turso CLI: 197 | 198 | ```shell 199 | brew install tursodatabase/tap/turso 200 | turso auth signup # Sign up to Turso 201 | ``` 202 | 203 | Then, create a new database: 204 | 205 | ```shell 206 | turso db create nextjs-boilerplate 207 | ``` 208 | 209 | Now, you need to update the `DATABASE_URL` in `.env` file with the database URL provided by Turso: 210 | 211 | ```shell 212 | turso db show nextjs-boilerplate --url 213 | 214 | # .env 215 | # DATABASE_URL=libsql://[RANDOM-CHARS]-[DB-NAME]-[ORG-NAME].turso.io 216 | ``` 217 | 218 | Finally, you also need to create a new environment variable `DATABASE_AUTH_TOKEN` in `.env.local` (not tracked by Git) with the auth token provided by Turso: 219 | 220 | ```shell 221 | turso db tokens create nextjs-boilerplate 222 | 223 | # .env.local 224 | # DATABASE_AUTH_TOKEN=[your-auth-token] 225 | ``` 226 | 227 | ### Translation (i18n) setup 228 | 229 | For translation, the project uses `next-intl` combined with [Crowdin](https://l.crowdin.com/next-js). As a developer, you only need to take care of the English (or another default language) version. Other languages are automatically generated and handled by Crowdin. You can use Crowdin to collaborate with your translation team or translate the messages yourself with the help of machine translation. 230 | 231 | To set up translation (i18n), create an account at [Crowdin.com](https://l.crowdin.com/next-js) and create a new project. In the newly created project, you will able to find the project ID. You'll also require to create a new Personal Access Tokens by going to Account Settings > API. Then, in your GitHub Actions, you need to define the following environment variables `CROWDIN_PROJECT_ID` and `CROWDIN_PERSONAL_TOKEN`. 232 | 233 | After defining the environment variables in your GitHub Actions, your localization files will be synchronized with Crowdin everytime you push a new commit to the `main` branch. 234 | 235 | ### Project structure 236 | 237 | ```shell 238 | . 239 | ├── README.md # README file 240 | ├── .github # GitHub folder 241 | ├── .husky # Husky configuration 242 | ├── .storybook # Storybook folder 243 | ├── .vscode # VSCode configuration 244 | ├── migrations # Database migrations 245 | ├── public # Public assets folder 246 | ├── scripts # Scripts folder 247 | ├── src 248 | │ ├── app # Next JS App (App Router) 249 | │ ├── components # React components 250 | │ ├── libs # 3rd party libraries configuration 251 | │ ├── locales # Locales folder (i18n messages) 252 | │ ├── models # Database models 253 | │ ├── styles # Styles folder 254 | │ ├── templates # Templates folder 255 | │ ├── types # Type definitions 256 | │ ├── utils # Utilities folder 257 | │ └── validations # Validation schemas 258 | ├── tests 259 | │ ├── e2e # E2E tests, also includes Monitoring as Code 260 | │ └── integration # Integration tests 261 | ├── tailwind.config.js # Tailwind CSS configuration 262 | └── tsconfig.json # TypeScript configuration 263 | ``` 264 | 265 | ### Customization 266 | 267 | You can easily configure Next js Boilerplate by making a search in the whole project with `FIXME:` for making quick customization. Here is some of the most important files to customize: 268 | 269 | - `public/apple-touch-icon.png`, `public/favicon.ico`, `public/favicon-16x16.png` and `public/favicon-32x32.png`: your website favicon, you can generate from https://favicon.io/favicon-converter/ 270 | - `src/utils/AppConfig.ts`: configuration file 271 | - `src/templates/BaseTemplate.tsx`: default theme 272 | - `next.config.mjs`: Next.js configuration 273 | - `.env`: default environment variables 274 | 275 | You have access to the whole code source if you need further customization. The provided code is only example for you to start your project. The sky is the limit 🚀. 276 | 277 | ### Commit Message Format 278 | 279 | The project enforces [Conventional Commits](https://www.conventionalcommits.org/) specification. This means that all your commit messages must be formatted according to the specification. To help you write commit messages, the project uses [Commitizen](https://github.com/commitizen/cz-cli), an interactive CLI that guides you through the commit process. To use it, run the following command: 280 | 281 | ```shell 282 | npm run commit 283 | ``` 284 | 285 | One of the benefits of using Conventional Commits is that it allows us to automatically generate a `CHANGELOG` file. It also allows us to automatically determine the next version number based on the types of commits that are included in a release. 286 | 287 | ### Testing 288 | 289 | All unit tests are located with the source code inside the same directory. So, it makes it easier to find them. The project uses Vitest and React Testing Library for unit testing. You can run the tests with: 290 | 291 | ```shell 292 | npm run test 293 | ``` 294 | 295 | ### Integration & E2E Testing 296 | 297 | The project uses Playwright for Integration and E2E testing. You can run the tests with: 298 | 299 | ```shell 300 | npx playwright install # Only for the first time in a new environment 301 | npm run test:e2e 302 | ``` 303 | 304 | ### Enable Edge runtime (optional) 305 | 306 | The App Router folder is compatible with the Edge runtime. You can enable it by uncommenting the following lines `src/app/layouts.tsx`: 307 | 308 | ```tsx 309 | // export const runtime = 'edge'; 310 | ``` 311 | 312 | For your information, the database migration is not compatible with the Edge runtime. So, you need to disable the automatic migration in `src/libs/DB.ts`: 313 | 314 | ```tsx 315 | if (process.env.NODE_ENV === 'development') { 316 | await migrate(db, { migrationsFolder: './migrations' }); 317 | } 318 | ``` 319 | 320 | After disabling it, you are required to run the migration manually with: 321 | 322 | ```shell 323 | npm run db:migrate 324 | ``` 325 | 326 | You also require to run the command each time you want to update the database schema. 327 | 328 | ### Deploy to production 329 | 330 | During the build process, the database migration is automatically executed. So, you don't need to run the migration manually. But, in your environment variable, `DATABASE_URL` and `DATABASE_AUTH_TOKEN` need to be defined. 331 | 332 | Then, you can generate a production build with: 333 | 334 | ```shell 335 | $ npm run build 336 | ``` 337 | 338 | It generates an optimized production build of the boilerplate. For testing the generated build, you can run: 339 | 340 | ```shell 341 | $ npm run start 342 | ``` 343 | 344 | You also need to defined the environment variables `CLERK_SECRET_KEY` using your own key. 345 | 346 | The command starts a local server with the production build. Then, you can now open http://localhost:3000 with your favorite browser to see the project. 347 | 348 | ### Error Monitoring 349 | 350 | The project uses [Sentry](https://sentry.io/for/nextjs/?utm_source=github&utm_medium=paid-community&utm_campaign=general-fy25q1-nextjs&utm_content=github-banner-nextjsboilerplate-logo) to monitor errors. For development environment, you don't need to do anything: Next.js Boilerplate is already configured to use Sentry and Spotlight (Sentry for Development). All errors will be automatically sent to your local Spotlight instance. So, you can try the Sentry experience locally. 351 | 352 | For production environment, you need to create a Sentry account and create a new project. Then, in `next.config.mjs`, you need to update the `org` and `project` attribute in `withSentryConfig` function. You also need to add your Sentry DSN in `sentry.client.config.ts`, `sentry.edge.config.ts` and `sentry.server.config.ts`. 353 | 354 | ### Code coverage 355 | 356 | Next.js Boilerplate relies on [Codecov](https://about.codecov.io/codecov-free-trial/?utm_source=github&utm_medium=paid-community&utm_campaign=general-fy25q1-nextjs&utm_content=github-banner-nextjsboilerplate-logo) for code coverage reporting solution. To use Codecov, create a Codecov account and connect it to your GitHub account. On your Codecov dashboard, it should display a list of your repositories. Select the repository you want to enable Codecov for and copy the token. Then, in your GitHub Actions, you need to define the `CODECOV_TOKEN` environment variable and paste the token you copied. 357 | 358 | Be sure to create the `CODECOV_TOKEN` as a Github Actions secret, do not paste it directly into your source code. 359 | 360 | ### Logging 361 | 362 | The project uses Pino.js for logging. By default, for development environment, the logs are displayed in the console. 363 | 364 | For production environment, the project is already integrated with [Better Stack](https://betterstack.com/?utm_source=github&utm_medium=sponsorship&utm_campaign=next-js-boilerplate) to manage and query your logs using SQL. To use Better Stack, you need to create a [Better Stack](https://betterstack.com/?utm_source=github&utm_medium=sponsorship&utm_campaign=next-js-boilerplate) account and create a new source: go to your Better Stack Logs Dashboard > Sources > Connect source. Then, you need to give a name to your source and select Node.js as the platform. 365 | 366 | After creating the source, you able to see your source token and copy it. Then, in your environment variabless, you can paste the token in `LOGTAIL_SOURCE_TOKEN` variable. Now, all your logs will be automatically sent and ingested by Better Stack. 367 | 368 | ### Checkly monitoring 369 | 370 | The project uses [Checkly](https://www.checklyhq.com/?utm_source=github&utm_medium=sponsorship&utm_campaign=next-js-boilerplate) to ensure that your production environment is always up and running. At regular intervals, Checkly runs the tests ending with `*.check.spec.ts` extension and notifies you if any of the tests fail. Additionally, you have the flexibility to execute tests across multiple locations to ensure that your application is available worldwide. 371 | 372 | To use Checkly, you must first create an account on [their website](https://www.checklyhq.com/?utm_source=github&utm_medium=sponsorship&utm_campaign=next-js-boilerplate). Once you have an account, you can set the `CHECKLY_API_KEY` environment variable in GitHub Actions by generating a new API key in the Checkly Dashboard. Additionally, you will need to define the `CHECKLY_ACCOUNT_ID`, which can also be found in your Checkly Dashboard under User Settings > General. 373 | 374 | To complete the setup, make sure to update the `checkly.config.ts` file with your own email address and production URL. 375 | 376 | ### Useful commands 377 | 378 | #### Bundle Analyzer 379 | 380 | Next.js Boilerplate comes with a built-in bundle analyzer. It can be used to analyze the size of your JavaScript bundles. To begin, run the following command: 381 | 382 | ```shell 383 | npm run build-stats 384 | ``` 385 | 386 | By running the command, it'll automatically open a new browser window with the results. 387 | 388 | #### Database Studio 389 | 390 | The project is already configured with Drizzle Studio to explore the database. You can run the following command to open the database studio: 391 | 392 | ```shell 393 | npm run db:studio 394 | ``` 395 | 396 | Then, you can open https://local.drizzle.studio with your favorite browser to explore your database. 397 | 398 | ### VSCode information (optional) 399 | 400 | If you are VSCode users, you can have a better integration with VSCode by installing the suggested extension in `.vscode/extension.json`. The starter code comes up with Settings for a seamless integration with VSCode. The Debug configuration is also provided for frontend and backend debugging experience. 401 | 402 | With the plugins installed on your VSCode, ESLint and Prettier can automatically fix the code and show you the errors. Same goes for testing, you can install VSCode Vitest extension to automatically run your tests and it also show the code coverage in context. 403 | 404 | Pro tips: if you need a project wide type checking with TypeScript, you can run a build with Cmd + Shift + B on Mac. 405 | 406 | ### Contributions 407 | 408 | Everyone is welcome to contribute to this project. Feel free to open an issue if you have question or found a bug. Totally open to any suggestions and improvements. 409 | 410 | ### License 411 | 412 | Licensed under the MIT License, Copyright © 2024 413 | 414 | See [LICENSE](LICENSE) for more information. 415 | 416 | ## Sponsors 417 | 418 | 419 | 420 | 429 | 438 | 447 | 448 | 449 | 465 | 474 | 483 | 484 | 485 | 494 | 499 | 500 | 501 | 506 | 507 |
421 | 422 | 423 | 424 | 425 | Clerk – Authentication & User Management for Next.js 426 | 427 | 428 | 430 | 431 | 432 | 433 | 434 | Turso - SQLite for Production 435 | 436 | 437 | 439 | 440 | 441 | 442 | 443 | Crowdin 444 | 445 | 446 |
450 | 451 | 452 | 453 | 454 | Sentry 455 | 456 | 457 | 458 | 459 | 460 | 461 | Codecov 462 | 463 | 464 | 466 | 467 | 468 | 469 | 470 | PostHog 471 | 472 | 473 | 475 | 476 | 477 | 478 | 479 | Better Stack 480 | 481 | 482 |
486 | 487 | 488 | 489 | 490 | Checkly 491 | 492 | 493 | 495 | 496 | React SaaS Boilerplate Next.js 497 | 498 |
502 | 503 | Add your logo here 504 | 505 |
508 | 509 | --- 510 | 511 | Made with ♥ by [CreativeDesignsGuru](https://creativedesignsguru.com) [![Twitter](https://img.shields.io/twitter/url/https/twitter.com/cloudposse.svg?style=social&label=Follow%20%40Ixartz)](https://twitter.com/ixartz) 512 | 513 | [![Sponsor Next JS Boilerplate](https://cdn.buymeacoffee.com/buttons/default-red.png)](https://github.com/sponsors/ixartz) 514 | -------------------------------------------------------------------------------- /checkly.config.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies */ 2 | import { defineConfig } from 'checkly'; 3 | import { EmailAlertChannel, Frequency } from 'checkly/constructs'; 4 | 5 | const emailChannel = new EmailAlertChannel('email-channel-1', { 6 | // FIXME: add your own email address, Checkly will send you an email notification if a check fails 7 | address: 'contact@creativedesignsguru.com', 8 | sendDegraded: true, 9 | }); 10 | 11 | export const config = defineConfig({ 12 | projectName: 'Next.js Boilerplate', 13 | logicalId: 'nextjs-boilerplate', 14 | repoUrl: 'https://github.com/ixartz/Next-js-Boilerplate', 15 | checks: { 16 | locations: ['us-east-1', 'eu-west-1'], 17 | tags: ['website'], 18 | runtimeId: '2023.09', 19 | environmentVariables: [ 20 | { 21 | key: 'PRODUCTION_URL', 22 | // FIXME: Add your own production URL 23 | value: 'https://google.com', 24 | }, 25 | ], 26 | browserChecks: { 27 | frequency: Frequency.EVERY_24H, 28 | testMatch: '**/tests/e2e/**/*.check.spec.ts', 29 | alertChannels: [emailChannel], 30 | }, 31 | }, 32 | cli: { 33 | runLocation: 'eu-west-1', 34 | reporters: ['list'], 35 | }, 36 | }); 37 | 38 | export default config; 39 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | status: 3 | patch: off 4 | -------------------------------------------------------------------------------- /commitlint.config.ts: -------------------------------------------------------------------------------- 1 | import type { UserConfig } from '@commitlint/types'; 2 | 3 | const Configuration: UserConfig = { 4 | extends: ['@commitlint/config-conventional'], 5 | }; 6 | 7 | export default Configuration; 8 | -------------------------------------------------------------------------------- /crowdin.yml: -------------------------------------------------------------------------------- 1 | # 2 | # Your Crowdin credentials 3 | # 4 | "project_id_env": "CROWDIN_PROJECT_ID" 5 | "api_token_env": "CROWDIN_PERSONAL_TOKEN" 6 | "base_path": "." 7 | # "base_url": "https://api.crowdin.com" 8 | 9 | # 10 | # Choose file structure in Crowdin 11 | # e.g. true or false 12 | # 13 | "preserve_hierarchy": true 14 | 15 | # 16 | # Files configuration 17 | # 18 | files: [ 19 | { 20 | # 21 | # Source files filter 22 | # e.g. "/resources/en/*.json" 23 | # 24 | "source": "/src/locales/en.json", 25 | 26 | # 27 | # Where translations will be placed 28 | # e.g. "/resources/%two_letters_code%/%original_file_name%" 29 | # 30 | "translation": "/src/locales/%two_letters_code%.json", 31 | 32 | # 33 | # File type 34 | # e.g. "json" 35 | # 36 | "type": "json", 37 | }, 38 | ] 39 | -------------------------------------------------------------------------------- /drizzle.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from 'drizzle-kit'; 2 | 3 | /** @type {import('drizzle-kit').Config} */ 4 | export default { 5 | out: './migrations', 6 | schema: './src/models/Schema.ts', 7 | dialect: 'sqlite', 8 | driver: 'turso', 9 | dbCredentials: { 10 | url: process.env.DATABASE_URL ?? '', 11 | authToken: process.env.DATABASE_AUTH_TOKEN ?? '', 12 | }, 13 | verbose: true, 14 | strict: true, 15 | } satisfies Config; 16 | -------------------------------------------------------------------------------- /lint-staged.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | '*.{js,jsx,ts,tsx}': ['eslint --fix', 'eslint'], 3 | '**/*.ts?(x)': () => 'npm run check-types', 4 | '*.{json,yaml}': ['prettier --write'], 5 | }; 6 | -------------------------------------------------------------------------------- /migrations/0000_init_db.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE `guestbook` ( 2 | `id` integer PRIMARY KEY NOT NULL, 3 | `username` text NOT NULL, 4 | `body` text NOT NULL, 5 | `created_at` integer DEFAULT (strftime('%s', 'now')), 6 | `updated_at` integer DEFAULT (strftime('%s', 'now')) 7 | ); 8 | -------------------------------------------------------------------------------- /migrations/meta/0000_snapshot.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "6", 3 | "dialect": "sqlite", 4 | "id": "d3078f57-71ae-450f-ba1c-af29b40f3a97", 5 | "prevId": "00000000-0000-0000-0000-000000000000", 6 | "tables": { 7 | "guestbook": { 8 | "name": "guestbook", 9 | "columns": { 10 | "id": { 11 | "name": "id", 12 | "type": "integer", 13 | "primaryKey": true, 14 | "notNull": true, 15 | "autoincrement": false 16 | }, 17 | "username": { 18 | "name": "username", 19 | "type": "text", 20 | "primaryKey": false, 21 | "notNull": true, 22 | "autoincrement": false 23 | }, 24 | "body": { 25 | "name": "body", 26 | "type": "text", 27 | "primaryKey": false, 28 | "notNull": true, 29 | "autoincrement": false 30 | }, 31 | "created_at": { 32 | "name": "created_at", 33 | "type": "integer", 34 | "primaryKey": false, 35 | "notNull": false, 36 | "autoincrement": false, 37 | "default": "(strftime('%s', 'now'))" 38 | }, 39 | "updated_at": { 40 | "name": "updated_at", 41 | "type": "integer", 42 | "primaryKey": false, 43 | "notNull": false, 44 | "autoincrement": false, 45 | "default": "(strftime('%s', 'now'))" 46 | } 47 | }, 48 | "indexes": {}, 49 | "foreignKeys": {}, 50 | "compositePrimaryKeys": {}, 51 | "uniqueConstraints": {} 52 | } 53 | }, 54 | "enums": {}, 55 | "_meta": { 56 | "schemas": {}, 57 | "tables": {}, 58 | "columns": {} 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /migrations/meta/_journal.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "6", 3 | "dialect": "sqlite", 4 | "entries": [ 5 | { 6 | "idx": 0, 7 | "version": "6", 8 | "when": 1716976326006, 9 | "tag": "0000_init_db", 10 | "breakpoints": true 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /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.mjs: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies, import/extensions */ 2 | import { fileURLToPath } from 'node:url'; 3 | 4 | import withBundleAnalyzer from '@next/bundle-analyzer'; 5 | import { withSentryConfig } from '@sentry/nextjs'; 6 | import createJiti from 'jiti'; 7 | import withNextIntl from 'next-intl/plugin'; 8 | 9 | const jiti = createJiti(fileURLToPath(import.meta.url)); 10 | 11 | jiti('./src/libs/Env'); 12 | 13 | const withNextIntlConfig = withNextIntl('./src/libs/i18n.ts'); 14 | 15 | const bundleAnalyzer = withBundleAnalyzer({ 16 | enabled: process.env.ANALYZE === 'true', 17 | }); 18 | 19 | /** @type {import('next').NextConfig} */ 20 | export default withSentryConfig( 21 | bundleAnalyzer( 22 | withNextIntlConfig({ 23 | eslint: { 24 | dirs: ['.'], 25 | }, 26 | poweredByHeader: false, 27 | reactStrictMode: true, 28 | }), 29 | ), 30 | { 31 | // For all available options, see: 32 | // https://github.com/getsentry/sentry-webpack-plugin#options 33 | 34 | // Suppresses source map uploading logs during build 35 | silent: true, 36 | // FIXME: Add your Sentry organization and project names 37 | org: 'nextjs-boilerplate-org', 38 | project: 'nextjs-boilerplate', 39 | }, 40 | { 41 | // For all available options, see: 42 | // https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/ 43 | 44 | // Upload a larger set of source maps for prettier stack traces (increases build time) 45 | widenClientFileUpload: true, 46 | 47 | // Transpiles SDK to be compatible with IE11 (increases bundle size) 48 | transpileClientSDK: true, 49 | 50 | // Routes browser requests to Sentry through a Next.js rewrite to circumvent ad-blockers (increases server load) 51 | tunnelRoute: '/monitoring', 52 | 53 | // Hides source maps from generated client bundles 54 | hideSourceMaps: true, 55 | 56 | // Automatically tree-shake Sentry logger statements to reduce bundle size 57 | disableLogger: true, 58 | 59 | // Enables automatic instrumentation of Vercel Cron Monitors. 60 | // See the following for more information: 61 | // https://docs.sentry.io/product/crons/ 62 | // https://vercel.com/docs/cron-jobs 63 | automaticVercelMonitors: true, 64 | }, 65 | ); 66 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "next-js-boilerplate", 3 | "version": "3.52.0", 4 | "scripts": { 5 | "dev:spotlight": "spotlight-sidecar", 6 | "dev:next": "next dev", 7 | "dev": "run-p dev:*", 8 | "build": "npm run db:migrate && next build", 9 | "start": "next start", 10 | "build-stats": "cross-env ANALYZE=true npm run build", 11 | "clean": "rimraf .next .swc out coverage", 12 | "lint": "next lint", 13 | "format": "next lint --fix && prettier '**/*.{json,yaml}' --write --ignore-path .gitignore", 14 | "check-types": "tsc --noEmit --pretty", 15 | "test": "vitest run", 16 | "commit": "cz", 17 | "db:generate": "drizzle-kit generate", 18 | "db:migrate": "dotenv -c -- drizzle-kit migrate", 19 | "db:studio": "dotenv -c -- drizzle-kit studio", 20 | "test:e2e": "playwright test", 21 | "storybook": "storybook dev -p 6006", 22 | "storybook:build": "storybook build", 23 | "storybook:serve": "http-server storybook-static --port 6006 --silent", 24 | "serve-storybook": "run-s storybook:*", 25 | "test-storybook:ci": "start-server-and-test serve-storybook http://127.0.0.1:6006 test-storybook", 26 | "prepare": "husky" 27 | }, 28 | "dependencies": { 29 | "@clerk/localizations": "^2.4.3", 30 | "@clerk/nextjs": "^5.1.3", 31 | "@hookform/resolvers": "^3.4.2", 32 | "@libsql/client": "^0.6.1", 33 | "@logtail/pino": "^0.4.21", 34 | "@sentry/nextjs": "^7.116.0", 35 | "@spotlightjs/spotlight": "^1.2.17", 36 | "@t3-oss/env-nextjs": "^0.10.1", 37 | "drizzle-orm": "^0.31.0", 38 | "next": "^14.2.3", 39 | "next-intl": "^3.14.1", 40 | "pino": "^8.21.0", 41 | "pino-pretty": "^10.3.1", 42 | "react": "~18.2.0", 43 | "react-dom": "~18.2.0", 44 | "react-hook-form": "^7.51.5", 45 | "sharp": "^0.33.4", 46 | "zod": "^3.23.8" 47 | }, 48 | "devDependencies": { 49 | "@commitlint/cli": "^19.3.0", 50 | "@commitlint/config-conventional": "^19.2.2", 51 | "@commitlint/cz-commitlint": "^19.2.0", 52 | "@faker-js/faker": "^8.4.1", 53 | "@next/bundle-analyzer": "^14.2.3", 54 | "@percy/cli": "1.28.7", 55 | "@percy/playwright": "^1.0.5", 56 | "@playwright/test": "^1.44.1", 57 | "@semantic-release/changelog": "^6.0.3", 58 | "@semantic-release/git": "^10.0.1", 59 | "@storybook/addon-essentials": "^8.1.5", 60 | "@storybook/addon-interactions": "^8.1.5", 61 | "@storybook/addon-links": "^8.1.5", 62 | "@storybook/addon-onboarding": "^8.1.5", 63 | "@storybook/blocks": "^8.1.5", 64 | "@storybook/nextjs": "^8.1.5", 65 | "@storybook/react": "^8.1.5", 66 | "@storybook/test": "^8.1.5", 67 | "@storybook/test-runner": "^0.18.2", 68 | "@testing-library/jest-dom": "^6.4.5", 69 | "@testing-library/react": "^15.0.7", 70 | "@types/node": "^20.13.0", 71 | "@types/react": "^18.3.3", 72 | "@typescript-eslint/eslint-plugin": "^7.11.0", 73 | "@typescript-eslint/parser": "^7.11.0", 74 | "@vitejs/plugin-react": "^4.3.0", 75 | "@vitest/coverage-v8": "^1.6.0", 76 | "@vitest/expect": "^1.6.0", 77 | "autoprefixer": "^10.4.19", 78 | "checkly": "^4.6.3", 79 | "commitizen": "^4.3.0", 80 | "cross-env": "^7.0.3", 81 | "cssnano": "^7.0.1", 82 | "dotenv-cli": "^7.4.2", 83 | "drizzle-kit": "^0.22.1", 84 | "eslint": "^8.57.0", 85 | "eslint-config-airbnb": "^19.0.4", 86 | "eslint-config-airbnb-typescript": "^18.0.0", 87 | "eslint-config-next": "^14.2.3", 88 | "eslint-config-prettier": "^9.1.0", 89 | "eslint-plugin-import": "^2.29.1", 90 | "eslint-plugin-jest-dom": "^5.4.0", 91 | "eslint-plugin-jest-formatting": "^3.1.0", 92 | "eslint-plugin-jsx-a11y": "^6.8.0", 93 | "eslint-plugin-playwright": "^1.6.2", 94 | "eslint-plugin-prettier": "^5.1.3", 95 | "eslint-plugin-react": "^7.34.2", 96 | "eslint-plugin-react-hooks": "^4.6.2", 97 | "eslint-plugin-simple-import-sort": "^12.1.0", 98 | "eslint-plugin-storybook": "^0.8.0", 99 | "eslint-plugin-tailwindcss": "^3.17.0", 100 | "eslint-plugin-testing-library": "^6.2.2", 101 | "eslint-plugin-unused-imports": "^3.2.0", 102 | "eslint-plugin-vitest": "~0.4.1", 103 | "http-server": "^14.1.1", 104 | "husky": "^9.0.11", 105 | "jiti": "^1.21.0", 106 | "jsdom": "^24.1.0", 107 | "lint-staged": "^15.2.5", 108 | "npm-run-all": "^4.1.5", 109 | "postcss": "^8.4.38", 110 | "prettier": "^3.2.5", 111 | "rimraf": "^5.0.7", 112 | "semantic-release": "^23.1.1", 113 | "start-server-and-test": "^2.0.3", 114 | "storybook": "^8.1.5", 115 | "tailwindcss": "^3.4.3", 116 | "typescript": "^5.4.5", 117 | "vite-tsconfig-paths": "^4.3.2", 118 | "vitest": "^1.6.0", 119 | "vitest-fail-on-console": "^0.7.0" 120 | }, 121 | "config": { 122 | "commitizen": { 123 | "path": "@commitlint/cz-commitlint" 124 | } 125 | }, 126 | "release": { 127 | "branches": [ 128 | "main" 129 | ], 130 | "plugins": [ 131 | [ 132 | "@semantic-release/commit-analyzer", 133 | { 134 | "preset": "conventionalcommits" 135 | } 136 | ], 137 | "@semantic-release/release-notes-generator", 138 | "@semantic-release/changelog", 139 | [ 140 | "@semantic-release/npm", 141 | { 142 | "npmPublish": false 143 | } 144 | ], 145 | "@semantic-release/git", 146 | "@semantic-release/github" 147 | ] 148 | }, 149 | "author": "Ixartz (https://github.com/ixartz)" 150 | } 151 | -------------------------------------------------------------------------------- /playwright.config.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies */ 2 | import { defineConfig, devices } from '@playwright/test'; 3 | 4 | // Use process.env.PORT by default and fallback to port 3000 5 | const PORT = process.env.PORT || 3000; 6 | 7 | // Set webServer.url and use.baseURL with the location of the WebServer respecting the correct set port 8 | const baseURL = `http://localhost:${PORT}`; 9 | 10 | // *.check.spec.ts files use ENVIRONMENT_URL instead of baseURL 11 | process.env.ENVIRONMENT_URL = baseURL; 12 | 13 | /** 14 | * See https://playwright.dev/docs/test-configuration. 15 | */ 16 | export default defineConfig({ 17 | testDir: './tests', 18 | // Timeout per test 19 | timeout: 30 * 1000, 20 | // Run tests in files in parallel on CI 21 | fullyParallel: !!process.env.CI, 22 | // Fail the build on CI if you accidentally left test.only in the source code. 23 | forbidOnly: !!process.env.CI, 24 | // Retry on CI only 25 | retries: process.env.CI ? 2 : 0, 26 | // Opt out of parallel tests on CI 27 | workers: process.env.CI ? 1 : undefined, 28 | // Limit the number of failures on CI to save resources 29 | maxFailures: process.env.CI ? 10 : undefined, 30 | // Reporter to use. See https://playwright.dev/docs/test-reporters 31 | reporter: process.env.CI ? 'github' : 'list', 32 | 33 | // Run your local dev server before starting the tests: 34 | // https://playwright.dev/docs/test-advanced#launching-a-development-web-server-during-the-tests 35 | webServer: { 36 | command: process.env.CI ? 'npm run start' : 'npm run dev:next', 37 | url: baseURL, 38 | timeout: 2 * 60 * 1000, 39 | reuseExistingServer: !process.env.CI, 40 | }, 41 | 42 | // Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. 43 | use: { 44 | // Use baseURL so to make navigations relative. 45 | // More information: https://playwright.dev/docs/api/class-testoptions#test-options-base-url 46 | baseURL, 47 | 48 | // Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer 49 | trace: 'on-first-retry', 50 | }, 51 | 52 | projects: [ 53 | { 54 | name: 'chromium', 55 | use: { ...devices['Desktop Chrome'] }, 56 | }, 57 | ...(process.env.CI 58 | ? [ 59 | { 60 | name: 'firefox', 61 | use: { ...devices['Desktop Firefox'] }, 62 | }, 63 | { 64 | name: 'webkit', 65 | use: { ...devices['Desktop Safari'] }, 66 | }, 67 | ] 68 | : []), 69 | ], 70 | }); 71 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | // Please do not use the array form (like ['tailwindcss', 'postcss-preset-env']) 2 | // it will create an unexpected error: Invalid PostCSS Plugin found: [0] 3 | 4 | /** @type {import('postcss-load-config').Config} */ 5 | module.exports = { 6 | plugins: { 7 | tailwindcss: {}, 8 | autoprefixer: {}, 9 | ...(process.env.NODE_ENV === 'production' ? { cssnano: {} } : {}), 10 | }, 11 | }; 12 | -------------------------------------------------------------------------------- /public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mqyqingfeng/next-app-demo/51e1edc74a1180e188802d6969a71fe390929140/public/apple-touch-icon.png -------------------------------------------------------------------------------- /public/assets/images/better-stack-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mqyqingfeng/next-app-demo/51e1edc74a1180e188802d6969a71fe390929140/public/assets/images/better-stack-dark.png -------------------------------------------------------------------------------- /public/assets/images/better-stack-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mqyqingfeng/next-app-demo/51e1edc74a1180e188802d6969a71fe390929140/public/assets/images/better-stack-white.png -------------------------------------------------------------------------------- /public/assets/images/checkly-logo-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mqyqingfeng/next-app-demo/51e1edc74a1180e188802d6969a71fe390929140/public/assets/images/checkly-logo-dark.png -------------------------------------------------------------------------------- /public/assets/images/checkly-logo-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mqyqingfeng/next-app-demo/51e1edc74a1180e188802d6969a71fe390929140/public/assets/images/checkly-logo-light.png -------------------------------------------------------------------------------- /public/assets/images/clerk-logo-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mqyqingfeng/next-app-demo/51e1edc74a1180e188802d6969a71fe390929140/public/assets/images/clerk-logo-dark.png -------------------------------------------------------------------------------- /public/assets/images/codecov-dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /public/assets/images/codecov-white.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /public/assets/images/crowdin-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mqyqingfeng/next-app-demo/51e1edc74a1180e188802d6969a71fe390929140/public/assets/images/crowdin-dark.png -------------------------------------------------------------------------------- /public/assets/images/crowdin-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mqyqingfeng/next-app-demo/51e1edc74a1180e188802d6969a71fe390929140/public/assets/images/crowdin-white.png -------------------------------------------------------------------------------- /public/assets/images/nextjs-starter-banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mqyqingfeng/next-app-demo/51e1edc74a1180e188802d6969a71fe390929140/public/assets/images/nextjs-starter-banner.png -------------------------------------------------------------------------------- /public/assets/images/nextlessjs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mqyqingfeng/next-app-demo/51e1edc74a1180e188802d6969a71fe390929140/public/assets/images/nextlessjs.png -------------------------------------------------------------------------------- /public/assets/images/sentry-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mqyqingfeng/next-app-demo/51e1edc74a1180e188802d6969a71fe390929140/public/assets/images/sentry-dark.png -------------------------------------------------------------------------------- /public/assets/images/sentry-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mqyqingfeng/next-app-demo/51e1edc74a1180e188802d6969a71fe390929140/public/assets/images/sentry-white.png -------------------------------------------------------------------------------- /public/assets/images/turso-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mqyqingfeng/next-app-demo/51e1edc74a1180e188802d6969a71fe390929140/public/assets/images/turso-dark.png -------------------------------------------------------------------------------- /public/assets/images/turso-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mqyqingfeng/next-app-demo/51e1edc74a1180e188802d6969a71fe390929140/public/assets/images/turso-light.png -------------------------------------------------------------------------------- /public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mqyqingfeng/next-app-demo/51e1edc74a1180e188802d6969a71fe390929140/public/favicon-16x16.png -------------------------------------------------------------------------------- /public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mqyqingfeng/next-app-demo/51e1edc74a1180e188802d6969a71fe390929140/public/favicon-32x32.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mqyqingfeng/next-app-demo/51e1edc74a1180e188802d6969a71fe390929140/public/favicon.ico -------------------------------------------------------------------------------- /sentry.client.config.ts: -------------------------------------------------------------------------------- 1 | // This file configures the initialization of Sentry on the client. 2 | // The config you add here will be used whenever a users loads a page in their browser. 3 | // https://docs.sentry.io/platforms/javascript/guides/nextjs/ 4 | 5 | import * as Sentry from '@sentry/nextjs'; 6 | import * as Spotlight from '@spotlightjs/spotlight'; 7 | 8 | Sentry.init({ 9 | // FIXME: Add your Sentry DSN 10 | // dsn: '___DSN___', 11 | 12 | // Adjust this value in production, or use tracesSampler for greater control 13 | tracesSampleRate: 1, 14 | 15 | // Setting this option to true will print useful information to the console while you're setting up Sentry. 16 | debug: false, 17 | 18 | replaysOnErrorSampleRate: 1.0, 19 | 20 | // This sets the sample rate to be 10%. You may want this to be 100% while 21 | // in development and sample at a lower rate in production 22 | replaysSessionSampleRate: 0.1, 23 | 24 | // You can remove this option if you're not planning to use the Sentry Session Replay feature: 25 | integrations: [ 26 | Sentry.replayIntegration({ 27 | // Additional Replay configuration goes in here, for example: 28 | maskAllText: true, 29 | blockAllMedia: true, 30 | }), 31 | ], 32 | }); 33 | 34 | if (process.env.NODE_ENV === 'development') { 35 | Spotlight.init(); 36 | } 37 | -------------------------------------------------------------------------------- /sentry.edge.config.ts: -------------------------------------------------------------------------------- 1 | // This file configures the initialization of Sentry for edge features (middleware, edge routes, and so on). 2 | // The config you add here will be used whenever one of the edge features is loaded. 3 | // Note that this config is unrelated to the Vercel Edge Runtime and is also required when running locally. 4 | // https://docs.sentry.io/platforms/javascript/guides/nextjs/ 5 | 6 | import * as Sentry from '@sentry/nextjs'; 7 | 8 | Sentry.init({ 9 | // FIXME: Add your Sentry DSN 10 | // dsn: '___DSN___', 11 | 12 | // Enable Spotlight in development 13 | spotlight: process.env.NODE_ENV === 'development', 14 | 15 | ignoreErrors: [ 16 | // Workaround for React RSC and Suspense boundaries: https://github.com/amannn/next-intl/issues/614#issuecomment-1862508393 17 | // Can be removed once the change is integrated into Sentry SDK. 18 | "This is not a real error! It's an implementation detail of `use`", 19 | ], 20 | 21 | // Adjust this value in production, or use tracesSampler for greater control 22 | tracesSampleRate: 1, 23 | 24 | // Setting this option to true will print useful information to the console while you're setting up Sentry. 25 | debug: false, 26 | }); 27 | -------------------------------------------------------------------------------- /sentry.server.config.ts: -------------------------------------------------------------------------------- 1 | // This file configures the initialization of Sentry on the server. 2 | // The config you add here will be used whenever the server handles a request. 3 | // https://docs.sentry.io/platforms/javascript/guides/nextjs/ 4 | 5 | import * as Sentry from '@sentry/nextjs'; 6 | 7 | Sentry.init({ 8 | // FIXME: Add your Sentry DSN 9 | // dsn: '___DSN___', 10 | 11 | // Enable Spotlight in development 12 | spotlight: process.env.NODE_ENV === 'development', 13 | 14 | ignoreErrors: [ 15 | // Workaround for React RSC and Suspense boundaries: https://github.com/amannn/next-intl/issues/614#issuecomment-1862508393 16 | // Can be removed once the change is integrated into Sentry SDK. 17 | "This is not a real error! It's an implementation detail of `use`", 18 | ], 19 | 20 | // Adjust this value in production, or use tracesSampler for greater control 21 | tracesSampleRate: 1, 22 | 23 | // Setting this option to true will print useful information to the console while you're setting up Sentry. 24 | debug: false, 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/[locale]/(auth)/(center)/layout.tsx: -------------------------------------------------------------------------------- 1 | import { auth } from '@clerk/nextjs/server'; 2 | import { redirect } from 'next/navigation'; 3 | 4 | export default function CenteredLayout(props: { children: React.ReactNode }) { 5 | const { userId } = auth(); 6 | 7 | if (userId) { 8 | redirect('/dashboard'); 9 | } 10 | 11 | return ( 12 |
13 | {props.children} 14 |
15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /src/app/[locale]/(auth)/(center)/sign-in/[[...sign-in]]/page.tsx: -------------------------------------------------------------------------------- 1 | import { SignIn } from '@clerk/nextjs'; 2 | import { getTranslations } from 'next-intl/server'; 3 | 4 | import { getI18nPath } from '@/utils/Helpers'; 5 | 6 | export async function generateMetadata(props: { params: { locale: string } }) { 7 | const t = await getTranslations({ 8 | locale: props.params.locale, 9 | namespace: 'SignIn', 10 | }); 11 | 12 | return { 13 | title: t('meta_title'), 14 | description: t('meta_description'), 15 | }; 16 | } 17 | 18 | const SignInPage = (props: { params: { locale: string } }) => ( 19 | 20 | ); 21 | 22 | export default SignInPage; 23 | -------------------------------------------------------------------------------- /src/app/[locale]/(auth)/(center)/sign-up/[[...sign-up]]/page.tsx: -------------------------------------------------------------------------------- 1 | import { SignUp } from '@clerk/nextjs'; 2 | import { getTranslations } from 'next-intl/server'; 3 | 4 | import { getI18nPath } from '@/utils/Helpers'; 5 | 6 | export async function generateMetadata(props: { params: { locale: string } }) { 7 | const t = await getTranslations({ 8 | locale: props.params.locale, 9 | namespace: 'SignUp', 10 | }); 11 | 12 | return { 13 | title: t('meta_title'), 14 | description: t('meta_description'), 15 | }; 16 | } 17 | 18 | const SignUpPage = (props: { params: { locale: string } }) => ( 19 | 20 | ); 21 | 22 | export default SignUpPage; 23 | -------------------------------------------------------------------------------- /src/app/[locale]/(auth)/dashboard/layout.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link'; 2 | import { useTranslations } from 'next-intl'; 3 | 4 | import LocaleSwitcher from '@/components/LocaleSwitcher'; 5 | import { LogOutButton } from '@/components/LogOutButton'; 6 | import { BaseTemplate } from '@/templates/BaseTemplate'; 7 | 8 | export default function DashboardLayout(props: { children: React.ReactNode }) { 9 | const t = useTranslations('DashboardLayout'); 10 | 11 | return ( 12 | 15 |
  • 16 | 20 | {t('dashboard_link')} 21 | 22 |
  • 23 |
  • 24 | 28 | {t('user_profile_link')} 29 | 30 |
  • 31 | 32 | } 33 | rightNav={ 34 | <> 35 |
  • 36 | 37 |
  • 38 | 39 |
  • 40 | 41 |
  • 42 | 43 | } 44 | > 45 | {props.children} 46 |
    47 | ); 48 | } 49 | -------------------------------------------------------------------------------- /src/app/[locale]/(auth)/dashboard/page.tsx: -------------------------------------------------------------------------------- 1 | import { getTranslations } from 'next-intl/server'; 2 | 3 | import { Hello } from '@/components/Hello'; 4 | 5 | export async function generateMetadata(props: { params: { locale: string } }) { 6 | const t = await getTranslations({ 7 | locale: props.params.locale, 8 | namespace: 'Dashboard', 9 | }); 10 | 11 | return { 12 | title: t('meta_title'), 13 | }; 14 | } 15 | 16 | const Dashboard = () => ( 17 |
    18 | 19 |
    20 | ); 21 | 22 | export default Dashboard; 23 | -------------------------------------------------------------------------------- /src/app/[locale]/(auth)/dashboard/user-profile/[[...user-profile]]/page.tsx: -------------------------------------------------------------------------------- 1 | import { UserProfile } from '@clerk/nextjs'; 2 | import { getTranslations } from 'next-intl/server'; 3 | 4 | import { getI18nPath } from '@/utils/Helpers'; 5 | 6 | export async function generateMetadata(props: { params: { locale: string } }) { 7 | const t = await getTranslations({ 8 | locale: props.params.locale, 9 | namespace: 'UserProfile', 10 | }); 11 | 12 | return { 13 | title: t('meta_title'), 14 | }; 15 | } 16 | 17 | const UserProfilePage = (props: { params: { locale: string } }) => ( 18 |
    19 | 22 |
    23 | ); 24 | 25 | export default UserProfilePage; 26 | -------------------------------------------------------------------------------- /src/app/[locale]/(auth)/layout.tsx: -------------------------------------------------------------------------------- 1 | import { enUS, zhCN } from '@clerk/localizations'; 2 | import { ClerkProvider } from '@clerk/nextjs'; 3 | 4 | export default function AuthLayout(props: { 5 | children: React.ReactNode; 6 | params: { locale: string }; 7 | }) { 8 | let clerkLocale = zhCN; 9 | let signInUrl = '/sign-in'; 10 | let signUpUrl = '/sign-up'; 11 | let dashboardUrl = '/dashboard'; 12 | 13 | if (props.params.locale === 'en') { 14 | clerkLocale = enUS; 15 | } 16 | 17 | if (props.params.locale !== 'zh') { 18 | signInUrl = `/${props.params.locale}${signInUrl}`; 19 | signUpUrl = `/${props.params.locale}${signUpUrl}`; 20 | dashboardUrl = `/${props.params.locale}${dashboardUrl}`; 21 | } 22 | 23 | return ( 24 | 31 | {props.children} 32 | 33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /src/app/[locale]/(unauth)/about/page.test.tsx: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react'; 2 | import { NextIntlClientProvider } from 'next-intl'; 3 | 4 | import messages from '@/locales/en.json'; 5 | 6 | import About from './page'; 7 | 8 | describe('About page', () => { 9 | describe('Render method', () => { 10 | it('should have a text starting with `Welcome to our About page`', () => { 11 | render( 12 | 13 | 14 | , 15 | ); 16 | 17 | const paragraph = screen.getByText(/Welcome to our About page/); 18 | 19 | expect(paragraph).toBeInTheDocument(); 20 | }); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /src/app/[locale]/(unauth)/about/page.tsx: -------------------------------------------------------------------------------- 1 | import Image from 'next/image'; 2 | import { useTranslations } from 'next-intl'; 3 | import { getTranslations } from 'next-intl/server'; 4 | 5 | export async function generateMetadata(props: { params: { locale: string } }) { 6 | const t = await getTranslations({ 7 | locale: props.params.locale, 8 | namespace: 'About', 9 | }); 10 | 11 | return { 12 | title: t('meta_title'), 13 | description: t('meta_description'), 14 | }; 15 | } 16 | 17 | export default function About() { 18 | const t = useTranslations('About'); 19 | 20 | return ( 21 | <> 22 |

    {t('about_paragraph')}

    23 | 24 |
    25 | {`${t('translation_powered_by')} `} 26 | 31 | Crowdin 32 | 33 |
    34 | 35 | 36 | Crowdin Translation Management System 43 | 44 | 45 | ); 46 | } 47 | -------------------------------------------------------------------------------- /src/app/[locale]/(unauth)/api/guestbook/route.ts: -------------------------------------------------------------------------------- 1 | import { eq, sql } from 'drizzle-orm'; 2 | import { NextResponse } from 'next/server'; 3 | 4 | import { db } from '@/libs/DB'; 5 | import { logger } from '@/libs/Logger'; 6 | import { guestbookSchema } from '@/models/Schema'; 7 | import { 8 | DeleteGuestbookValidation, 9 | EditGuestbookValidation, 10 | GuestbookValidation, 11 | } from '@/validations/GuestbookValidation'; 12 | 13 | export const POST = async (request: Request) => { 14 | const json = await request.json(); 15 | const parse = GuestbookValidation.safeParse(json); 16 | 17 | if (!parse.success) { 18 | return NextResponse.json(parse.error.format(), { status: 422 }); 19 | } 20 | 21 | try { 22 | const guestbook = await db 23 | .insert(guestbookSchema) 24 | .values(parse.data) 25 | .returning(); 26 | 27 | logger.info('A new guestbook has been created'); 28 | 29 | return NextResponse.json({ 30 | id: guestbook[0]?.id, 31 | }); 32 | } catch (error) { 33 | logger.error(error, 'An error occurred while creating a guestbook'); 34 | 35 | return NextResponse.json({}, { status: 500 }); 36 | } 37 | }; 38 | 39 | export const PUT = async (request: Request) => { 40 | const json = await request.json(); 41 | const parse = EditGuestbookValidation.safeParse(json); 42 | 43 | if (!parse.success) { 44 | return NextResponse.json(parse.error.format(), { status: 422 }); 45 | } 46 | 47 | try { 48 | await db 49 | .update(guestbookSchema) 50 | .set({ 51 | ...parse.data, 52 | updatedAt: sql`(strftime('%s', 'now'))`, 53 | }) 54 | .where(eq(guestbookSchema.id, parse.data.id)) 55 | .run(); 56 | 57 | logger.info('A guestbook entry has been updated'); 58 | 59 | return NextResponse.json({}); 60 | } catch (error) { 61 | logger.error(error, 'An error occurred while updating a guestbook'); 62 | 63 | return NextResponse.json({}, { status: 500 }); 64 | } 65 | }; 66 | 67 | export const DELETE = async (request: Request) => { 68 | const json = await request.json(); 69 | const parse = DeleteGuestbookValidation.safeParse(json); 70 | 71 | if (!parse.success) { 72 | return NextResponse.json(parse.error.format(), { status: 422 }); 73 | } 74 | 75 | try { 76 | await db 77 | .delete(guestbookSchema) 78 | .where(eq(guestbookSchema.id, parse.data.id)) 79 | .run(); 80 | 81 | logger.info('A guestbook entry has been deleted'); 82 | 83 | return NextResponse.json({}); 84 | } catch (error) { 85 | logger.error(error, 'An error occurred while deleting a guestbook'); 86 | 87 | return NextResponse.json({}, { status: 500 }); 88 | } 89 | }; 90 | -------------------------------------------------------------------------------- /src/app/[locale]/(unauth)/guestbook/page.tsx: -------------------------------------------------------------------------------- 1 | import Image from 'next/image'; 2 | import { useTranslations } from 'next-intl'; 3 | import { getTranslations } from 'next-intl/server'; 4 | import { Suspense } from 'react'; 5 | 6 | import { AddGuestbookForm } from '@/components/AddGuestbookForm'; 7 | import { GuestbookList } from '@/components/GuestbookList'; 8 | 9 | export async function generateMetadata(props: { params: { locale: string } }) { 10 | const t = await getTranslations({ 11 | locale: props.params.locale, 12 | namespace: 'Guestbook', 13 | }); 14 | 15 | return { 16 | title: t('meta_title'), 17 | description: t('meta_description'), 18 | }; 19 | } 20 | 21 | const Guestbook = () => { 22 | const t = useTranslations('Guestbook'); 23 | 24 | return ( 25 | <> 26 | 27 | 28 | {t('loading_guestbook')}

    }> 29 | 30 |
    31 | 32 |
    33 | {`${t('error_reporting_powered_by')} `} 34 | 39 | Sentry 40 | 41 |
    42 | 43 | 47 | Sentry 54 | 55 | 56 | ); 57 | }; 58 | 59 | export default Guestbook; 60 | -------------------------------------------------------------------------------- /src/app/[locale]/(unauth)/layout.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link'; 2 | import { useTranslations } from 'next-intl'; 3 | 4 | import LocaleSwitcher from '@/components/LocaleSwitcher'; 5 | import { BaseTemplate } from '@/templates/BaseTemplate'; 6 | 7 | export default function Layout(props: { children: React.ReactNode }) { 8 | const t = useTranslations('RootLayout'); 9 | 10 | return ( 11 | 14 |
  • 15 | 19 | {t('home_link')} 20 | 21 |
  • 22 |
  • 23 | 27 | {t('about_link')} 28 | 29 |
  • 30 |
  • 31 | 35 | {t('guestbook_link')} 36 | 37 |
  • 38 |
  • 39 | 43 | {t('portfolio_link')} 44 | 45 |
  • 46 |
  • 47 | 51 | GitHub 52 | 53 |
  • 54 | 55 | } 56 | rightNav={ 57 | <> 58 |
  • 59 | 63 | {t('sign_in_link')} 64 | 65 |
  • 66 | 67 |
  • 68 | 72 | {t('sign_up_link')} 73 | 74 |
  • 75 | 76 |
  • 77 | 78 |
  • 79 | 80 | } 81 | > 82 |
    {props.children}
    83 |
    84 | ); 85 | } 86 | -------------------------------------------------------------------------------- /src/app/[locale]/(unauth)/page.test.tsx: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react'; 2 | import { NextIntlClientProvider } from 'next-intl'; 3 | 4 | import messages from '@/locales/en.json'; 5 | 6 | import Index from './page'; 7 | 8 | describe('Index page', () => { 9 | describe('Render method', () => { 10 | it('should have h1 tag', () => { 11 | render( 12 | 13 | 14 | , 15 | ); 16 | 17 | const heading = screen.getByRole('heading', { 18 | name: /Boilerplate Code/, 19 | }); 20 | 21 | expect(heading).toBeInTheDocument(); 22 | }); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /src/app/[locale]/(unauth)/page.tsx: -------------------------------------------------------------------------------- 1 | import { getTranslations } from 'next-intl/server'; 2 | 3 | import { Sponsors } from '@/components/Sponsors'; 4 | 5 | export async function generateMetadata(props: { params: { locale: string } }) { 6 | const t = await getTranslations({ 7 | locale: props.params.locale, 8 | namespace: 'Index', 9 | }); 10 | 11 | return { 12 | title: t('meta_title'), 13 | description: t('meta_description'), 14 | }; 15 | } 16 | 17 | export default function Index() { 18 | return ( 19 | <> 20 |

    21 | Looking for a SaaS Boilerplate?{' '} 22 | 26 | Next.js Boilerplate SaaS 27 | {' '} 28 | can help you build one. 29 |

    30 |

    31 | Follow{' '} 32 | 37 | @Ixartz on Twitter 38 | {' '} 39 | for updates and more information about the boilerplate. 40 |

    41 |

    42 | Our sponsors' exceptional support has made this project possible. 43 | Their services integrate seamlessly with the boilerplate, and we 44 | recommend trying them out. 45 |

    46 |

    Sponsors

    47 | 48 |

    49 | Boilerplate Code for Your Next.js Project with Tailwind CSS 50 |

    51 |

    52 | 53 | 🚀 54 | {' '} 55 | Next.js Boilerplate is a developer-friendly starter code for Next.js 56 | projects, built with Tailwind CSS, and TypeScript.{' '} 57 | 58 | ⚡️ 59 | {' '} 60 | Made with developer experience first: Next.js, TypeScript, ESLint, 61 | Prettier, Husky, Lint-Staged, Jest (replaced by Vitest), Testing 62 | Library, Commitlint, VSCode, PostCSS, Tailwind CSS, Authentication with{' '} 63 | 68 | Clerk 69 | 70 | , Database with DrizzleORM (SQLite, PostgreSQL, and MySQL) and{' '} 71 | 76 | Turso 77 | 78 | , Error Monitoring with{' '} 79 | 84 | Sentry 85 | 86 | , Logging with Pino.js and Log Management with{' '} 87 | 92 | Better Stack 93 | 94 | , Monitoring as Code with Checkly, Storybook, Multi-language (i18n), and 95 | more. 96 |

    97 | 98 | ); 99 | } 100 | -------------------------------------------------------------------------------- /src/app/[locale]/(unauth)/portfolio/[slug]/page.tsx: -------------------------------------------------------------------------------- 1 | import Image from 'next/image'; 2 | import { useTranslations } from 'next-intl'; 3 | import { getTranslations, unstable_setRequestLocale } from 'next-intl/server'; 4 | 5 | import { AppConfig } from '@/utils/AppConfig'; 6 | 7 | type IPortfolioDetailProps = { 8 | params: { slug: string; locale: string }; 9 | }; 10 | 11 | export function generateStaticParams() { 12 | return AppConfig.locales 13 | .map((locale) => 14 | Array.from(Array(6).keys()).map((elt) => ({ 15 | slug: `${elt}`, 16 | locale, 17 | })), 18 | ) 19 | .flat(1); 20 | } 21 | 22 | export async function generateMetadata(props: IPortfolioDetailProps) { 23 | const t = await getTranslations({ 24 | locale: props.params.locale, 25 | namespace: 'PortfolioSlug', 26 | }); 27 | 28 | return { 29 | title: t('meta_title', { slug: props.params.slug }), 30 | description: t('meta_description', { slug: props.params.slug }), 31 | }; 32 | } 33 | 34 | const PortfolioDetail = (props: IPortfolioDetailProps) => { 35 | unstable_setRequestLocale(props.params.locale); 36 | const t = useTranslations('PortfolioSlug'); 37 | 38 | return ( 39 | <> 40 |

    {t('header', { slug: props.params.slug })}

    41 |

    {t('content')}

    42 | 43 |
    44 | {`${t('log_management_powered_by')} `} 45 | 50 | Better Stack 51 | 52 |
    53 | 54 | 58 | Better Stack 65 | 66 | 67 | ); 68 | }; 69 | 70 | export const dynamicParams = false; 71 | 72 | export default PortfolioDetail; 73 | -------------------------------------------------------------------------------- /src/app/[locale]/(unauth)/portfolio/page.tsx: -------------------------------------------------------------------------------- 1 | import Image from 'next/image'; 2 | import Link from 'next/link'; 3 | import { useTranslations } from 'next-intl'; 4 | import { getTranslations } from 'next-intl/server'; 5 | 6 | export async function generateMetadata(props: { params: { locale: string } }) { 7 | const t = await getTranslations({ 8 | locale: props.params.locale, 9 | namespace: 'Portfolio', 10 | }); 11 | 12 | return { 13 | title: t('meta_title'), 14 | description: t('meta_description'), 15 | }; 16 | } 17 | 18 | const Portfolio = () => { 19 | const t = useTranslations('Portfolio'); 20 | 21 | return ( 22 | <> 23 |

    {t('presentation')}

    24 | 25 |
    26 | {Array.from(Array(6).keys()).map((elt) => ( 27 | 32 | {t('portfolio_name', { name: elt })} 33 | 34 | ))} 35 |
    36 | 37 |
    38 | {`${t('error_reporting_powered_by')} `} 39 | 44 | Sentry 45 | {' '} 46 | - {`${t('coverage_powered_by')} `} 47 | 52 | Codecov 53 | 54 |
    55 | 56 | 60 | Sentry 67 | 68 | 69 | ); 70 | }; 71 | 72 | export default Portfolio; 73 | -------------------------------------------------------------------------------- /src/app/[locale]/layout.tsx: -------------------------------------------------------------------------------- 1 | import '@/styles/global.css'; 2 | 3 | import type { Metadata } from 'next'; 4 | import { notFound } from 'next/navigation'; 5 | import { NextIntlClientProvider, useMessages } from 'next-intl'; 6 | 7 | import { DemoBadge } from '@/components/DemoBadge'; 8 | import { AppConfig } from '@/utils/AppConfig'; 9 | 10 | export const metadata: Metadata = { 11 | icons: [ 12 | { 13 | rel: 'apple-touch-icon', 14 | url: '/apple-touch-icon.png', 15 | }, 16 | { 17 | rel: 'icon', 18 | type: 'image/png', 19 | sizes: '32x32', 20 | url: '/favicon-32x32.png', 21 | }, 22 | { 23 | rel: 'icon', 24 | type: 'image/png', 25 | sizes: '16x16', 26 | url: '/favicon-16x16.png', 27 | }, 28 | { 29 | rel: 'icon', 30 | url: '/favicon.ico', 31 | }, 32 | ], 33 | }; 34 | 35 | export default function RootLayout(props: { 36 | children: React.ReactNode; 37 | params: { locale: string }; 38 | }) { 39 | // Validate that the incoming `locale` parameter is valid 40 | if (!AppConfig.locales.includes(props.params.locale)) notFound(); 41 | 42 | // Using internationalization in Client Components 43 | const messages = useMessages(); 44 | 45 | return ( 46 | 47 | 48 | 52 | {props.children} 53 | 54 | 55 | 56 | 57 | 58 | ); 59 | } 60 | 61 | // Enable edge runtime but you are required to disable the `migrate` function in `src/libs/DB.ts` 62 | // Unfortunately, this also means it will also disable the automatic migration of the database 63 | // And, you will have to manually migrate it with `drizzle-kit push` 64 | // export const runtime = 'edge'; 65 | -------------------------------------------------------------------------------- /src/app/global-error.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import * as Sentry from '@sentry/nextjs'; 4 | import Error from 'next/error'; 5 | import { useEffect } from 'react'; 6 | 7 | export default function GlobalError(props: { 8 | error: Error & { digest?: string }; 9 | params: { locale: string }; 10 | }) { 11 | useEffect(() => { 12 | Sentry.captureException(props.error); 13 | }, [props.error]); 14 | 15 | return ( 16 | 17 | 18 | {/* This is the default Next.js error component but it doesn't allow omitting the statusCode property yet. */} 19 | 20 | 21 | 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /src/app/robots.ts: -------------------------------------------------------------------------------- 1 | import type { MetadataRoute } from 'next'; 2 | 3 | import { getBaseUrl } from '@/utils/Helpers'; 4 | 5 | export default function robots(): MetadataRoute.Robots { 6 | return { 7 | rules: { 8 | userAgent: '*', 9 | allow: '/', 10 | }, 11 | sitemap: `${getBaseUrl()}/sitemap.xml`, 12 | }; 13 | } 14 | -------------------------------------------------------------------------------- /src/app/sitemap.ts: -------------------------------------------------------------------------------- 1 | import type { MetadataRoute } from 'next'; 2 | 3 | import { getBaseUrl } from '@/utils/Helpers'; 4 | 5 | export default function sitemap(): MetadataRoute.Sitemap { 6 | return [ 7 | { 8 | url: `${getBaseUrl()}/`, 9 | lastModified: new Date(), 10 | changeFrequency: 'daily', 11 | priority: 0.7, 12 | }, 13 | // Add more URLs here 14 | ]; 15 | } 16 | -------------------------------------------------------------------------------- /src/components/AddGuestbookForm.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { GuestbookForm } from './GuestbookForm'; 4 | 5 | const AddGuestbookForm = () => ( 6 | { 8 | await fetch(`/api/guestbook`, { 9 | method: 'POST', 10 | headers: { 11 | 'Content-Type': 'application/json', 12 | }, 13 | body: JSON.stringify(data), 14 | }); 15 | }} 16 | /> 17 | ); 18 | 19 | export { AddGuestbookForm }; 20 | -------------------------------------------------------------------------------- /src/components/DeleteGuestbookEntry.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useRouter } from 'next/navigation'; 4 | 5 | const DeleteGuestbookEntry = (props: { id: number }) => { 6 | const router = useRouter(); 7 | 8 | const handleDelete = async () => { 9 | await fetch(`/api/guestbook`, { 10 | method: 'DELETE', 11 | headers: { 12 | 'Content-Type': 'application/json', 13 | }, 14 | body: JSON.stringify({ 15 | id: props.id, 16 | }), 17 | }); 18 | 19 | router.refresh(); 20 | }; 21 | 22 | return ( 23 | 41 | ); 42 | }; 43 | 44 | export { DeleteGuestbookEntry }; 45 | -------------------------------------------------------------------------------- /src/components/DemoBadge.tsx: -------------------------------------------------------------------------------- 1 | const DemoBadge = () => ( 2 | 13 | ); 14 | 15 | export { DemoBadge }; 16 | -------------------------------------------------------------------------------- /src/components/EditableGuestbookEntry.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useState } from 'react'; 4 | 5 | import { GuestbookForm } from './GuestbookForm'; 6 | 7 | const EditableGuestbookEntry = (props: { 8 | id: number; 9 | username: string; 10 | body: string; 11 | }) => { 12 | const [isEditing, setIsEditing] = useState(false); 13 | 14 | const handleEdit = () => { 15 | setIsEditing((value) => !value); 16 | }; 17 | 18 | return ( 19 | <> 20 | 38 | 39 |
    40 | {isEditing ? ( 41 | { 49 | await fetch(`/api/guestbook`, { 50 | method: 'PUT', 51 | headers: { 52 | 'Content-Type': 'application/json', 53 | }, 54 | body: JSON.stringify({ 55 | id: props.id, 56 | ...data, 57 | }), 58 | }); 59 | 60 | setIsEditing(false); 61 | }} 62 | /> 63 | ) : ( 64 | <> 65 | {props.username}:{' '} 66 | {props.body} 67 | 68 | )} 69 |
    70 | 71 | ); 72 | }; 73 | 74 | export { EditableGuestbookEntry }; 75 | -------------------------------------------------------------------------------- /src/components/GuestbookForm.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { zodResolver } from '@hookform/resolvers/zod'; 4 | import { useRouter } from 'next/navigation'; 5 | import { useTranslations } from 'next-intl'; 6 | import { type SubmitHandler, useForm } from 'react-hook-form'; 7 | import type { z } from 'zod'; 8 | 9 | import { GuestbookValidation } from '@/validations/GuestbookValidation'; 10 | 11 | type IGuestbookFormProps = 12 | | { 13 | edit: true; 14 | id: number; 15 | defaultValues: z.infer; 16 | onValid: SubmitHandler>; 17 | } 18 | | { 19 | edit?: false; 20 | onValid: SubmitHandler>; 21 | }; 22 | 23 | const GuestbookForm = (props: IGuestbookFormProps) => { 24 | const { 25 | handleSubmit, 26 | register, 27 | reset, 28 | formState: { errors }, 29 | } = useForm>({ 30 | resolver: zodResolver(GuestbookValidation), 31 | defaultValues: props.edit ? props.defaultValues : undefined, 32 | }); 33 | const router = useRouter(); 34 | const t = useTranslations('GuestbookForm'); 35 | 36 | const handleCreate = handleSubmit(async (data) => { 37 | await props.onValid(data); 38 | 39 | reset(); 40 | router.refresh(); 41 | }); 42 | 43 | return ( 44 |
    45 |
    46 | 57 | {errors.username?.message && ( 58 |
    59 | {errors.username?.message} 60 |
    61 | )} 62 |
    63 | 64 |
    65 | 76 | {errors.body?.message && ( 77 |
    78 | {errors.body?.message} 79 |
    80 | )} 81 |
    82 | 83 |
    84 | 90 |
    91 |
    92 | ); 93 | }; 94 | 95 | export { GuestbookForm }; 96 | -------------------------------------------------------------------------------- /src/components/GuestbookList.tsx: -------------------------------------------------------------------------------- 1 | import { db } from '@/libs/DB'; 2 | import { logger } from '@/libs/Logger'; 3 | import { guestbookSchema } from '@/models/Schema'; 4 | 5 | import { DeleteGuestbookEntry } from './DeleteGuestbookEntry'; 6 | import { EditableGuestbookEntry } from './EditableGuestbookEntry'; 7 | 8 | const GuestbookList = async () => { 9 | const guestbook = await db.select().from(guestbookSchema).all(); 10 | 11 | logger.info('Get all guestbook entries'); 12 | 13 | return ( 14 |
    15 | {guestbook.map((elt) => ( 16 |
    17 | 18 | 19 | 24 |
    25 | ))} 26 |
    27 | ); 28 | }; 29 | 30 | export { GuestbookList }; 31 | -------------------------------------------------------------------------------- /src/components/Hello.tsx: -------------------------------------------------------------------------------- 1 | import { currentUser } from '@clerk/nextjs/server'; 2 | import { getTranslations } from 'next-intl/server'; 3 | 4 | const Hello = async () => { 5 | const t = await getTranslations('Dashboard'); 6 | const user = await currentUser(); 7 | 8 | return ( 9 | <> 10 |

    11 | 👋{' '} 12 | {t('hello_message', { email: user?.emailAddresses[0]?.emailAddress })} 13 |

    14 |

    15 | Looking for a SaaS Boilerplate?{' '} 16 | 20 | Next.js Boilerplate SaaS 21 | {' '} 22 | can help you build one. 23 |

    24 | 25 | ); 26 | }; 27 | 28 | export { Hello }; 29 | -------------------------------------------------------------------------------- /src/components/LocaleSwitcher.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useLocale } from 'next-intl'; 4 | import type { ChangeEventHandler } from 'react'; 5 | 6 | import { usePathname, useRouter } from '@/libs/i18nNavigation'; 7 | import { AppConfig } from '@/utils/AppConfig'; 8 | 9 | export default function LocaleSwitcher() { 10 | const router = useRouter(); 11 | const pathname = usePathname(); 12 | const locale = useLocale(); 13 | 14 | const handleChange: ChangeEventHandler = (event) => { 15 | router.push(pathname, { locale: event.target.value }); 16 | router.refresh(); 17 | }; 18 | 19 | return ( 20 | 31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /src/components/LogOutButton.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useClerk } from '@clerk/nextjs'; 4 | import { useRouter } from 'next/navigation'; 5 | import { useTranslations } from 'next-intl'; 6 | 7 | const LogOutButton = () => { 8 | const router = useRouter(); 9 | const { signOut } = useClerk(); 10 | const t = useTranslations('DashboardLayout'); 11 | 12 | return ( 13 | 20 | ); 21 | }; 22 | 23 | export { LogOutButton }; 24 | -------------------------------------------------------------------------------- /src/components/Sponsors.tsx: -------------------------------------------------------------------------------- 1 | import Image from 'next/image'; 2 | 3 | const Sponsors = () => ( 4 | 5 | 6 | 7 | 20 | 33 | 43 | 44 | 45 | 58 | 71 | 84 | 85 | 86 | 99 | 109 | 110 | 111 |
    8 | 12 | Clerk – Authentication & User Management for Next.js 18 | 19 | 21 | 25 | SQLite Developer Experience 31 | 32 | 34 | 35 | Crowdin 41 | 42 |
    46 | 50 | Sentry 56 | 57 | 59 | 63 | PostHog 69 | 70 | 72 | 76 | Better Stack 82 | 83 |
    87 | 91 | Checkly 97 | 98 | 100 | 101 | React SaaS Boilerplate Next.js 107 | 108 |
    112 | ); 113 | 114 | export { Sponsors }; 115 | -------------------------------------------------------------------------------- /src/libs/DB.ts: -------------------------------------------------------------------------------- 1 | import { createClient } from '@libsql/client'; 2 | import { drizzle } from 'drizzle-orm/libsql'; 3 | import { migrate } from 'drizzle-orm/libsql/migrator'; 4 | 5 | import { Env } from './Env'; 6 | 7 | const client = createClient({ 8 | url: Env.DATABASE_URL, 9 | authToken: Env.DATABASE_AUTH_TOKEN, 10 | }); 11 | 12 | export const db = drizzle(client); 13 | 14 | // Disable migrate function if using Edge runtime and use `npm run db:migrate` instead. 15 | // Only run migrate in development. Otherwise, migrate will also be run during the build which can cause errors. 16 | // Migrate during the build can cause errors due to the locked database when multiple migrations are running at the same time. 17 | if (process.env.NODE_ENV === 'development') { 18 | await migrate(db, { migrationsFolder: './migrations' }); 19 | } 20 | -------------------------------------------------------------------------------- /src/libs/Env.ts: -------------------------------------------------------------------------------- 1 | import { createEnv } from '@t3-oss/env-nextjs'; 2 | import { z } from 'zod'; 3 | 4 | // Don't add NODE_ENV into T3 Env, it changes the tree-shaking behavior 5 | export const Env = createEnv({ 6 | server: { 7 | CLERK_SECRET_KEY: z.string().min(1), 8 | DATABASE_URL: z.string().min(1), 9 | DATABASE_AUTH_TOKEN: z.string().optional(), 10 | LOGTAIL_SOURCE_TOKEN: z.string().optional(), 11 | }, 12 | client: { 13 | NEXT_PUBLIC_APP_URL: z.string().optional(), 14 | NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: z.string().min(1), 15 | NEXT_PUBLIC_CLERK_SIGN_IN_URL: z.string().min(1), 16 | }, 17 | // You need to destructure all the keys manually 18 | runtimeEnv: { 19 | CLERK_SECRET_KEY: process.env.CLERK_SECRET_KEY, 20 | DATABASE_URL: process.env.DATABASE_URL, 21 | DATABASE_AUTH_TOKEN: process.env.DATABASE_AUTH_TOKEN, 22 | LOGTAIL_SOURCE_TOKEN: process.env.LOGTAIL_SOURCE_TOKEN, 23 | NEXT_PUBLIC_APP_URL: process.env.NEXT_PUBLIC_APP_URL, 24 | NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: 25 | process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY, 26 | NEXT_PUBLIC_CLERK_SIGN_IN_URL: process.env.NEXT_PUBLIC_CLERK_SIGN_IN_URL, 27 | }, 28 | }); 29 | -------------------------------------------------------------------------------- /src/libs/Logger.ts: -------------------------------------------------------------------------------- 1 | import logtail from '@logtail/pino'; 2 | import pino, { type DestinationStream } from 'pino'; 3 | import pretty from 'pino-pretty'; 4 | 5 | import { Env } from './Env'; 6 | 7 | let stream: DestinationStream; 8 | 9 | if (Env.LOGTAIL_SOURCE_TOKEN) { 10 | stream = pino.multistream([ 11 | await logtail({ 12 | sourceToken: Env.LOGTAIL_SOURCE_TOKEN, 13 | options: { 14 | sendLogsToBetterStack: true, 15 | }, 16 | }), 17 | { 18 | stream: pretty(), // Prints logs to the console 19 | }, 20 | ]); 21 | } else { 22 | stream = pretty({ 23 | colorize: true, 24 | }); 25 | } 26 | 27 | export const logger = pino({ base: undefined }, stream); 28 | -------------------------------------------------------------------------------- /src/libs/i18n.ts: -------------------------------------------------------------------------------- 1 | import { getRequestConfig } from 'next-intl/server'; 2 | 3 | // NextJS Boilerplate uses Crowdin as the localization software. 4 | // As a developer, you only need to take care of the English (or another default language) version. 5 | // Other languages are automatically generated and handled by Crowdin. 6 | 7 | // The localisation files are synced with Crowdin using GitHub Actions. 8 | // By default, there are 3 ways to sync the message files: 9 | // 1. Automatically sync on push to the `main` branch 10 | // 2. Run manually the workflow on GitHub Actions 11 | // 3. Every 24 hours at 5am, the workflow will run automatically 12 | 13 | // Using internationalization in Server Components 14 | export default getRequestConfig(async ({ locale }) => ({ 15 | messages: (await import(`../locales/${locale}.json`)).default, 16 | })); 17 | -------------------------------------------------------------------------------- /src/libs/i18nNavigation.ts: -------------------------------------------------------------------------------- 1 | import { createSharedPathnamesNavigation } from 'next-intl/navigation'; 2 | 3 | import { AppConfig } from '@/utils/AppConfig'; 4 | 5 | export const { usePathname, useRouter } = createSharedPathnamesNavigation({ 6 | locales: AppConfig.locales, 7 | localePrefix: AppConfig.localePrefix, 8 | }); 9 | -------------------------------------------------------------------------------- /src/locales/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "RootLayout": { 3 | "home_link": "Home", 4 | "about_link": "About", 5 | "guestbook_link": "Guestbook", 6 | "portfolio_link": "Portfolio", 7 | "sign_in_link": "Sign in", 8 | "sign_up_link": "Sign up" 9 | }, 10 | "BaseTemplate": { 11 | "description": "Starter code for your Nextjs Boilerplate with Tailwind CSS", 12 | "made_with": "Made with" 13 | }, 14 | "Index": { 15 | "meta_title": "Next.js Boilerplate Presentation", 16 | "meta_description": "Next js Boilerplate is the perfect starter code for your project. Build your React application with the Next.js framework." 17 | }, 18 | "Guestbook": { 19 | "meta_title": "Guestbook", 20 | "meta_description": "An example of CRUD operation", 21 | "database_powered_by": "Database powered by", 22 | "loading_guestbook": "Loading guestbook...", 23 | "error_reporting_powered_by": "Error reporting powered by" 24 | }, 25 | "GuestbookForm": { 26 | "username": "Username", 27 | "body": "Body", 28 | "save": "Save" 29 | }, 30 | "About": { 31 | "meta_title": "About", 32 | "meta_description": "About page description", 33 | "about_paragraph": "Welcome to our About page! We are a team of passionate individuals dedicated to creating amazing software.", 34 | "translation_powered_by": "Translation powered by" 35 | }, 36 | "Portfolio": { 37 | "meta_title": "Portfolio", 38 | "meta_description": "Welcome to my portfolio page!", 39 | "presentation": "Welcome to my portfolio page! Here you will find a carefully curated collection of my work and accomplishments. Through this portfolio, I'm to showcase my expertise, creativity, and the value I can bring to your projects.", 40 | "portfolio_name": "Portfolio {name}", 41 | "error_reporting_powered_by": "Error reporting powered by", 42 | "coverage_powered_by": "Code coverage powered by" 43 | }, 44 | "PortfolioSlug": { 45 | "meta_title": "Portfolio {slug}", 46 | "meta_description": "Portfolio {slug} description", 47 | "header": "Portfolio {slug}", 48 | "content": "Created a set of promotional materials and branding elements for a corporate event. Crafted a visually unified theme, encompassing a logo, posters, banners, and digital assets. Integrated the client's brand identity while infusing it with a contemporary and innovative approach. Garnered favorable responses from event attendees, resulting in a successful event with heightened participant engagement and increased brand visibility.", 49 | "log_management_powered_by": "Log management powered by" 50 | }, 51 | "SignIn": { 52 | "meta_title": "Sign in", 53 | "meta_description": "Seamlessly sign in to your account with our user-friendly login process." 54 | }, 55 | "SignUp": { 56 | "meta_title": "Sign up", 57 | "meta_description": "Effortlessly create an account through our intuitive sign-up process." 58 | }, 59 | "Dashboard": { 60 | "meta_title": "Dashboard", 61 | "hello_message": "Hello {email}!" 62 | }, 63 | "UserProfile": { 64 | "meta_title": "User Profile" 65 | }, 66 | "DashboardLayout": { 67 | "dashboard_link": "Dashboard", 68 | "user_profile_link": "Manage your account", 69 | "sign_out": "Sign out" 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/locales/fr.json: -------------------------------------------------------------------------------- 1 | { 2 | "RootLayout": { 3 | "home_link": "Accueil", 4 | "about_link": "A propos", 5 | "guestbook_link": "Livre d'or", 6 | "portfolio_link": "Portfolio", 7 | "sign_in_link": "Se connecter", 8 | "sign_up_link": "S'inscrire" 9 | }, 10 | "BaseTemplate": { 11 | "description": "Code de démarrage pour Next.js avec Tailwind CSS", 12 | "made_with": "Fait avec" 13 | }, 14 | "Index": { 15 | "meta_title": "Présentation de Next.js Boilerplate", 16 | "meta_description": "Next js Boilerplate est le code de démarrage parfait pour votre projet. Construisez votre application React avec le framework Next.js." 17 | }, 18 | "Guestbook": { 19 | "meta_title": "Livre d'or", 20 | "meta_description": "Un exemple de fonctionnement CRUD", 21 | "database_powered_by": "Base de données propulsée par", 22 | "loading_guestbook": "Chargement du livre d'or...", 23 | "error_reporting_powered_by": "Rapport d'erreur propulsé par" 24 | }, 25 | "GuestbookForm": { 26 | "username": "Nom d'utilisateur", 27 | "body": "Message", 28 | "save": "Sauvegarder" 29 | }, 30 | "About": { 31 | "meta_title": "A propos", 32 | "meta_description": "A propos description", 33 | "about_paragraph": "Bienvenue sur notre page À propos ! Nous sommes une équipe de passionnés et dévoués à la création de logiciels.", 34 | "translation_powered_by": "Traduction propulsée par" 35 | }, 36 | "Portfolio": { 37 | "meta_title": "Portfolio", 38 | "meta_description": "Bienvenue sur la page de mon portfolio !", 39 | "presentation": "Bienvenue sur ma page portfolio ! Vous trouverez ici une collection soigneusement organisée de mon travail et de mes réalisations. À travers ce portfolio, je mets en valeur mon expertise, ma créativité et la valeur que je peux apporter à vos projets.", 40 | "portfolio_name": "Portfolio {name}", 41 | "error_reporting_powered_by": "Rapport d'erreur propulsé par", 42 | "coverage_powered_by": "Couverture de code propulsée par" 43 | }, 44 | "PortfolioSlug": { 45 | "meta_title": "Portfolio {slug}", 46 | "meta_description": "Description du Portfolio {slug}", 47 | "header": "Portfolio {slug}", 48 | "content": "Créé un ensemble de matériel promotionnel et d'éléments de marquage pour un événement d'entreprise. Conçu un thème visuellement unifié, englobant un logo, des affiches, des bannières et des actifs numériques. Intégrer l'identité de marque du client tout en l'insufflant à une approche contemporaine et innovante. Des réponses favorables de la part des participants ont été obtenues, ce qui a donné lieu à un événement réussi avec un engagement accru des participants et une meilleure visibilité de la marque.", 49 | "log_management_powered_by": "Gestion des logs propulsée par" 50 | }, 51 | "SignIn": { 52 | "meta_title": "Se connecter", 53 | "meta_description": "Connectez-vous à votre compte avec facilité." 54 | }, 55 | "SignUp": { 56 | "meta_title": "S'inscrire", 57 | "meta_description": "Créez un compte sans effort grâce à notre processus d'inscription intuitif." 58 | }, 59 | "Dashboard": { 60 | "meta_title": "Tableau de bord", 61 | "hello_message": "Bonjour {email}!" 62 | }, 63 | "UserProfile": { 64 | "meta_title": "Profil de l'utilisateur" 65 | }, 66 | "DashboardLayout": { 67 | "dashboard_link": "Tableau de bord", 68 | "user_profile_link": "Gérer votre compte", 69 | "sign_out": "Se déconnecter" 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/middleware.ts: -------------------------------------------------------------------------------- 1 | import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server'; 2 | import type { NextFetchEvent, NextRequest } from 'next/server'; 3 | import createMiddleware from 'next-intl/middleware'; 4 | 5 | import { AppConfig } from './utils/AppConfig'; 6 | 7 | const intlMiddleware = createMiddleware({ 8 | locales: AppConfig.locales, 9 | localePrefix: AppConfig.localePrefix, 10 | defaultLocale: AppConfig.defaultLocale, 11 | }); 12 | 13 | const isProtectedRoute = createRouteMatcher([ 14 | '/dashboard(.*)', 15 | '/:locale/dashboard(.*)', 16 | ]); 17 | 18 | export default function middleware( 19 | request: NextRequest, 20 | event: NextFetchEvent, 21 | ) { 22 | // Run Clerk middleware only when it's necessary 23 | if ( 24 | request.nextUrl.pathname.includes('/sign-in') || 25 | request.nextUrl.pathname.includes('/sign-up') || 26 | isProtectedRoute(request) 27 | ) { 28 | return clerkMiddleware((auth, req) => { 29 | if (isProtectedRoute(req)) { 30 | const locale = 31 | req.nextUrl.pathname.match(/(\/.*)\/dashboard/)?.at(1) ?? ''; 32 | 33 | const signInUrl = new URL(`${locale}/sign-in`, req.url); 34 | 35 | auth().protect({ 36 | // `unauthenticatedUrl` is needed to avoid error: "Unable to find `next-intl` locale because the middleware didn't run on this request" 37 | unauthenticatedUrl: signInUrl.toString(), 38 | }); 39 | } 40 | 41 | return intlMiddleware(req); 42 | })(request, event); 43 | } 44 | 45 | return intlMiddleware(request); 46 | } 47 | 48 | export const config = { 49 | matcher: ['/((?!.+\\.[\\w]+$|_next).*)', '/', '/(api|trpc)(.*)'], 50 | }; 51 | -------------------------------------------------------------------------------- /src/models/Schema.ts: -------------------------------------------------------------------------------- 1 | import { sql } from 'drizzle-orm'; 2 | import { integer, sqliteTable, text } from 'drizzle-orm/sqlite-core'; 3 | 4 | export const guestbookSchema = sqliteTable('guestbook', { 5 | id: integer('id').primaryKey(), 6 | username: text('username').notNull(), 7 | body: text('body').notNull(), 8 | createdAt: integer('created_at', { mode: 'timestamp' }).default( 9 | sql`(strftime('%s', 'now'))`, 10 | ), 11 | updatedAt: integer('updated_at', { mode: 'timestamp' }).default( 12 | sql`(strftime('%s', 'now'))`, 13 | ), 14 | }); 15 | -------------------------------------------------------------------------------- /src/styles/global.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /src/templates/BaseTemplate.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | import { userEvent, within } from '@storybook/test'; 3 | import { NextIntlClientProvider } from 'next-intl'; 4 | 5 | import messages from '@/locales/en.json'; 6 | 7 | import { BaseTemplate } from './BaseTemplate'; 8 | 9 | // More on how to set up stories at: https://storybook.js.org/docs/7.0/react/writing-stories/introduction 10 | const meta = { 11 | title: 'Example/BaseTemplate', 12 | component: BaseTemplate, 13 | parameters: { 14 | // More on how to position stories at: https://storybook.js.org/docs/7.0/react/configure/story-layout 15 | layout: 'fullscreen', 16 | }, 17 | tags: ['autodocs'], 18 | decorators: [ 19 | (Story) => ( 20 | 21 | 22 | 23 | ), 24 | ], 25 | } satisfies Meta; 26 | 27 | export default meta; 28 | type Story = StoryObj; 29 | 30 | export const BaseWithReactComponent = { 31 | args: { 32 | children:
    Children node
    , 33 | leftNav: ( 34 | <> 35 |
  • Link 1
  • 36 |
  • Link 2
  • 37 | 38 | ), 39 | }, 40 | } satisfies Story; 41 | 42 | export const BaseWithString = { 43 | args: { 44 | children: 'String', 45 | leftNav: ( 46 | <> 47 |
  • Link 1
  • 48 |
  • Link 2
  • 49 | 50 | ), 51 | }, 52 | } satisfies Story; 53 | 54 | // More on interaction testing: https://storybook.js.org/docs/7.0/react/writing-tests/interaction-testing 55 | export const BaseWithHomeLink: Story = { 56 | args: { 57 | children:
    Children node
    , 58 | leftNav: ( 59 | <> 60 |
  • Link 1
  • 61 |
  • Link 2
  • 62 | 63 | ), 64 | }, 65 | play: async ({ canvasElement }) => { 66 | const canvas = within(canvasElement); 67 | const link = canvas.getByText('Link 1'); 68 | 69 | await userEvent.click(link); 70 | }, 71 | } satisfies Story; 72 | -------------------------------------------------------------------------------- /src/templates/BaseTemplate.test.tsx: -------------------------------------------------------------------------------- 1 | import { render, screen, within } from '@testing-library/react'; 2 | import { NextIntlClientProvider } from 'next-intl'; 3 | 4 | import messages from '@/locales/en.json'; 5 | 6 | import { BaseTemplate } from './BaseTemplate'; 7 | 8 | describe('Base template', () => { 9 | describe('Render method', () => { 10 | it('should have 3 menu items', () => { 11 | render( 12 | 13 | 16 |
  • link 1
  • 17 |
  • link 2
  • 18 |
  • link 3
  • 19 | 20 | } 21 | > 22 | {null} 23 |
    24 |
    , 25 | ); 26 | 27 | const menuItemList = screen.getAllByRole('listitem'); 28 | 29 | expect(menuItemList).toHaveLength(3); 30 | }); 31 | 32 | it('should have a link to support creativedesignsguru.com', () => { 33 | render( 34 | 35 | 1}>{null} 36 | , 37 | ); 38 | 39 | const copyrightSection = screen.getByText(/© Copyright/); 40 | const copyrightLink = within(copyrightSection).getByRole('link'); 41 | 42 | /* 43 | * PLEASE READ THIS SECTION 44 | * We'll really appreciate if you could have a link to our website 45 | * The link doesn't need to appear on every pages, one link on one page is enough. 46 | * Thank you for your support it'll mean a lot for us. 47 | */ 48 | expect(copyrightLink).toHaveAttribute( 49 | 'href', 50 | 'https://creativedesignsguru.com', 51 | ); 52 | }); 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /src/templates/BaseTemplate.tsx: -------------------------------------------------------------------------------- 1 | import { useTranslations } from 'next-intl'; 2 | 3 | import { AppConfig } from '@/utils/AppConfig'; 4 | 5 | const BaseTemplate = (props: { 6 | leftNav: React.ReactNode; 7 | rightNav?: React.ReactNode; 8 | children: React.ReactNode; 9 | }) => { 10 | const t = useTranslations('BaseTemplate'); 11 | 12 | return ( 13 |
    14 |
    15 |
    16 |
    17 |

    18 | {AppConfig.name} 19 |

    20 |

    {t('description')}

    21 |
    22 | 23 |
    24 | 29 | 30 | 35 |
    36 |
    37 | 38 |
    {props.children}
    39 | 40 | 57 |
    58 |
    59 | ); 60 | }; 61 | 62 | export { BaseTemplate }; 63 | -------------------------------------------------------------------------------- /src/types/global.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/consistent-type-imports */ 2 | // Use type safe message keys with `next-intl` 3 | type Messages = typeof import('../locales/en.json'); 4 | declare interface IntlMessages extends Messages {} 5 | -------------------------------------------------------------------------------- /src/utils/AppConfig.ts: -------------------------------------------------------------------------------- 1 | import type { LocalePrefix } from 'node_modules/next-intl/dist/types/src/shared/types'; 2 | 3 | const localePrefix: LocalePrefix = 'as-needed'; 4 | 5 | // FIXME: Update this configuration file based on your project information 6 | export const AppConfig = { 7 | name: 'Nextjs Starter', 8 | locales: ['zh', 'en'], 9 | defaultLocale: 'zh', 10 | localePrefix, 11 | }; 12 | -------------------------------------------------------------------------------- /src/utils/Helpers.test.ts: -------------------------------------------------------------------------------- 1 | import { AppConfig } from './AppConfig'; 2 | import { getI18nPath } from './Helpers'; 3 | 4 | describe('Helpers', () => { 5 | describe('getI18nPath function', () => { 6 | it('should not change the path for default language', () => { 7 | const url = '/random-url'; 8 | const locale = AppConfig.defaultLocale; 9 | 10 | expect(getI18nPath(url, locale)).toBe(url); 11 | }); 12 | 13 | it('should prepend the locale to the path for non-default language', () => { 14 | const url = '/random-url'; 15 | const locale = 'fr'; 16 | 17 | expect(getI18nPath(url, locale)).toMatch(/^\/fr/); 18 | }); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /src/utils/Helpers.ts: -------------------------------------------------------------------------------- 1 | import { AppConfig } from './AppConfig'; 2 | 3 | export const getBaseUrl = () => { 4 | if (process.env.NEXT_PUBLIC_APP_URL) { 5 | return process.env.NEXT_PUBLIC_APP_URL; 6 | } 7 | 8 | if (process.env.VERCEL_URL) { 9 | return `https://${process.env.VERCEL_URL}`; 10 | } 11 | 12 | return 'http://localhost:3000'; 13 | }; 14 | 15 | export const getI18nPath = (url: string, locale: string) => { 16 | if (locale === AppConfig.defaultLocale) { 17 | return url; 18 | } 19 | 20 | return `/${locale}${url}`; 21 | }; 22 | -------------------------------------------------------------------------------- /src/utils/dist/AppConfig.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | exports.__esModule = true; 3 | exports.AppConfig = void 0; 4 | var localePrefix = 'as-needed'; 5 | // FIXME: Update this configuration file based on your project information 6 | exports.AppConfig = { 7 | name: 'Nextjs Starter', 8 | locales: ['zh', 'en'], 9 | defaultLocale: 'zh', 10 | localePrefix: localePrefix 11 | }; 12 | -------------------------------------------------------------------------------- /src/validations/GuestbookValidation.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | 3 | export const GuestbookValidation = z.object({ 4 | username: z.string().min(1), 5 | body: z.string().min(1), 6 | }); 7 | 8 | export const EditGuestbookValidation = z.object({ 9 | id: z.coerce.number(), 10 | username: z.string().min(1), 11 | body: z.string().min(1), 12 | }); 13 | 14 | export const DeleteGuestbookValidation = z.object({ 15 | id: z.coerce.number(), 16 | }); 17 | -------------------------------------------------------------------------------- /tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from 'tailwindcss'; 2 | 3 | export default { 4 | content: ['./src/**/*.{js,ts,jsx,tsx}'], 5 | theme: { 6 | extend: {}, 7 | }, 8 | plugins: [], 9 | } satisfies Config; 10 | -------------------------------------------------------------------------------- /tests/e2e/Guestbook.spec.ts: -------------------------------------------------------------------------------- 1 | import { faker } from '@faker-js/faker'; 2 | import { expect, test } from '@playwright/test'; 3 | 4 | test.describe('Guestbook', () => { 5 | test.describe('CRUD operation', () => { 6 | test('should browse to guestbook, crate a new entry, read, update and remove the newly created', async ({ 7 | page, 8 | }) => { 9 | await page.goto('/guestbook'); 10 | await expect(page.getByText('Username')).toBeVisible(); 11 | 12 | const username = faker.internet.userName(); 13 | const body = faker.lorem.words(); 14 | 15 | // Create 16 | await page.getByLabel('Username').fill(username); 17 | await page.getByLabel('Body').fill(body); 18 | await page.getByRole('button', { name: 'Save' }).click(); 19 | 20 | const guestbookList = page.getByTestId('guestbook-list'); 21 | 22 | // Read 23 | await expect(guestbookList.getByText(username)).toBeVisible(); 24 | 25 | const updatedUsername = `${username} updated`; 26 | 27 | // Update 28 | await guestbookList.locator('button[aria-label=edit]').last().click(); 29 | await guestbookList.getByText('Username').fill(updatedUsername); 30 | await guestbookList.getByRole('button', { name: 'Save' }).click(); 31 | 32 | // Verify after update 33 | await expect(guestbookList.getByText(updatedUsername)).toBeVisible(); 34 | 35 | // Delete 36 | await guestbookList.locator('button[aria-label=delete]').last().click(); 37 | await expect(guestbookList.getByText(updatedUsername)).toBeHidden(); 38 | }); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /tests/e2e/Navigation.spec.ts: -------------------------------------------------------------------------------- 1 | import percySnapshot from '@percy/playwright'; 2 | import { expect, test } from '@playwright/test'; 3 | 4 | test.describe('Navigation', () => { 5 | test.describe('Static pages', () => { 6 | test('should take screenshot of the homepage', async ({ page }) => { 7 | await page.goto('/'); 8 | 9 | await expect( 10 | page.getByRole('heading', { 11 | name: 'Boilerplate Code for Your Next.js Project with Tailwind CSS', 12 | }), 13 | ).toBeVisible(); 14 | 15 | await percySnapshot(page, 'Homepage'); 16 | }); 17 | 18 | test('should take screenshot of the about page', async ({ page }) => { 19 | await page.goto('/about'); 20 | 21 | await expect( 22 | page.getByRole('link', { 23 | name: 'About', 24 | }), 25 | ).toBeVisible(); 26 | 27 | await percySnapshot(page, 'About'); 28 | }); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /tests/e2e/Sanity.check.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from '@playwright/test'; 2 | 3 | // Checkly is a tool used to monitor deployed environments, such as production or preview environments. 4 | // It runs end-to-end tests with the `.check.spec.ts` extension after each deployment to ensure that the environment is up and running. 5 | // With Checkly, you can monitor your production environment and run `*.check.spec.ts` tests regularly at a frequency of your choice. 6 | // If the tests fail, Checkly will notify you via email, Slack, or other channels of your choice. 7 | // On the other hand, E2E tests ending with `*.spec.ts` are only run before deployment. 8 | // You can run them locally or on CI to ensure that the application is ready for deployment. 9 | 10 | const targetUrl = process.env.ENVIRONMENT_URL || process.env.PRODUCTION_URL; 11 | 12 | if (!targetUrl) { 13 | throw new Error( 14 | 'Please set the ENVIRONMENT_URL or PRODUCTION_URL environment variable', 15 | ); 16 | } 17 | 18 | test.describe('Sanity', () => { 19 | test.describe('Static pages', () => { 20 | test('should display the homepage', async ({ page }) => { 21 | await page.goto(targetUrl); 22 | 23 | await expect( 24 | page.getByRole('heading', { 25 | name: 'Boilerplate Code for Your Next.js Project with Tailwind CSS', 26 | }), 27 | ).toBeVisible(); 28 | }); 29 | 30 | test('should navigate to the about page', async ({ page }) => { 31 | await page.goto(targetUrl); 32 | 33 | await page.getByRole('link', { name: 'About' }).click(); 34 | await expect(page).toHaveURL(/about$/); 35 | 36 | await expect( 37 | page.getByText('Welcome to our About page', { exact: false }), 38 | ).toBeVisible(); 39 | }); 40 | 41 | test('should navigate to the portfolio page', async ({ page }) => { 42 | await page.goto(targetUrl); 43 | 44 | await page.getByRole('link', { name: 'Portfolio' }).click(); 45 | await expect(page).toHaveURL(/portfolio$/); 46 | 47 | await expect( 48 | page.locator('main').getByRole('link', { 49 | name: /^Portfolio/, 50 | }), 51 | ).toHaveCount(6); 52 | }); 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /tests/integration/Guestbook.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from '@playwright/test'; 2 | 3 | // Don't use the default user agent to avoid the requests to be blocked by Clerk middleware. 4 | test.use({ userAgent: '' }); 5 | 6 | test.describe('Guestbook', () => { 7 | test.describe('Basic CRUD operations', () => { 8 | test('should create a new entry in the guestbook and delete it', async ({ 9 | request, 10 | }) => { 11 | const create = await request.post('/api/guestbook', { 12 | data: { 13 | username: 'RANDOM_USERNAME', 14 | body: 'RANDOM_BODY', 15 | }, 16 | }); 17 | const createJson = await create.json(); 18 | 19 | expect(create.status()).toBe(200); 20 | expect(createJson.id).toBeDefined(); 21 | 22 | const del = await request.delete('/api/guestbook', { 23 | data: { 24 | id: createJson.id, 25 | }, 26 | }); 27 | expect(del.status()).toBe(200); 28 | }); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["dom", "dom.iterable", "esnext"], 4 | "module": "esnext", 5 | "moduleResolution": "bundler", 6 | "resolveJsonModule": true, 7 | "removeComments": true, 8 | "preserveConstEnums": true, 9 | "strict": true, 10 | "alwaysStrict": true, 11 | "strictNullChecks": true, 12 | "noUncheckedIndexedAccess": true, 13 | 14 | "noImplicitAny": true, 15 | "noImplicitReturns": true, 16 | "noImplicitThis": true, 17 | "noUnusedLocals": true, 18 | "noUnusedParameters": true, 19 | "allowUnreachableCode": false, 20 | "noFallthroughCasesInSwitch": true, 21 | 22 | "target": "es2017", 23 | "outDir": "out", 24 | "sourceMap": true, 25 | 26 | "esModuleInterop": true, 27 | "allowSyntheticDefaultImports": true, 28 | "allowJs": true, 29 | "checkJs": true, 30 | "skipLibCheck": true, 31 | "forceConsistentCasingInFileNames": true, 32 | 33 | "jsx": "preserve", 34 | "noEmit": true, 35 | "isolatedModules": true, 36 | "incremental": true, 37 | 38 | // Load types 39 | "types": ["vitest/globals"], 40 | 41 | // Path aliases 42 | "baseUrl": ".", 43 | "paths": { 44 | "@/*": ["./src/*"], 45 | "@/public/*": ["./public/*"] 46 | }, 47 | 48 | // Editor support 49 | "plugins": [ 50 | { 51 | "name": "next" 52 | } 53 | ] 54 | }, 55 | "exclude": ["./out/**/*", "./node_modules/**/*"], 56 | "include": [ 57 | "next-env.d.ts", 58 | "**/*.ts", 59 | "**/*.tsx", 60 | ".storybook/*.ts", 61 | ".next/types/**/*.ts", 62 | "**/*.mts" 63 | ] 64 | } 65 | -------------------------------------------------------------------------------- /vitest-setup.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies */ 2 | import '@testing-library/jest-dom/vitest'; 3 | 4 | import failOnConsole from 'vitest-fail-on-console'; 5 | 6 | failOnConsole({ 7 | shouldFailOnDebug: true, 8 | shouldFailOnError: true, 9 | shouldFailOnInfo: true, 10 | shouldFailOnLog: true, 11 | shouldFailOnWarn: true, 12 | }); 13 | -------------------------------------------------------------------------------- /vitest.config.mts: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies */ 2 | import react from '@vitejs/plugin-react'; 3 | import { loadEnv } from 'vite'; 4 | import tsconfigPaths from 'vite-tsconfig-paths'; 5 | import { defineConfig } from 'vitest/config'; 6 | 7 | export default defineConfig({ 8 | plugins: [react(), tsconfigPaths()], 9 | test: { 10 | globals: true, // This is needed by @testing-library to be cleaned up after each test 11 | include: ['src/**/*.test.{js,jsx,ts,tsx}'], 12 | coverage: { 13 | include: ['src/**/*'], 14 | exclude: ['src/**/*.stories.{js,jsx,ts,tsx}', '**/*.d.ts'], 15 | reporter: ['html'], 16 | }, 17 | environmentMatchGlobs: [['**/*.test.tsx', 'jsdom']], 18 | setupFiles: ['./vitest-setup.ts'], 19 | env: loadEnv('', process.cwd(), ''), 20 | }, 21 | }); 22 | --------------------------------------------------------------------------------