├── .all-contributorsrc
├── .env.example
├── .github
├── FUNDING.yml
└── workflows
│ ├── code-quality.yml
│ ├── codeql-analysis.yml
│ ├── codesee-arch-diagram.yml
│ ├── pr-check.yml
│ ├── pre-commit.yml
│ └── review-dog.yml
├── .gitignore
├── .nvmrc
├── .pre-commit-config.yaml
├── .storybook
├── main.js
└── preview.js
├── .vscode
└── settings.json
├── LICENSE
├── README.md
├── SECURITY.md
├── app
└── layout.tsx
├── biome.json
├── bun.lockb
├── components.json
├── components
├── Button
│ └── index.tsx
├── Counter
│ └── index.tsx
├── Header
│ ├── header.module.css
│ ├── index.tsx
│ └── types.ts
├── Layout
│ └── index.tsx
├── ui
│ └── card.tsx
└── utils.ts
├── cspell-tool.txt
├── cspell.json
├── logger.ts
├── mocks
├── browser.ts
├── handlers.ts
└── server.ts
├── next-env.d.ts
├── next.config.js
├── package.json
├── pages
├── _app.tsx
├── api
│ ├── auth
│ │ └── [...nextauth].ts
│ └── hello.ts
├── contact.tsx
├── index.tsx
└── ssr.tsx
├── postcss.config.js
├── prisma
├── migrations
│ ├── 20221223143416_add_next_auth_schema
│ │ └── migration.sql
│ └── migration_lock.toml
└── schema.prisma
├── public
├── favicon.ico
├── logo.svg
└── mockServiceWorker.js
├── renovate.json
├── sandbox.config.json
├── screenshot.png
├── store
├── counter.ts
└── index.ts
├── stories
├── Button.stories.tsx
└── Header.stories.tsx
├── styles
├── Home.module.css
└── globals.css
├── tests
├── Button.test.tsx
└── Header.test.tsx
├── tsconfig.json
├── types
└── next-auth.d.ts
└── vitest.config.ts
/.all-contributorsrc:
--------------------------------------------------------------------------------
1 | {
2 | "files": ["README.md"],
3 | "imageSize": 100,
4 | "commit": false,
5 | "commitType": "docs",
6 | "commitConvention": "angular",
7 | "contributors": [
8 | {
9 | "login": "jellydn",
10 | "name": "Dung Duc Huynh (Kaka)",
11 | "avatar_url": "https://avatars.githubusercontent.com/u/870029?v=4",
12 | "profile": "https://productsway.com/",
13 | "contributions": ["code", "doc"]
14 | },
15 | {
16 | "login": "mikah13",
17 | "name": "Mike Hoang",
18 | "avatar_url": "https://avatars.githubusercontent.com/u/25890552?v=4",
19 | "profile": "https://mike-hoang-dev.vercel.app/",
20 | "contributions": ["code"]
21 | },
22 | {
23 | "login": "salmansheri",
24 | "name": "Salman Sheriff",
25 | "avatar_url": "https://avatars.githubusercontent.com/u/95226945?v=4",
26 | "profile": "https://github.com/salmansheri",
27 | "contributions": ["code"]
28 | }
29 | ],
30 | "contributorsPerLine": 7,
31 | "skipCi": true,
32 | "repoType": "github",
33 | "repoHost": "https://github.com",
34 | "projectName": "next-app-starter",
35 | "projectOwner": "jellydn"
36 | }
37 |
--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------
1 | NEXT_PUBLIC_API_MOCKING=yes/no
2 | NEXTAUTH_URL=http://localhost:3000
3 | EMAIL_SERVER=
4 | EMAIL_FROM=
5 | DATABASE_URL='postgresql://'
6 | SHADOW_DATABASE_URL='postgresql://'
7 | # Used to encrypt the NextAuth.js JWT, and to hash email verification tokens. This is the default value for the secret option in NextAuth and Middleware.
8 | NEXTAUTH_SECRET=JWT_SECRET_KEY
9 |
10 | # Github NextAuth Credentials
11 | GITHUB_ID=
12 | GITHUB_SECRET=
13 |
14 |
15 | # Google NextAuth Credentials
16 | GOOGLE_CLIENT_ID=
17 | GOOGLE_CLIENT_SECRET=
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | github: [jellydn]
2 | ko_fi: dunghd
3 |
--------------------------------------------------------------------------------
/.github/workflows/code-quality.yml:
--------------------------------------------------------------------------------
1 | name: Code quality
2 |
3 | on:
4 | push:
5 | pull_request:
6 |
7 | jobs:
8 | quality:
9 | runs-on: ubuntu-latest
10 | permissions:
11 | # Give the default GITHUB_TOKEN write permission to commit and push the
12 | # added or changed files to the repository.
13 | contents: write
14 | steps:
15 | - name: Checkout
16 | uses: actions/checkout@v4
17 | - name: Setup Biome
18 | uses: biomejs/setup-biome@v2
19 | with:
20 | version: latest
21 | - name: Run Biome
22 | run: biome ci .
23 | continue-on-error: true # Continue even if biome fails
24 |
25 | # Commit all changed files back to the repository
26 | - uses: stefanzweifel/git-auto-commit-action@v5
27 |
--------------------------------------------------------------------------------
/.github/workflows/codeql-analysis.yml:
--------------------------------------------------------------------------------
1 | # For most projects, this workflow file will not need changing; you simply need
2 | # to commit it to your repository.
3 | #
4 | # You may wish to alter this file to override the set of languages analyzed,
5 | # or to provide custom queries or build logic.
6 | #
7 | # ******** NOTE ********
8 | # We have attempted to detect the languages in your repository. Please check
9 | # the `language` matrix defined below to confirm you have the correct set of
10 | # supported CodeQL languages.
11 | #
12 | name: "CodeQL"
13 |
14 | on:
15 | push:
16 | branches: [ "main" ]
17 | pull_request:
18 | # The branches below must be a subset of the branches above
19 | branches: [ "main" ]
20 | schedule:
21 | - cron: '41 13 * * 6'
22 |
23 | jobs:
24 | analyze:
25 | name: Analyze
26 | runs-on: ubuntu-latest
27 | permissions:
28 | actions: read
29 | contents: read
30 | security-events: write
31 |
32 | strategy:
33 | fail-fast: false
34 | matrix:
35 | language: [ 'javascript' ]
36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
37 | # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
38 |
39 | steps:
40 | - name: Checkout repository
41 | uses: actions/checkout@v4
42 |
43 | # Initializes the CodeQL tools for scanning.
44 | - name: Initialize CodeQL
45 | uses: github/codeql-action/init@v3
46 | with:
47 | languages: ${{ matrix.language }}
48 | # If you wish to specify custom queries, you can do so here or in a config file.
49 | # By default, queries listed here will override any specified in a config file.
50 | # Prefix the list here with "+" to use these queries and those in the config file.
51 |
52 | # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
53 | # queries: security-extended,security-and-quality
54 |
55 |
56 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
57 | # If this step fails, then you should remove it and run the build manually (see below)
58 | - name: Autobuild
59 | uses: github/codeql-action/autobuild@v3
60 |
61 | # ℹ️ Command-line programs to run using the OS shell.
62 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
63 |
64 | # If the Autobuild fails above, remove it and uncomment the following three lines.
65 | # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
66 |
67 | # - run: |
68 | # echo "Run, Build Application using script"
69 | # ./location_of_script_within_repo/buildscript.sh
70 |
71 | - name: Perform CodeQL Analysis
72 | uses: github/codeql-action/analyze@v3
73 |
--------------------------------------------------------------------------------
/.github/workflows/codesee-arch-diagram.yml:
--------------------------------------------------------------------------------
1 | # This workflow was added by CodeSee. Learn more at https://codesee.io/
2 | # This is v2.0 of this workflow file
3 | on:
4 | push:
5 | branches:
6 | - main
7 | pull_request_target:
8 | types: [opened, synchronize, reopened]
9 |
10 | name: CodeSee
11 |
12 | permissions: read-all
13 |
14 | jobs:
15 | codesee:
16 | runs-on: ubuntu-latest
17 | continue-on-error: true
18 | name: Analyze the repo with CodeSee
19 | steps:
20 | - uses: Codesee-io/codesee-action@v2
21 | with:
22 | codesee-token: ${{ secrets.CODESEE_ARCH_DIAG_API_TOKEN }}
23 |
--------------------------------------------------------------------------------
/.github/workflows/pr-check.yml:
--------------------------------------------------------------------------------
1 | name: Linter & Typecheck
2 | on:
3 | push:
4 | branches:
5 | - main
6 | pull_request:
7 | branches:
8 | - main
9 | workflow_dispatch:
10 | jobs:
11 | typecheck:
12 | runs-on: ubuntu-latest
13 | continue-on-error: true
14 | steps:
15 | - name: 🛑 Cancel Previous Runs
16 | uses: styfle/cancel-workflow-action@0.12.1
17 | - id: checkout
18 | name: Checkout
19 | uses: actions/checkout@v4
20 | - name: Install bun
21 | uses: oven-sh/setup-bun@v2
22 | - name: Install dependencies
23 | run: bun install
24 | - id: Typecheck
25 | name: Checking Typescript Error
26 | run: |
27 | bun run typecheck
28 | linter:
29 | runs-on: ubuntu-latest
30 | continue-on-error: true
31 | steps:
32 | - name: 🛑 Cancel Previous Runs
33 | uses: styfle/cancel-workflow-action@0.12.1
34 | - id: checkout
35 | name: Checkout
36 | uses: actions/checkout@v4
37 | - name: Install bun
38 | uses: oven-sh/setup-bun@v2
39 | - name: Install dependencies
40 | run: bun install
41 | - id: linter
42 | name: Linter check
43 | run: |
44 | bun run lint
45 |
--------------------------------------------------------------------------------
/.github/workflows/pre-commit.yml:
--------------------------------------------------------------------------------
1 | name: pre-commit
2 |
3 | on:
4 | pull_request:
5 | push:
6 | branches: [main]
7 |
8 | jobs:
9 | pre-commit:
10 | runs-on: ubuntu-latest
11 | permissions:
12 | # Give the default GITHUB_TOKEN write permission to commit and push the
13 | # added or changed files to the repository.
14 | contents: write
15 | steps:
16 | - uses: actions/checkout@v4
17 | - uses: actions/setup-python@v5
18 | - name: Install pre-commit
19 | run: pip install pre-commit
20 |
21 | - name: Run pre-commit
22 | run: pre-commit run --all-files
23 | continue-on-error: true # Continue even if pre-commit fails
24 |
25 | # Commit all changed files back to the repository
26 | - uses: stefanzweifel/git-auto-commit-action@v5
27 |
--------------------------------------------------------------------------------
/.github/workflows/review-dog.yml:
--------------------------------------------------------------------------------
1 | name: reviewdog
2 |
3 | on:
4 | pull_request:
5 |
6 | jobs:
7 | review:
8 | name: Biome
9 | runs-on: ubuntu-latest
10 | permissions:
11 | contents: read
12 | pull-requests: write
13 | steps:
14 | - uses: actions/checkout@v4
15 | - uses: mongolyy/reviewdog-action-biome@v1
16 | with:
17 | github_token: ${{ secrets.github_token }}
18 | reporter: github-pr-review
19 |
20 | misspell:
21 | name: Misspell
22 | runs-on: ubuntu-latest
23 | permissions:
24 | contents: read
25 | pull-requests: write
26 | steps:
27 | - uses: actions/checkout@v4
28 | - name: misspell
29 | uses: reviewdog/action-misspell@v1
30 | with:
31 | github_token: ${{ secrets.github_token }}
32 | locale: "US"
33 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # next.js
12 | /.next/
13 | /out/
14 |
15 | # production
16 | /build
17 |
18 | # misc
19 | .DS_Store
20 | *.pem
21 |
22 | # debug
23 | npm-debug.log*
24 | yarn-debug.log*
25 | yarn-error.log*
26 |
27 | # local env files
28 | .env
29 | .env.local
30 | .env.development.local
31 | .env.test.local
32 | .env.production.local
33 |
34 | # vercel
35 | .vercel
36 |
37 | .husky
38 |
39 | .eslintcache
40 | .vscode
41 | .yarn
42 |
--------------------------------------------------------------------------------
/.nvmrc:
--------------------------------------------------------------------------------
1 | lts/*
2 |
--------------------------------------------------------------------------------
/.pre-commit-config.yaml:
--------------------------------------------------------------------------------
1 | repos:
2 | - repo: https://github.com/jellydn/sort-package-json
3 | rev: "1686d9d2ddfb065c4514c637ee4b9985dfbf01dd" # Use the sha / tag you want to point at
4 | hooks:
5 | - id: sort-package-json
6 |
7 | - repo: https://github.com/pre-commit/mirrors-prettier
8 | rev: "v4.0.0-alpha.8" # Use the sha or tag you want to point at
9 | hooks:
10 | - id: prettier
11 | # Those are not supported by biomejs yet, refer https://biomejs.dev/internals/language-support/
12 | types_or: [html, css, markdown]
13 | - repo: https://github.com/biomejs/pre-commit
14 | rev: "v2.0.0-beta.1" # Use the sha / tag you want to point at
15 | hooks:
16 | - id: biome-check
17 | exclude: "package.json"
18 | additional_dependencies: ["@biomejs/biome@1.9.4"]
19 |
--------------------------------------------------------------------------------
/.storybook/main.js:
--------------------------------------------------------------------------------
1 | import { mergeConfig } from 'vite';
2 |
3 | export default {
4 | stories: [
5 | '../stories/**/*.stories.mdx',
6 | '../stories/**/*.stories.@(js|jsx|ts|tsx)',
7 | ],
8 | addons: ['@storybook/addon-essentials', 'storybook-addon-designs'],
9 | framework: '@storybook/nextjs',
10 | async viteFinal(config) {
11 | // Merge custom configuration into the default config
12 | return mergeConfig(config, {
13 | // Add storybook-specific dependencies to pre-optimization
14 | optimizeDeps: {
15 | include: ['storybook-addon-designs'],
16 | },
17 | });
18 | },
19 | };
20 |
--------------------------------------------------------------------------------
/.storybook/preview.js:
--------------------------------------------------------------------------------
1 | export const parameters = {
2 | actions: { argTypesRegex: '^on[A-Z].*' },
3 | controls: {
4 | matchers: {
5 | color: /(background|color)$/i,
6 | date: /Date$/,
7 | },
8 | },
9 | };
10 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "typescript.tsdk": "node_modules/typescript/lib",
3 | "typescript.enablePromptUseWorkspaceTsdk": true
4 | }
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 Huynh Duc Dung
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Welcome to next-app-starter 👋
2 |
3 |
4 |
5 | [](#contributors-)
6 |
7 |
8 |
9 | 
10 | [](https://twitter.com/jellydn)
11 |
12 | > Another awesome starter for your app
13 |
14 | [](https://www.youtube-nocookie.com/embed/videoseries?list=PLOdXIcVPTyB_2IiS36upNkEw2fLhyb5D6)
15 |
16 | ## 🏠 [Homepage](https://github.com/jellydn/next-app-starter)
17 |
18 | ### ✨ [Demo](https://next-app-starter.vercel.app)
19 |
20 | 
21 |
22 | ## Pre-requirements
23 |
24 | - [Bun — A fast all-in-one JavaScript runtime](https://bun.sh/)
25 | - [Biome, toolchain of the web](https://biomejs.dev/)
26 |
27 | ## 💻 Stack
28 |
29 | - [NextJS: the React Framework for Production](https://nextjs.org/docs)
30 | - [Tailwindcss: rapidly build modern websites without ever leaving your HTML](https://tailwindcss.com/)
31 | - [shadcn/ui: Beautifully designed components built with Radix UI and Tailwind CSS.](https://github.com/shadcn/ui)
32 | - [Jotai: primitive and flexible state management for React.](https://docs.pmnd.rs/jotai/introduction)
33 | - [Prisma: next-generation ORM for Node.js and TypeScrip](https://www.prisma.io/)
34 | - [NextAuth.js: Authentication for Next.js](https://next-auth.js.org/v3/getting-started/introduction)
35 | - [next-validations: NextJS API Validations, support Yup, Fastest-Validator, Joi, and more](https://next-validations.productsway.com/)
36 | - [zod: TypeScript-first schema validation with static type inference](https://github.com/colinhacks/zod)
37 | - [consola: Elegant Console Logger for Node.js and Browser 🐨](https://github.com/unjs/consola)
38 | - [Storybook: build bulletproof UI components faster](https://storybook.js.org)
39 | - [React-hook-form: performance, flexible and extensible forms with easy-to-use validation](https://www.react-hook-form.com/)
40 | - [react-testing: simple and complete testing utilities that encourage good testing practices](https://testing-library.com/)
41 | - [React-query: performant and powerful data synchronization for React](https://react-query.tanstack.com/)
42 |
43 | ## 📝 Project Summary
44 |
45 | - [**app**](app): Main application logic and entry point.
46 | - [**components**](components): Reusable UI components.
47 | - [**pages**](pages): Individual pages/views of the application.
48 | - [**prisma**](prisma): Database ORM and migration scripts.
49 | - [**public**](public): Static assets accessible to the public.
50 | - [**store**](store): State management for the application.
51 | - [**tests**](tests): Unit and integration tests.
52 | - [**types**](types): Custom TypeScript types and interfaces.
53 | - [**storybook**](storybook): Component library and documentation.
54 | - [**.github/workflows**](.github/workflows): CI/CD workflows for GitHub Actions.
55 |
56 | ## Install
57 |
58 | ```sh
59 | bun install
60 | ```
61 |
62 | ## Usage
63 |
64 | Create .env file base on .env.example then run below command
65 |
66 | ```sh
67 | bun run dev
68 | ```
69 |
70 | ## Run tests
71 |
72 | ```sh
73 | bun run test
74 | ```
75 |
76 | ## Run storybook
77 |
78 | ```sh
79 | bun run storybook
80 | ```
81 |
82 | ## Pre-commit
83 |
84 | This project uses [pre-commit](https://pre-commit.com/) to enforce code quality and consistency. To install pre-commit hooks, run:
85 |
86 | ```sh
87 | pre-commit install
88 | ```
89 |
90 | ## 📄 License
91 |
92 | This project is licensed under the **MIT License** - see the [**MIT License**](https://github.com/jellydn/next-app-starter/blob/main/LICENSE) file for details.
93 |
94 | ## Author
95 |
96 | - Website: https://productsway.com/
97 | - Twitter: [@jellydn](https://twitter.com/jellydn)
98 | - Github: [@jellydn](https://github.com/jellydn)
99 |
100 | ## Start History 🌟
101 |
102 | [](https://star-history.com/#jellydn/next-app-starter)
103 |
104 | ## Show your support
105 |
106 | [](https://ko-fi.com/dunghd)
107 | [](https://paypal.me/dunghd)
108 | [](https://www.buymeacoffee.com/dunghd)
109 |
110 | Give a ⭐️ if this project helped you!
111 |
112 | ## Contributors ✨
113 |
114 | Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
115 |
116 |
117 |
118 |
119 |
128 |
129 |
130 |
131 |
132 |
133 |
134 | This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!
135 |
--------------------------------------------------------------------------------
/SECURITY.md:
--------------------------------------------------------------------------------
1 | # Security Policy
2 |
3 | ## Supported Versions
4 |
5 | Use this section to tell people about which versions of your project are
6 | currently being supported with security updates.
7 |
8 | | Version | Supported |
9 | | ------- | ------------------ |
10 | | 5.1.x | :white_check_mark: |
11 | | 5.0.x | :x: |
12 | | 4.0.x | :white_check_mark: |
13 | | < 4.0 | :x: |
14 |
15 | ## Reporting a Vulnerability
16 |
17 | Use this section to tell people how to report a vulnerability.
18 |
19 | Tell them where to go, how often they can expect to get an update on a
20 | reported vulnerability, what to expect if the vulnerability is accepted or
21 | declined, etc.
22 |
--------------------------------------------------------------------------------
/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import Script from 'next/script';
2 | import { ReactNode } from 'react';
3 |
4 | function RootLayout({
5 | // Layouts must accept a children prop.
6 | // This will be populated with nested layouts or pages
7 | children,
8 | }: {
9 | readonly children: ReactNode;
10 | }) {
11 | return (
12 |
13 |
14 |
20 | NextJs Starter App
21 |
22 | {children}
23 |
24 | );
25 | }
26 |
27 | export default RootLayout;
28 |
--------------------------------------------------------------------------------
/biome.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
3 | "vcs": {
4 | "clientKind": "git",
5 | "enabled": true,
6 | "useIgnoreFile": true,
7 | "defaultBranch": "main"
8 | },
9 | "files": {
10 | "ignore": ["public/*.js"]
11 | },
12 | "formatter": {
13 | "enabled": true,
14 | "formatWithErrors": false,
15 | "indentStyle": "space",
16 | "indentWidth": 4,
17 | "lineEnding": "lf",
18 | "lineWidth": 80,
19 | "attributePosition": "auto"
20 | },
21 | "organizeImports": {
22 | "enabled": true
23 | },
24 | "linter": {
25 | "enabled": true,
26 | "rules": {
27 | "recommended": false,
28 | "a11y": {
29 | "noBlankTarget": "error",
30 | "useButtonType": "error"
31 | },
32 | "complexity": {
33 | "noBannedTypes": "error",
34 | "noExtraBooleanCast": "error",
35 | "noMultipleSpacesInRegularExpressionLiterals": "error",
36 | "noStaticOnlyClass": "error",
37 | "noUselessCatch": "error",
38 | "noUselessConstructor": "error",
39 | "noUselessEmptyExport": "error",
40 | "noUselessFragments": "error",
41 | "noUselessLabel": "error",
42 | "noUselessLoneBlockStatements": "error",
43 | "noUselessRename": "error",
44 | "noUselessTernary": "error",
45 | "noUselessTypeConstraint": "error",
46 | "noVoid": "error",
47 | "noWith": "error",
48 | "useLiteralKeys": "error",
49 | "useOptionalChain": "error",
50 | "useRegexLiterals": "error"
51 | },
52 | "correctness": {
53 | "noChildrenProp": "error",
54 | "noConstAssign": "error",
55 | "noConstantCondition": "error",
56 | "noConstructorReturn": "error",
57 | "noEmptyCharacterClassInRegex": "error",
58 | "noEmptyPattern": "error",
59 | "noGlobalObjectCalls": "error",
60 | "noInnerDeclarations": "error",
61 | "noInvalidConstructorSuper": "error",
62 | "noInvalidNewBuiltin": "error",
63 | "noNonoctalDecimalEscape": "error",
64 | "noPrecisionLoss": "error",
65 | "noSelfAssign": "error",
66 | "noSetterReturn": "error",
67 | "noSwitchDeclarations": "error",
68 | "noUndeclaredVariables": "error",
69 | "noUnreachable": "error",
70 | "noUnreachableSuper": "error",
71 | "noUnsafeFinally": "error",
72 | "noUnsafeOptionalChaining": "error",
73 | "noUnusedLabels": "error",
74 | "noUnusedVariables": "error",
75 | "noVoidElementsWithChildren": "error",
76 | "useExhaustiveDependencies": "warn",
77 | "useHookAtTopLevel": "error",
78 | "useIsNan": "error",
79 | "useJsxKeyInIterable": "error",
80 | "useValidForDirection": "error",
81 | "useYield": "error"
82 | },
83 | "security": {
84 | "noDangerouslySetInnerHtml": "error",
85 | "noDangerouslySetInnerHtmlWithChildren": "error",
86 | "noGlobalEval": "error"
87 | },
88 | "style": {
89 | "noArguments": "error",
90 | "noCommaOperator": "error",
91 | "noImplicitBoolean": "error",
92 | "noInferrableTypes": "error",
93 | "noNamespace": "error",
94 | "noNegationElse": "error",
95 | "noRestrictedGlobals": {
96 | "level": "error",
97 | "options": {
98 | "deniedGlobals": ["event", "atob", "btoa"]
99 | }
100 | },
101 | "noVar": "error",
102 | "useAsConstAssertion": "error",
103 | "useBlockStatements": "error",
104 | "useCollapsedElseIf": "error",
105 | "useConsistentArrayType": {
106 | "level": "error",
107 | "options": {
108 | "syntax": "shorthand"
109 | }
110 | },
111 | "useConst": "error",
112 | "useDefaultParameterLast": "error",
113 | "useExponentiationOperator": "error",
114 | "useForOf": "error",
115 | "useFragmentSyntax": "error",
116 | "useLiteralEnumMembers": "error",
117 | "useNumericLiterals": "error",
118 | "useShorthandAssign": "error",
119 | "useShorthandFunctionType": "error",
120 | "useSingleVarDeclarator": "error"
121 | },
122 | "suspicious": {
123 | "noArrayIndexKey": "error",
124 | "noAsyncPromiseExecutor": "error",
125 | "noCatchAssign": "error",
126 | "noClassAssign": "error",
127 | "noCommentText": "error",
128 | "noCompareNegZero": "error",
129 | "noControlCharactersInRegex": "error",
130 | "noDebugger": "error",
131 | "noDoubleEquals": "error",
132 | "noDuplicateCase": "error",
133 | "noDuplicateClassMembers": "error",
134 | "noDuplicateJsxProps": "error",
135 | "noDuplicateObjectKeys": "error",
136 | "noDuplicateParameters": "error",
137 | "noEmptyBlockStatements": "error",
138 | "noExtraNonNullAssertion": "error",
139 | "noFallthroughSwitchClause": "error",
140 | "noFunctionAssign": "error",
141 | "noGlobalAssign": "error",
142 | "noImportAssign": "error",
143 | "noLabelVar": "error",
144 | "noMisleadingCharacterClass": "error",
145 | "noMisleadingInstantiator": "error",
146 | "noPrototypeBuiltins": "error",
147 | "noRedeclare": "error",
148 | "noSelfCompare": "error",
149 | "noShadowRestrictedNames": "error",
150 | "noUnsafeDeclarationMerging": "error",
151 | "noUnsafeNegation": "error",
152 | "useDefaultSwitchClauseLast": "error",
153 | "useGetterReturn": "error",
154 | "useNamespaceKeyword": "error",
155 | "useValidTypeof": "error"
156 | }
157 | }
158 | },
159 | "javascript": {
160 | "formatter": {
161 | "jsxQuoteStyle": "double",
162 | "quoteProperties": "asNeeded",
163 | "trailingComma": "all",
164 | "semicolons": "always",
165 | "arrowParentheses": "always",
166 | "bracketSpacing": true,
167 | "bracketSameLine": false,
168 | "quoteStyle": "single",
169 | "attributePosition": "auto"
170 | }
171 | },
172 | "overrides": [
173 | {
174 | "include": [
175 | "*.stories.@(ts|tsx|js|jsx|mjs|cjs)",
176 | "*.story.@(ts|tsx|js|jsx|mjs|cjs)"
177 | ],
178 | "linter": {
179 | "rules": {
180 | "correctness": {
181 | "useHookAtTopLevel": "off"
182 | }
183 | }
184 | }
185 | },
186 | {
187 | "include": [".storybook/main.@(js|cjs|mjs|ts)"],
188 | "linter": {
189 | "rules": {}
190 | }
191 | },
192 | {
193 | "include": ["**/*.d.ts"],
194 | "linter": {
195 | "rules": {
196 | "correctness": {
197 | "noUnusedVariables": "off"
198 | }
199 | }
200 | }
201 | },
202 | {
203 | "include": ["**/*.test-d.ts"],
204 | "linter": {
205 | "rules": {}
206 | }
207 | },
208 | {
209 | "include": ["**/*.tsx"],
210 | "linter": {
211 | "rules": {}
212 | }
213 | }
214 | ]
215 | }
216 |
--------------------------------------------------------------------------------
/bun.lockb:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jellydn/next-app-starter/29f7aa2481aa658af5f54ddeee76d81e8c4a0ed0/bun.lockb
--------------------------------------------------------------------------------
/components.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://ui.shadcn.com/schema.json",
3 | "style": "default",
4 | "rsc": false,
5 | "tailwind": {
6 | "config": "tailwind.config.js",
7 | "css": "styles/globals.css",
8 | "baseColor": "neutral",
9 | "cssVariables": true
10 | },
11 | "aliases": {
12 | "components": "/components",
13 | "utils": "/components/utils"
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/components/Button/index.tsx:
--------------------------------------------------------------------------------
1 | import { cva } from 'class-variance-authority';
2 | import type { VariantProps } from 'class-variance-authority';
3 | import { type ButtonHTMLAttributes } from 'react';
4 |
5 | const buttonStyles = cva(
6 | [
7 | 'font-semibold',
8 | 'bg-transparent',
9 | 'border',
10 | 'border-blue-500',
11 | 'inline-flex',
12 | 'items-center',
13 | 'justify-center',
14 | 'rounded',
15 | 'hover:bg-blue-500',
16 | 'text-blue-800',
17 | 'hover:text-white',
18 | 'hover:border-transparent',
19 | 'text',
20 | ],
21 | {
22 | variants: {
23 | intent: {
24 | primary: [
25 | 'bg-blue-600',
26 | 'focus:border-blue-500',
27 | 'focus:ring-2',
28 | 'focus:ring-blue-200',
29 | ],
30 | },
31 |
32 | size: {
33 | s: ['px-4', 'py-2', 'text-base'],
34 | m: ['px-6', 'py-3', 'text-lg'],
35 | l: ['px-8', 'py-4', 'text-xl'],
36 | },
37 | },
38 | defaultVariants: {
39 | size: 'm',
40 | },
41 | },
42 | );
43 |
44 | export type ButtonProps = VariantProps;
45 | type Props = Record &
46 | ButtonProps &
47 | ButtonHTMLAttributes;
48 |
49 | function Button({ children, size, type = 'button', ...rest }: Props) {
50 | return (
51 | // eslint-disable-next-line react/button-has-type
52 |
55 | );
56 | }
57 |
58 | export default Button;
59 |
--------------------------------------------------------------------------------
/components/Counter/index.tsx:
--------------------------------------------------------------------------------
1 | import { useAtom } from 'jotai';
2 |
3 | import counterAtom from '../../store/counter';
4 | import Button from '../Button';
5 |
6 | function Counter() {
7 | const [count, setCount] = useAtom(counterAtom);
8 | return (
9 |
10 |
18 | Counter: {count}
19 |
20 | );
21 | }
22 |
23 | export default Counter;
24 |
--------------------------------------------------------------------------------
/components/Header/header.module.css:
--------------------------------------------------------------------------------
1 | .avatar {
2 | border-radius: 2rem;
3 | float: left;
4 | height: 2.8rem;
5 | width: 2.8rem;
6 | background-color: white;
7 | background-size: cover;
8 | background-repeat: no-repeat;
9 | }
10 |
--------------------------------------------------------------------------------
/components/Header/index.tsx:
--------------------------------------------------------------------------------
1 | import { signIn, signOut, useSession } from 'next-auth/react';
2 | import Image from 'next/image';
3 | import Link from 'next/link';
4 |
5 | import Button from '../Button';
6 | import styles from './header.module.css';
7 | import { type HeaderProps } from './types';
8 |
9 | function Header({ links = [] }: HeaderProps) {
10 | const { data: session, status } = useSession();
11 | const loading = status === 'loading';
12 |
13 | if (loading) {
14 | return (
15 |
16 | Loading...
17 |
18 | );
19 | }
20 |
21 | return (
22 |
103 | );
104 | }
105 |
106 | export default Header;
107 | export * from './types';
108 |
--------------------------------------------------------------------------------
/components/Header/types.ts:
--------------------------------------------------------------------------------
1 | export type HeaderProps = {
2 | links: Array<{
3 | title: string;
4 | url: string;
5 | }>;
6 | };
7 |
--------------------------------------------------------------------------------
/components/Layout/index.tsx:
--------------------------------------------------------------------------------
1 | import { ReactNode } from 'react';
2 | import Header from '../Header';
3 |
4 | function Layout({ children }: { readonly children: ReactNode }) {
5 | return (
6 |
7 |
15 | {children}
16 |
17 | );
18 | }
19 |
20 | export default Layout;
21 |
--------------------------------------------------------------------------------
/components/ui/card.tsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable react/prop-types */
2 | import * as React from 'react';
3 |
4 | import { cn } from '../utils';
5 |
6 | const Card = React.forwardRef<
7 | HTMLDivElement,
8 | React.HTMLAttributes
9 | >(({ className, ...props }, ref) => (
10 |
18 | ));
19 | Card.displayName = 'Card';
20 |
21 | const CardHeader = React.forwardRef<
22 | HTMLDivElement,
23 | React.HTMLAttributes
24 | >(({ className, ...props }, ref) => (
25 |
30 | ));
31 | CardHeader.displayName = 'CardHeader';
32 |
33 | const CardTitle = React.forwardRef<
34 | HTMLParagraphElement,
35 | React.HTMLAttributes
36 | >(({ className, ...props }, ref) => (
37 |
45 | ));
46 | CardTitle.displayName = 'CardTitle';
47 |
48 | const CardDescription = React.forwardRef<
49 | HTMLParagraphElement,
50 | React.HTMLAttributes
51 | >(({ className, ...props }, ref) => (
52 |
57 | ));
58 | CardDescription.displayName = 'CardDescription';
59 |
60 | const CardContent = React.forwardRef<
61 | HTMLDivElement,
62 | React.HTMLAttributes
63 | >(({ className, ...props }, ref) => (
64 |
65 | ));
66 | CardContent.displayName = 'CardContent';
67 |
68 | const CardFooter = React.forwardRef<
69 | HTMLDivElement,
70 | React.HTMLAttributes
71 | >(({ className, ...props }, ref) => (
72 |
77 | ));
78 | CardFooter.displayName = 'CardFooter';
79 |
80 | export {
81 | Card,
82 | CardHeader,
83 | CardFooter,
84 | CardTitle,
85 | CardDescription,
86 | CardContent,
87 | };
88 |
--------------------------------------------------------------------------------
/components/utils.ts:
--------------------------------------------------------------------------------
1 | import { type ClassValue, clsx } from 'clsx';
2 | import { twMerge } from 'tailwind-merge';
3 |
4 | export function cn(...inputs: ClassValue[]) {
5 | return twMerge(clsx(inputs));
6 | }
7 |
--------------------------------------------------------------------------------
/cspell-tool.txt:
--------------------------------------------------------------------------------
1 | adsbygoogle
2 | Nonoctal
3 | Instantiator
4 | consola
5 | typecheck
6 | headlessui
7 | loglevel
8 | biomejs
9 | rustywind
10 | shadcn
11 | jellydn
12 | kofi
13 | buymeacoffee
14 | Huynh
15 | Hoang
16 | Salman
17 | automerge
18 | Bitstream
--------------------------------------------------------------------------------
/cspell.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://raw.githubusercontent.com/streetsidesoftware/cspell/main/cspell.schema.json",
3 | "version": "0.2",
4 | "language": "en",
5 | "globRoot": ".",
6 | "dictionaryDefinitions": [
7 | {
8 | "name": "cspell-tool",
9 | "path": "./cspell-tool.txt",
10 | "addWords": true
11 | }
12 | ],
13 | "dictionaries": ["cspell-tool"],
14 | "ignorePaths": ["node_modules", "dist", "build", "/cspell-tool.txt"]
15 | }
16 |
--------------------------------------------------------------------------------
/logger.ts:
--------------------------------------------------------------------------------
1 | import { consola } from 'consola';
2 |
3 | const logger = consola.create({});
4 |
5 | export default logger;
6 |
--------------------------------------------------------------------------------
/mocks/browser.ts:
--------------------------------------------------------------------------------
1 | import { setupWorker } from 'msw/browser';
2 |
3 | import handlers from './handlers';
4 |
5 | export const browser = setupWorker(...handlers);
6 |
7 | export default browser;
8 |
--------------------------------------------------------------------------------
/mocks/handlers.ts:
--------------------------------------------------------------------------------
1 | import { http, HttpResponse } from 'msw';
2 |
3 | const handlers = [
4 | http.post('/login', async () => {
5 | // Persist user's authentication in the session
6 | sessionStorage.setItem('is-authenticated', 'true');
7 |
8 | return new HttpResponse(null, { status: 200 });
9 | }),
10 | http.get('/user', async () => {
11 | // Check if the user is authenticated in this session
12 | const isAuthenticated = sessionStorage.getItem('is-authenticated');
13 | if (!isAuthenticated) {
14 | // If not authenticated, respond with a 403 error
15 | return HttpResponse.json(
16 | {
17 | errorMessage: 'Not authorized',
18 | },
19 | { status: 403 },
20 | );
21 | }
22 |
23 | // If authenticated, return a mocked user details
24 |
25 | return HttpResponse.json(
26 | { username: 'admin' },
27 | {
28 | status: 200,
29 | },
30 | );
31 | }),
32 | ];
33 | export default handlers;
34 |
--------------------------------------------------------------------------------
/mocks/server.ts:
--------------------------------------------------------------------------------
1 | import { setupServer } from 'msw/node';
2 |
3 | import handlers from './handlers';
4 |
5 | export const server = setupServer(...handlers);
6 |
7 | export default server;
8 |
--------------------------------------------------------------------------------
/next-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 | ///
4 |
5 | // NOTE: This file should not be edited
6 | // see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
7 |
--------------------------------------------------------------------------------
/next.config.js:
--------------------------------------------------------------------------------
1 | const { createSecureHeaders } = require('next-secure-headers');
2 |
3 | /** @type {import('next').NextConfig} */
4 | module.exports = {
5 | async headers() {
6 | return [{ source: '/(.*)', headers: createSecureHeaders() }];
7 | },
8 | images: {
9 | dangerouslyAllowSVG: true,
10 | remotePatterns: [
11 | {
12 | protocol: 'https',
13 | hostname: 'vercel.com',
14 | },
15 | ],
16 | },
17 | };
18 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "next-app-starter",
3 | "version": "0.3.0",
4 | "private": true,
5 | "scripts": {
6 | "build": "prisma generate && next build",
7 | "build-storybook": "storybook build",
8 | "check": "biome check --apply .",
9 | "coverage": "vitest run --coverage",
10 | "dev": "next dev",
11 | "dev:turbo": "next dev --turbo",
12 | "lint": "biome lint --apply .",
13 | "start": "next start",
14 | "storybook": "storybook dev -p 6006",
15 | "test": "vitest",
16 | "test:ui": "vitest --ui",
17 | "typecheck": "tsc --noEmit"
18 | },
19 | "babel": {
20 | "presets": [
21 | "next/babel"
22 | ]
23 | },
24 | "dependencies": {
25 | "@headlessui/react": "2.2.4",
26 | "@heroicons/react": "2.2.0",
27 | "@hookform/devtools": "4.4.0",
28 | "@hookform/resolvers": "5.0.1",
29 | "@next-auth/prisma-adapter": "1.0.7",
30 | "@prisma/client": "6.8.2",
31 | "@radix-ui/react-icons": "1.3.2",
32 | "@radix-ui/react-slot": "1.2.3",
33 | "@tailwindcss/forms": "0.5.10",
34 | "@tanstack/react-query": "5.79.2",
35 | "@tanstack/react-query-devtools": "5.79.2",
36 | "@typeschema/zod": "0.14.0",
37 | "class-variance-authority": "0.7.1",
38 | "clsx": "2.1.1",
39 | "consola": "3.4.2",
40 | "jotai": "2.12.5",
41 | "loglevel": "1.9.2",
42 | "lucide-react": "0.511.0",
43 | "next": "15.3.3",
44 | "next-auth": "4.24.11",
45 | "next-secure-headers": "2.2.0",
46 | "next-validations": "1.1.0",
47 | "nodemailer": "7.0.3",
48 | "react": "19.1.0",
49 | "react-dom": "19.1.0",
50 | "react-hook-form": "7.57.0",
51 | "tailwind-merge": "3.3.0",
52 | "tailwindcss-animate": "1.0.7",
53 | "zod": "3.25.48"
54 | },
55 | "devDependencies": {
56 | "@babel/core": "7.27.4",
57 | "@biomejs/biome": "1.9.4",
58 | "@storybook/addon-essentials": "8.6.14",
59 | "@storybook/addon-interactions": "8.6.14",
60 | "@storybook/addon-links": "8.6.14",
61 | "@storybook/nextjs": "8.6.14",
62 | "@storybook/react": "8.6.14",
63 | "@storybook/testing-library": "0.2.2",
64 | "@tailwindcss/postcss": "4.1.8",
65 | "@testing-library/jest-dom": "6.6.3",
66 | "@testing-library/react": "16.3.0",
67 | "@types/react": "19.1.6",
68 | "@vitejs/plugin-react": "4.5.0",
69 | "@vitest/ui": "3.2.0",
70 | "babel-jest": "29.7.0",
71 | "babel-loader": "10.0.0",
72 | "c8": "10.1.3",
73 | "concurrently": "9.1.2",
74 | "husky": "9.1.7",
75 | "identity-obj-proxy": "3.0.0",
76 | "jest-fetch-mock": "3.0.3",
77 | "jsdom": "26.1.0",
78 | "msw": "2.8.7",
79 | "postcss": "8.5.4",
80 | "prisma": "6.8.2",
81 | "rustywind": "0.24.0",
82 | "sort-package-json": "3.2.1",
83 | "storybook": "8.6.14",
84 | "tailwindcss": "4.1.8",
85 | "typescript": "5.8.3",
86 | "vite": "6.3.5",
87 | "vitest": "3.2.0"
88 | },
89 | "msw": {
90 | "workerDirectory": "public"
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/pages/_app.tsx:
--------------------------------------------------------------------------------
1 | import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
2 | import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
3 | import { type Atom, Provider } from 'jotai';
4 | import { SessionProvider } from 'next-auth/react';
5 | import type { AppProps } from 'next/app';
6 |
7 | import logger from '../logger';
8 | import store from '../store';
9 | import '../styles/globals.css';
10 |
11 | if (process.env.NEXT_PUBLIC_API_MOCKING === 'yes') {
12 | if (typeof window === 'undefined') {
13 | import('../mocks/server')
14 | .then(({ server }) => {
15 | server.listen();
16 | })
17 | .catch(logger.error);
18 | } else {
19 | import('../mocks/browser')
20 | .then(async ({ browser }) => browser.start())
21 | .catch(logger.error);
22 | }
23 | }
24 |
25 | const queryClient = new QueryClient();
26 |
27 | function MyApp({ Component, pageProps }: AppProps) {
28 | const { initialState } = pageProps;
29 | return (
30 | , unknown]
36 | >)
37 | }
38 | >
39 |
43 |
44 |
45 |
46 |
47 |
48 |
49 | );
50 | }
51 |
52 | export default MyApp;
53 |
--------------------------------------------------------------------------------
/pages/api/auth/[...nextauth].ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable new-cap */
2 | import { PrismaAdapter } from '@next-auth/prisma-adapter';
3 | import * as Prisma from '@prisma/client';
4 | import NextAuth from 'next-auth';
5 | import EmailProvider from 'next-auth/providers/email';
6 | import GithubProvider from 'next-auth/providers/github';
7 | import GoogleProvider from 'next-auth/providers/google';
8 |
9 | const prisma = new Prisma.PrismaClient();
10 |
11 | const providers = [];
12 |
13 | // Add Github Provider if Github Client ID and Secret are set
14 | if (process.env.GITHUB_ID && process.env.GITHUB_SECRET) {
15 | providers.push(
16 | GithubProvider({
17 | clientId: process.env.GITHUB_ID,
18 | clientSecret: process.env.GITHUB_SECRET,
19 | }),
20 | );
21 | }
22 |
23 | // Add Google Provider if Google Client ID and Secret are set
24 | if (process.env.GOOGLE_CLIENT_ID && process.env.GOOGLE_CLIENT_SECRET) {
25 | providers.push(
26 | GoogleProvider({
27 | clientId: process.env.GOOGLE_CLIENT_ID,
28 | clientSecret: process.env.GOOGLE_CLIENT_SECRET,
29 | }),
30 | );
31 | }
32 |
33 | export default NextAuth({
34 | providers: [
35 | EmailProvider({
36 | server: process.env.EMAIL_SERVER,
37 | from: process.env.EMAIL_FROM,
38 | }),
39 | ...providers,
40 | ],
41 | adapter: PrismaAdapter(prisma),
42 | });
43 |
--------------------------------------------------------------------------------
/pages/api/hello.ts:
--------------------------------------------------------------------------------
1 | import { type NextApiRequest, type NextApiResponse } from 'next';
2 | import { withValidation } from 'next-validations';
3 | import { z } from 'zod';
4 |
5 | const schema = z.object({
6 | name: z.string().min(5),
7 | });
8 |
9 | const validate = withValidation({
10 | schema,
11 | mode: 'query',
12 | });
13 |
14 | const handler = (req: NextApiRequest, res: NextApiResponse) => {
15 | res.status(200).json(req.query);
16 | };
17 |
18 | export default validate(handler);
19 |
--------------------------------------------------------------------------------
/pages/contact.tsx:
--------------------------------------------------------------------------------
1 | import { DevTool } from '@hookform/devtools';
2 | import { zodResolver } from '@hookform/resolvers/zod';
3 | import { useForm } from 'react-hook-form';
4 | import { z } from 'zod';
5 |
6 | import Button from '../components/Button';
7 | import Counter from '../components/Counter';
8 | import Layout from '../components/Layout';
9 | import logger from '../logger';
10 |
11 | const schema = z.object({
12 | email: z.string().email(),
13 | message: z.string().min(10),
14 | });
15 |
16 | type FormValue = z.infer;
17 |
18 | export default function Contact() {
19 | const {
20 | register,
21 | handleSubmit,
22 | control,
23 | formState: { errors },
24 | } = useForm({
25 | resolver: zodResolver(schema),
26 | });
27 | const onSubmit = (data: FormValue) => {
28 | logger.log(data);
29 | };
30 |
31 | return (
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 | Feedback
40 |
41 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 | );
98 | }
99 |
--------------------------------------------------------------------------------
/pages/index.tsx:
--------------------------------------------------------------------------------
1 | import { type GetServerSideProps } from 'next';
2 | import { getSession } from 'next-auth/react';
3 | import Head from 'next/head';
4 | import Image from 'next/image';
5 |
6 | import Counter from '../components/Counter';
7 | import Layout from '../components/Layout';
8 | import styles from '../styles/Home.module.css';
9 |
10 | export default function Index() {
11 | return (
12 |
13 |
14 |
15 |
Next App Starter
16 |
17 |
18 |
19 |
20 |
26 |
27 |
28 | Get started by editing{' '}
29 | pages/index.tsx
30 |
31 |
32 |
33 |
144 |
145 |
146 |
173 |
174 |
175 | );
176 | }
177 |
178 | export const getServerSideProps: GetServerSideProps = async (ctx) => ({
179 | props: {
180 | session: await getSession(ctx),
181 | },
182 | });
183 |
--------------------------------------------------------------------------------
/pages/ssr.tsx:
--------------------------------------------------------------------------------
1 | import Index from './index';
2 |
3 | export default function ServerSideRendering() {
4 | return ;
5 | }
6 |
7 | export function getServerSideProps() {
8 | return { props: { initialState: 1000 } };
9 | }
10 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | '@tailwindcss/postcss': {},
4 | },
5 | };
6 |
--------------------------------------------------------------------------------
/prisma/migrations/20221223143416_add_next_auth_schema/migration.sql:
--------------------------------------------------------------------------------
1 | -- CreateTable
2 | CREATE TABLE "Account" (
3 | "id" TEXT NOT NULL,
4 | "userId" TEXT NOT NULL,
5 | "type" TEXT NOT NULL,
6 | "provider" TEXT NOT NULL,
7 | "providerAccountId" TEXT NOT NULL,
8 | "refresh_token" TEXT,
9 | "access_token" TEXT,
10 | "expires_at" INTEGER,
11 | "token_type" TEXT,
12 | "scope" TEXT,
13 | "id_token" TEXT,
14 | "session_state" TEXT,
15 |
16 | CONSTRAINT "Account_pkey" PRIMARY KEY ("id")
17 | );
18 |
19 | -- CreateTable
20 | CREATE TABLE "Session" (
21 | "id" TEXT NOT NULL,
22 | "sessionToken" TEXT NOT NULL,
23 | "userId" TEXT NOT NULL,
24 | "expires" TIMESTAMP(3) NOT NULL,
25 |
26 | CONSTRAINT "Session_pkey" PRIMARY KEY ("id")
27 | );
28 |
29 | -- CreateTable
30 | CREATE TABLE "User" (
31 | "id" TEXT NOT NULL,
32 | "name" TEXT,
33 | "email" TEXT,
34 | "emailVerified" TIMESTAMP(3),
35 | "image" TEXT,
36 |
37 | CONSTRAINT "User_pkey" PRIMARY KEY ("id")
38 | );
39 |
40 | -- CreateTable
41 | CREATE TABLE "VerificationToken" (
42 | "identifier" TEXT NOT NULL,
43 | "token" TEXT NOT NULL,
44 | "expires" TIMESTAMP(3) NOT NULL
45 | );
46 |
47 | -- CreateIndex
48 | CREATE UNIQUE INDEX "Account_provider_providerAccountId_key" ON "Account"("provider", "providerAccountId");
49 |
50 | -- CreateIndex
51 | CREATE UNIQUE INDEX "Session_sessionToken_key" ON "Session"("sessionToken");
52 |
53 | -- CreateIndex
54 | CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
55 |
56 | -- CreateIndex
57 | CREATE UNIQUE INDEX "VerificationToken_token_key" ON "VerificationToken"("token");
58 |
59 | -- CreateIndex
60 | CREATE UNIQUE INDEX "VerificationToken_identifier_token_key" ON "VerificationToken"("identifier", "token");
61 |
62 | -- AddForeignKey
63 | ALTER TABLE "Account" ADD CONSTRAINT "Account_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
64 |
65 | -- AddForeignKey
66 | ALTER TABLE "Session" ADD CONSTRAINT "Session_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
67 |
--------------------------------------------------------------------------------
/prisma/migrations/migration_lock.toml:
--------------------------------------------------------------------------------
1 | # Please do not edit this file manually
2 | # It should be added in your version-control system (i.e. Git)
3 | provider = "postgresql"
--------------------------------------------------------------------------------
/prisma/schema.prisma:
--------------------------------------------------------------------------------
1 | generator client {
2 | provider = "prisma-client-js"
3 | }
4 |
5 | datasource db {
6 | // Set the provider of the datasource block in schema.prisma to match your database: postgresql, mysql, sqlite, sqlserver, mongodb or cockroachdb.
7 | provider = "postgresql"
8 | // Set the DATABASE_URL in the .env file to point to your existing database. If your database has no tables yet, read https://pris.ly/d/getting-started
9 | url = env("DATABASE_URL")
10 | shadowDatabaseUrl = env("SHADOW_DATABASE_URL")
11 | }
12 |
13 | // datasource db {
14 | // provider = "postgresql"
15 | // url = env("DATABASE_URL")
16 | // shadowDatabaseUrl = env("SHADOW_DATABASE_URL") // Only needed when using a cloud provider that doesn't support the creation of new databases, like Heroku. Learn more: https://pris.ly/migrate-shadow
17 | // }
18 |
19 | model Account {
20 | id String @id @default(cuid())
21 | userId String
22 | type String
23 | provider String
24 | providerAccountId String
25 | refresh_token String? @db.Text
26 | access_token String? @db.Text
27 | expires_at Int?
28 | token_type String?
29 | scope String?
30 | id_token String? @db.Text
31 | session_state String?
32 |
33 | user User @relation(fields: [userId], references: [id], onDelete: Cascade)
34 |
35 | @@unique([provider, providerAccountId])
36 | }
37 |
38 | model Session {
39 | id String @id @default(cuid())
40 | sessionToken String @unique
41 | userId String
42 | expires DateTime
43 | user User @relation(fields: [userId], references: [id], onDelete: Cascade)
44 | }
45 |
46 | model User {
47 | id String @id @default(cuid())
48 | name String?
49 | email String? @unique
50 | emailVerified DateTime?
51 | image String?
52 | accounts Account[]
53 | sessions Session[]
54 | }
55 |
56 | model VerificationToken {
57 | identifier String
58 | token String @unique
59 | expires DateTime
60 |
61 | @@unique([identifier, token])
62 | }
63 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jellydn/next-app-starter/29f7aa2481aa658af5f54ddeee76d81e8c4a0ed0/public/favicon.ico
--------------------------------------------------------------------------------
/public/mockServiceWorker.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | /* tslint:disable */
3 |
4 | /**
5 | * Mock Service Worker.
6 | * @see https://github.com/mswjs/msw
7 | * - Please do NOT modify this file.
8 | * - Please do NOT serve this file on production.
9 | */
10 |
11 | const PACKAGE_VERSION = '2.7.3'
12 | const INTEGRITY_CHECKSUM = '00729d72e3b82faf54ca8b9621dbb96f'
13 | const IS_MOCKED_RESPONSE = Symbol('isMockedResponse')
14 | const activeClientIds = new Set()
15 |
16 | self.addEventListener('install', function () {
17 | self.skipWaiting()
18 | })
19 |
20 | self.addEventListener('activate', function (event) {
21 | event.waitUntil(self.clients.claim())
22 | })
23 |
24 | self.addEventListener('message', async function (event) {
25 | const clientId = event.source.id
26 |
27 | if (!clientId || !self.clients) {
28 | return
29 | }
30 |
31 | const client = await self.clients.get(clientId)
32 |
33 | if (!client) {
34 | return
35 | }
36 |
37 | const allClients = await self.clients.matchAll({
38 | type: 'window',
39 | })
40 |
41 | switch (event.data) {
42 | case 'KEEPALIVE_REQUEST': {
43 | sendToClient(client, {
44 | type: 'KEEPALIVE_RESPONSE',
45 | })
46 | break
47 | }
48 |
49 | case 'INTEGRITY_CHECK_REQUEST': {
50 | sendToClient(client, {
51 | type: 'INTEGRITY_CHECK_RESPONSE',
52 | payload: {
53 | packageVersion: PACKAGE_VERSION,
54 | checksum: INTEGRITY_CHECKSUM,
55 | },
56 | })
57 | break
58 | }
59 |
60 | case 'MOCK_ACTIVATE': {
61 | activeClientIds.add(clientId)
62 |
63 | sendToClient(client, {
64 | type: 'MOCKING_ENABLED',
65 | payload: {
66 | client: {
67 | id: client.id,
68 | frameType: client.frameType,
69 | },
70 | },
71 | })
72 | break
73 | }
74 |
75 | case 'MOCK_DEACTIVATE': {
76 | activeClientIds.delete(clientId)
77 | break
78 | }
79 |
80 | case 'CLIENT_CLOSED': {
81 | activeClientIds.delete(clientId)
82 |
83 | const remainingClients = allClients.filter((client) => {
84 | return client.id !== clientId
85 | })
86 |
87 | // Unregister itself when there are no more clients
88 | if (remainingClients.length === 0) {
89 | self.registration.unregister()
90 | }
91 |
92 | break
93 | }
94 | }
95 | })
96 |
97 | self.addEventListener('fetch', function (event) {
98 | const { request } = event
99 |
100 | // Bypass navigation requests.
101 | if (request.mode === 'navigate') {
102 | return
103 | }
104 |
105 | // Opening the DevTools triggers the "only-if-cached" request
106 | // that cannot be handled by the worker. Bypass such requests.
107 | if (request.cache === 'only-if-cached' && request.mode !== 'same-origin') {
108 | return
109 | }
110 |
111 | // Bypass all requests when there are no active clients.
112 | // Prevents the self-unregistered worked from handling requests
113 | // after it's been deleted (still remains active until the next reload).
114 | if (activeClientIds.size === 0) {
115 | return
116 | }
117 |
118 | // Generate unique request ID.
119 | const requestId = crypto.randomUUID()
120 | event.respondWith(handleRequest(event, requestId))
121 | })
122 |
123 | async function handleRequest(event, requestId) {
124 | const client = await resolveMainClient(event)
125 | const response = await getResponse(event, client, requestId)
126 |
127 | // Send back the response clone for the "response:*" life-cycle events.
128 | // Ensure MSW is active and ready to handle the message, otherwise
129 | // this message will pend indefinitely.
130 | if (client && activeClientIds.has(client.id)) {
131 | ;(async function () {
132 | const responseClone = response.clone()
133 |
134 | sendToClient(
135 | client,
136 | {
137 | type: 'RESPONSE',
138 | payload: {
139 | requestId,
140 | isMockedResponse: IS_MOCKED_RESPONSE in response,
141 | type: responseClone.type,
142 | status: responseClone.status,
143 | statusText: responseClone.statusText,
144 | body: responseClone.body,
145 | headers: Object.fromEntries(responseClone.headers.entries()),
146 | },
147 | },
148 | [responseClone.body],
149 | )
150 | })()
151 | }
152 |
153 | return response
154 | }
155 |
156 | // Resolve the main client for the given event.
157 | // Client that issues a request doesn't necessarily equal the client
158 | // that registered the worker. It's with the latter the worker should
159 | // communicate with during the response resolving phase.
160 | async function resolveMainClient(event) {
161 | const client = await self.clients.get(event.clientId)
162 |
163 | if (activeClientIds.has(event.clientId)) {
164 | return client
165 | }
166 |
167 | if (client?.frameType === 'top-level') {
168 | return client
169 | }
170 |
171 | const allClients = await self.clients.matchAll({
172 | type: 'window',
173 | })
174 |
175 | return allClients
176 | .filter((client) => {
177 | // Get only those clients that are currently visible.
178 | return client.visibilityState === 'visible'
179 | })
180 | .find((client) => {
181 | // Find the client ID that's recorded in the
182 | // set of clients that have registered the worker.
183 | return activeClientIds.has(client.id)
184 | })
185 | }
186 |
187 | async function getResponse(event, client, requestId) {
188 | const { request } = event
189 |
190 | // Clone the request because it might've been already used
191 | // (i.e. its body has been read and sent to the client).
192 | const requestClone = request.clone()
193 |
194 | function passthrough() {
195 | // Cast the request headers to a new Headers instance
196 | // so the headers can be manipulated with.
197 | const headers = new Headers(requestClone.headers)
198 |
199 | // Remove the "accept" header value that marked this request as passthrough.
200 | // This prevents request alteration and also keeps it compliant with the
201 | // user-defined CORS policies.
202 | const acceptHeader = headers.get('accept')
203 | if (acceptHeader) {
204 | const values = acceptHeader.split(',').map((value) => value.trim())
205 | const filteredValues = values.filter(
206 | (value) => value !== 'msw/passthrough',
207 | )
208 |
209 | if (filteredValues.length > 0) {
210 | headers.set('accept', filteredValues.join(', '))
211 | } else {
212 | headers.delete('accept')
213 | }
214 | }
215 |
216 | return fetch(requestClone, { headers })
217 | }
218 |
219 | // Bypass mocking when the client is not active.
220 | if (!client) {
221 | return passthrough()
222 | }
223 |
224 | // Bypass initial page load requests (i.e. static assets).
225 | // The absence of the immediate/parent client in the map of the active clients
226 | // means that MSW hasn't dispatched the "MOCK_ACTIVATE" event yet
227 | // and is not ready to handle requests.
228 | if (!activeClientIds.has(client.id)) {
229 | return passthrough()
230 | }
231 |
232 | // Notify the client that a request has been intercepted.
233 | const requestBuffer = await request.arrayBuffer()
234 | const clientMessage = await sendToClient(
235 | client,
236 | {
237 | type: 'REQUEST',
238 | payload: {
239 | id: requestId,
240 | url: request.url,
241 | mode: request.mode,
242 | method: request.method,
243 | headers: Object.fromEntries(request.headers.entries()),
244 | cache: request.cache,
245 | credentials: request.credentials,
246 | destination: request.destination,
247 | integrity: request.integrity,
248 | redirect: request.redirect,
249 | referrer: request.referrer,
250 | referrerPolicy: request.referrerPolicy,
251 | body: requestBuffer,
252 | keepalive: request.keepalive,
253 | },
254 | },
255 | [requestBuffer],
256 | )
257 |
258 | switch (clientMessage.type) {
259 | case 'MOCK_RESPONSE': {
260 | return respondWithMock(clientMessage.data)
261 | }
262 |
263 | case 'PASSTHROUGH': {
264 | return passthrough()
265 | }
266 | }
267 |
268 | return passthrough()
269 | }
270 |
271 | function sendToClient(client, message, transferrables = []) {
272 | return new Promise((resolve, reject) => {
273 | const channel = new MessageChannel()
274 |
275 | channel.port1.onmessage = (event) => {
276 | if (event.data && event.data.error) {
277 | return reject(event.data.error)
278 | }
279 |
280 | resolve(event.data)
281 | }
282 |
283 | client.postMessage(
284 | message,
285 | [channel.port2].concat(transferrables.filter(Boolean)),
286 | )
287 | })
288 | }
289 |
290 | async function respondWithMock(response) {
291 | // Setting response status code to 0 is a no-op.
292 | // However, when responding with a "Response.error()", the produced Response
293 | // instance will have status code set to 0. Since it's not possible to create
294 | // a Response instance with status code 0, handle that use-case separately.
295 | if (response.status === 0) {
296 | return Response.error()
297 | }
298 |
299 | const mockedResponse = new Response(response.body, response)
300 |
301 | Reflect.defineProperty(mockedResponse, IS_MOCKED_RESPONSE, {
302 | value: true,
303 | enumerable: true,
304 | })
305 |
306 | return mockedResponse
307 | }
308 |
--------------------------------------------------------------------------------
/renovate.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": [
3 | "config:base",
4 | "group:allNonMajor",
5 | ":pinAllExceptPeerDependencies"
6 | ],
7 | "lockFileMaintenance": {
8 | "enabled": true
9 | },
10 | "automerge": true,
11 | "major": {
12 | "automerge": false
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/sandbox.config.json:
--------------------------------------------------------------------------------
1 | {
2 | "template": "next"
3 | }
4 |
--------------------------------------------------------------------------------
/screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jellydn/next-app-starter/29f7aa2481aa658af5f54ddeee76d81e8c4a0ed0/screenshot.png
--------------------------------------------------------------------------------
/store/counter.ts:
--------------------------------------------------------------------------------
1 | import { atom } from 'jotai';
2 |
3 | const counterAtom = atom(0);
4 |
5 | export default counterAtom;
6 |
--------------------------------------------------------------------------------
/store/index.ts:
--------------------------------------------------------------------------------
1 | import counterAtom from './counter';
2 |
3 | export default {
4 | counterAtom,
5 | };
6 |
--------------------------------------------------------------------------------
/stories/Button.stories.tsx:
--------------------------------------------------------------------------------
1 | import { StoryFn } from '@storybook/react';
2 |
3 | import Button, { ButtonProps } from '../components/Button';
4 |
5 | export default {
6 | title: 'Button',
7 | argTypes: { onClick: { action: 'clicked' } },
8 | };
9 |
10 | const Template: StoryFn = (args) => (
11 |
12 | );
13 |
14 | export const Small = Template.bind({});
15 |
16 | Small.args = {
17 | size: 's',
18 | };
19 |
20 | export const Medium = Template.bind({});
21 |
22 | Medium.args = {
23 | size: 'm',
24 | };
25 |
26 | export const Large = Template.bind({});
27 |
28 | Large.args = {
29 | size: 'l',
30 | };
31 |
--------------------------------------------------------------------------------
/stories/Header.stories.tsx:
--------------------------------------------------------------------------------
1 | import { StoryFn } from '@storybook/react';
2 |
3 | import Header, { HeaderProps } from '../components/Header';
4 |
5 | export default {
6 | title: 'Header',
7 | argTypes: { onSignIn: { action: 'clicked' } },
8 | };
9 |
10 | const Template: StoryFn = (args) => ;
11 |
12 | export const ContactLink = Template.bind({});
13 |
14 | ContactLink.args = {
15 | links: [
16 | {
17 | title: 'Contact',
18 | url: '/contact',
19 | },
20 | ],
21 | };
22 |
--------------------------------------------------------------------------------
/styles/Home.module.css:
--------------------------------------------------------------------------------
1 | .container {
2 | min-height: 100vh;
3 | padding: 0 0.5rem;
4 | display: flex;
5 | flex-direction: column;
6 | justify-content: center;
7 | align-items: center;
8 | }
9 |
10 | .main {
11 | padding: 5rem 0;
12 | flex: 1;
13 | display: flex;
14 | flex-direction: column;
15 | justify-content: center;
16 | align-items: center;
17 | }
18 |
19 | .footer {
20 | width: 100%;
21 | height: 100px;
22 | border-top: 1px solid #eaeaea;
23 | display: flex;
24 | justify-content: center;
25 | align-items: center;
26 | }
27 |
28 | .footer img {
29 | margin-left: 0.5rem;
30 | }
31 |
32 | .footer a {
33 | display: flex;
34 | justify-content: center;
35 | align-items: center;
36 | }
37 |
38 | .title a {
39 | color: #0070f3;
40 | text-decoration: none;
41 | }
42 |
43 | .title a:hover,
44 | .title a:focus,
45 | .title a:active {
46 | text-decoration: underline;
47 | }
48 |
49 | .title {
50 | margin: 0;
51 | line-height: 1.15;
52 | font-size: 4rem;
53 | }
54 |
55 | .title,
56 | .description {
57 | text-align: center;
58 | }
59 |
60 | .description {
61 | line-height: 1.5;
62 | font-size: 1.5rem;
63 | }
64 |
65 | .code {
66 | background: #fafafa;
67 | border-radius: 5px;
68 | padding: 0.75rem;
69 | font-size: 1.1rem;
70 | font-family:
71 | Menlo,
72 | Monaco,
73 | Lucida Console,
74 | Liberation Mono,
75 | DejaVu Sans Mono,
76 | Bitstream Vera Sans Mono,
77 | Courier New,
78 | monospace;
79 | }
80 |
81 | .grid {
82 | display: flex;
83 | align-items: center;
84 | justify-content: center;
85 | flex-wrap: wrap;
86 | max-width: 800px;
87 | margin-top: 3rem;
88 | }
89 |
90 | .card {
91 | margin: 1rem;
92 | flex-basis: 45%;
93 | padding: 1.5rem;
94 | text-align: left;
95 | color: inherit;
96 | text-decoration: none;
97 | border: 1px solid #eaeaea;
98 | border-radius: 10px;
99 | transition: color 0.15s ease, border-color 0.15s ease;
100 | }
101 |
102 | .card:hover,
103 | .card:focus,
104 | .card:active {
105 | color: #0070f3;
106 | border-color: #0070f3;
107 | }
108 |
109 | .card h3 {
110 | margin: 0 0 1rem 0;
111 | font-size: 1.5rem;
112 | }
113 |
114 | .card p {
115 | margin: 0;
116 | font-size: 1.25rem;
117 | line-height: 1.5;
118 | }
119 |
120 | .logo {
121 | height: 1em;
122 | }
123 |
124 | @media (max-width: 600px) {
125 | .grid {
126 | width: 100%;
127 | flex-direction: column;
128 | }
129 | }
130 |
--------------------------------------------------------------------------------
/styles/globals.css:
--------------------------------------------------------------------------------
1 | @import "tailwindcss";
2 |
3 | @plugin 'tailwindcss-animate';
4 |
5 | @custom-variant dark (&:is(.dark *));
6 |
7 | @theme {
8 | --color-border: hsl(var(--border));
9 | --color-input: hsl(var(--input));
10 | --color-ring: hsl(var(--ring));
11 | --color-background: hsl(var(--background));
12 | --color-foreground: hsl(var(--foreground));
13 |
14 | --color-primary: hsl(var(--primary));
15 | --color-primary-foreground: hsl(var(--primary-foreground));
16 |
17 | --color-secondary: hsl(var(--secondary));
18 | --color-secondary-foreground: hsl(var(--secondary-foreground));
19 |
20 | --color-destructive: hsl(var(--destructive));
21 | --color-destructive-foreground: hsl(var(--destructive-foreground));
22 |
23 | --color-muted: hsl(var(--muted));
24 | --color-muted-foreground: hsl(var(--muted-foreground));
25 |
26 | --color-accent: hsl(var(--accent));
27 | --color-accent-foreground: hsl(var(--accent-foreground));
28 |
29 | --color-popover: hsl(var(--popover));
30 | --color-popover-foreground: hsl(var(--popover-foreground));
31 |
32 | --color-card: hsl(var(--card));
33 | --color-card-foreground: hsl(var(--card-foreground));
34 |
35 | --radius-lg: var(--radius);
36 | --radius-md: calc(var(--radius) - 2px);
37 | --radius-sm: calc(var(--radius) - 4px);
38 |
39 | --animate-accordion-down: accordion-down 0.2s ease-out;
40 | --animate-accordion-up: accordion-up 0.2s ease-out;
41 |
42 | @keyframes accordion-down {
43 | from {
44 | height: 0;
45 | }
46 | to {
47 | height: var(--radix-accordion-content-height);
48 | }
49 | }
50 | @keyframes accordion-up {
51 | from {
52 | height: var(--radix-accordion-content-height);
53 | }
54 | to {
55 | height: 0;
56 | }
57 | }
58 | }
59 |
60 | @utility container {
61 | margin-inline: auto;
62 | padding-inline: 2rem;
63 | @media (width >= --theme(--breakpoint-sm)) {
64 | max-width: none;
65 | }
66 | @media (width >= 1400px) {
67 | max-width: 1400px;
68 | }
69 | }
70 |
71 | /*
72 | The default border color has changed to `currentColor` in Tailwind CSS v4,
73 | so we've added these compatibility styles to make sure everything still
74 | looks the same as it did with Tailwind CSS v3.
75 |
76 | If we ever want to remove these styles, we need to add an explicit border
77 | color utility to any element that depends on these defaults.
78 | */
79 | @layer base {
80 | *,
81 | ::after,
82 | ::before,
83 | ::backdrop,
84 | ::file-selector-button {
85 | border-color: var(--color-gray-200, currentColor);
86 | }
87 | }
88 |
89 | @layer base {
90 | :root {
91 | --background: 0 0% 100%;
92 | --foreground: 0 0% 3.9%;
93 |
94 | --muted: 0 0% 96.1%;
95 | --muted-foreground: 0 0% 45.1%;
96 |
97 | --popover: 0 0% 100%;
98 | --popover-foreground: 0 0% 3.9%;
99 |
100 | --card: 0 0% 100%;
101 | --card-foreground: 0 0% 3.9%;
102 |
103 | --border: 0 0% 89.8%;
104 | --input: 0 0% 89.8%;
105 |
106 | --primary: 0 0% 9%;
107 | --primary-foreground: 0 0% 98%;
108 |
109 | --secondary: 0 0% 96.1%;
110 | --secondary-foreground: 0 0% 9%;
111 |
112 | --accent: 0 0% 96.1%;
113 | --accent-foreground: 0 0% 9%;
114 |
115 | --destructive: 0 84.2% 60.2%;
116 | --destructive-foreground: 0 0% 98%;
117 |
118 | --ring: 0 0% 63.9%;
119 |
120 | --radius: 0.5rem;
121 | }
122 |
123 | .dark {
124 | --background: 0 0% 3.9%;
125 | --foreground: 0 0% 98%;
126 |
127 | --muted: 0 0% 14.9%;
128 | --muted-foreground: 0 0% 63.9%;
129 |
130 | --popover: 0 0% 3.9%;
131 | --popover-foreground: 0 0% 98%;
132 |
133 | --card: 0 0% 3.9%;
134 | --card-foreground: 0 0% 98%;
135 |
136 | --border: 0 0% 14.9%;
137 | --input: 0 0% 14.9%;
138 |
139 | --primary: 0 0% 98%;
140 | --primary-foreground: 0 0% 9%;
141 |
142 | --secondary: 0 0% 14.9%;
143 | --secondary-foreground: 0 0% 98%;
144 |
145 | --accent: 0 0% 14.9%;
146 | --accent-foreground: 0 0% 98%;
147 |
148 | --destructive: 0 62.8% 30.6%;
149 | --destructive-foreground: 0 85.7% 97.3%;
150 |
151 | --ring: 0 0% 14.9%;
152 | }
153 | }
154 |
155 | @layer base {
156 | * {
157 | @apply border-border;
158 | }
159 | body {
160 | @apply bg-background text-foreground;
161 | }
162 | }
163 |
--------------------------------------------------------------------------------
/tests/Button.test.tsx:
--------------------------------------------------------------------------------
1 | import { render } from '@testing-library/react';
2 | import { describe, test } from 'vitest';
3 |
4 | import Button from '../components/Button';
5 |
6 | /**
7 | * @jest-environment jsdom
8 | */
9 | describe('Button component', () => {
10 | test('should render button', () => {
11 | render();
12 | });
13 | });
14 |
--------------------------------------------------------------------------------
/tests/Header.test.tsx:
--------------------------------------------------------------------------------
1 | import { render } from '@testing-library/react';
2 | import { describe, test, vi } from 'vitest';
3 |
4 | import { SessionProvider } from 'next-auth/react';
5 | import Header from '../components/Header';
6 |
7 | global.fetch = vi.fn();
8 |
9 | /**
10 | * @jest-environment jsdom
11 | */
12 | describe('Header component', () => {
13 | test('should render header', () => {
14 | render(
15 |
16 |
17 | ,
18 | );
19 | });
20 | });
21 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "strict": true,
8 | "forceConsistentCasingInFileNames": true,
9 | "noEmit": true,
10 | "esModuleInterop": true,
11 | "module": "esnext",
12 | "moduleResolution": "node",
13 | "resolveJsonModule": true,
14 | "isolatedModules": true,
15 | "jsx": "preserve",
16 | "incremental": true,
17 | "plugins": [
18 | {
19 | "name": "next"
20 | }
21 | ]
22 | },
23 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
24 | "exclude": ["node_modules"]
25 | }
26 |
--------------------------------------------------------------------------------
/types/next-auth.d.ts:
--------------------------------------------------------------------------------
1 | import NextAuth from 'next-auth';
2 |
3 | declare module 'next-auth' {
4 | /**
5 | * Returned by `useSession`, `getSession` and received as a prop on the `Provider` React Context
6 | */
7 | interface Session {
8 | user: {
9 | /** The user's postal address. */
10 | address: string;
11 | image: string;
12 | email: string;
13 | name: string;
14 | };
15 | }
16 | /**
17 | * The shape of the user object returned in the OAuth providers' `profile` callback,
18 | * or the second parameter of the `session` callback, when using a database.
19 | */
20 | interface User {
21 | name: string;
22 | email: string;
23 | }
24 | /**
25 | * Usually contains information about the provider being used
26 | * and also extends `TokenSet`, which is different tokens returned by OAuth Providers.
27 | */
28 | interface Account {
29 | accessToken: string;
30 | refreshToken: string;
31 | }
32 |
33 | declare module 'next-auth/jwt' {
34 | /** Returned by the `jwt` callback and `getToken`, when using JWT sessions */
35 | interface JWT {
36 | /** OpenID ID Token */
37 | idToken?: string;
38 | }
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/vitest.config.ts:
--------------------------------------------------------------------------------
1 | ///
2 | import react from '@vitejs/plugin-react';
3 | import { defineConfig } from 'vitest/config';
4 |
5 | // @ts-ignore
6 | // https://vitejs.dev/config/
7 | export default defineConfig({
8 | // @ts-ignore
9 | plugins: [react()],
10 | test: {
11 | globals: true,
12 | environment: 'jsdom',
13 | },
14 | });
15 |
--------------------------------------------------------------------------------