├── .cursor
├── environment.json
└── mcp.json
├── .env.vault
├── .eslintignore
├── .eslintrc.json
├── .github
└── dependabot.yml
├── .gitignore
├── .husky
├── pre-commit
└── prepare-commit-msg
├── .prettierrc
├── .release-it.json
├── .windsurfrules
├── LICENSE
├── README.md
├── components.json
├── emails
├── thanks.tsx
└── verification.tsx
├── example.env
├── lefthook.yml
├── next.config.mjs
├── package.json
├── pnpm-lock.yaml
├── pnpm-workspace.yaml
├── postcss.config.js
├── prisma
└── schema.prisma
├── public
├── android-chrome-192x192.png
├── android-chrome-512x512.png
├── apple-touch-icon.png
├── chad-next.png
├── chadnext-homepage.png
├── favicon-16x16.png
└── favicon.ico
├── src
├── app
│ ├── [locale]
│ │ ├── (auth)
│ │ │ └── login
│ │ │ │ └── page.tsx
│ │ ├── (marketing)
│ │ │ ├── about
│ │ │ │ └── page.tsx
│ │ │ └── changelog
│ │ │ │ └── page.tsx
│ │ ├── @loginDialog
│ │ │ ├── (.)login
│ │ │ │ └── page.tsx
│ │ │ └── default.tsx
│ │ ├── actions.ts
│ │ ├── dashboard
│ │ │ ├── billing
│ │ │ │ ├── loading.tsx
│ │ │ │ └── page.tsx
│ │ │ ├── error.tsx
│ │ │ ├── layout.tsx
│ │ │ ├── projects
│ │ │ │ ├── [projectId]
│ │ │ │ │ ├── delete-card.tsx
│ │ │ │ │ ├── editable-details.tsx
│ │ │ │ │ ├── layout.tsx
│ │ │ │ │ ├── page.tsx
│ │ │ │ │ └── tab-sections.tsx
│ │ │ │ ├── action.ts
│ │ │ │ ├── create-project-modal.tsx
│ │ │ │ ├── loading.tsx
│ │ │ │ └── page.tsx
│ │ │ └── settings
│ │ │ │ ├── actions.ts
│ │ │ │ ├── error.tsx
│ │ │ │ ├── loading.tsx
│ │ │ │ ├── page.tsx
│ │ │ │ └── settings-form.tsx
│ │ ├── layout.tsx
│ │ ├── not-found.tsx
│ │ └── page.tsx
│ ├── api
│ │ ├── auth
│ │ │ └── login
│ │ │ │ ├── github
│ │ │ │ ├── callback
│ │ │ │ │ └── route.ts
│ │ │ │ └── route.ts
│ │ │ │ ├── send-otp
│ │ │ │ └── route.ts
│ │ │ │ └── verify-otp
│ │ │ │ └── route.ts
│ │ ├── og
│ │ │ └── route.ts
│ │ ├── stripe
│ │ │ └── route.ts
│ │ ├── uploadthing
│ │ │ ├── core.ts
│ │ │ └── route.ts
│ │ └── webhooks
│ │ │ └── stripe
│ │ │ └── route.ts
│ ├── global-error.tsx
│ ├── globals.css
│ ├── manifest.json
│ ├── robots.ts
│ ├── sitemap.ts
│ └── sw.ts
├── assets
│ └── fonts
│ │ ├── CalSans-SemiBold.ttf
│ │ ├── CalSans-SemiBold.woff
│ │ └── CalSans-SemiBold.woff2
├── components
│ ├── OGImgEl.tsx
│ ├── billing-form.tsx
│ ├── copy-button.tsx
│ ├── go-back.tsx
│ ├── layout
│ │ ├── auth-form.tsx
│ │ ├── cancel-confirm-modal.tsx
│ │ ├── footer.tsx
│ │ ├── header
│ │ │ ├── index.tsx
│ │ │ └── navbar.tsx
│ │ ├── image-upload-modal.tsx
│ │ ├── login-modal.tsx
│ │ └── sidebar-nav.tsx
│ ├── sections
│ │ ├── cta.tsx
│ │ ├── faq.tsx
│ │ ├── features.tsx
│ │ ├── hero.tsx
│ │ ├── open-source.tsx
│ │ ├── pricing.tsx
│ │ └── testimonials.tsx
│ ├── shared
│ │ ├── brand-icons.tsx
│ │ ├── icons.tsx
│ │ ├── locale-toggler.tsx
│ │ ├── logout-button.tsx
│ │ ├── theme-provider.tsx
│ │ └── theme-toggle.tsx
│ └── ui
│ │ ├── accordion.tsx
│ │ ├── alert-dialog.tsx
│ │ ├── alert.tsx
│ │ ├── aspect-ratio.tsx
│ │ ├── avatar.tsx
│ │ ├── badge.tsx
│ │ ├── breadcrumb.tsx
│ │ ├── button.tsx
│ │ ├── calendar.tsx
│ │ ├── card.tsx
│ │ ├── carousel.tsx
│ │ ├── chart.tsx
│ │ ├── checkbox.tsx
│ │ ├── collapsible.tsx
│ │ ├── command.tsx
│ │ ├── context-menu.tsx
│ │ ├── dialog.tsx
│ │ ├── drawer.tsx
│ │ ├── dropdown-menu.tsx
│ │ ├── form.tsx
│ │ ├── hover-card.tsx
│ │ ├── input-otp.tsx
│ │ ├── input.tsx
│ │ ├── label.tsx
│ │ ├── menubar.tsx
│ │ ├── navigation-menu.tsx
│ │ ├── pagination.tsx
│ │ ├── popover.tsx
│ │ ├── progress.tsx
│ │ ├── radio-group.tsx
│ │ ├── resizable.tsx
│ │ ├── scroll-area.tsx
│ │ ├── select.tsx
│ │ ├── separator.tsx
│ │ ├── sheet.tsx
│ │ ├── sidebar.tsx
│ │ ├── skeleton.tsx
│ │ ├── slider.tsx
│ │ ├── sonner.tsx
│ │ ├── switch.tsx
│ │ ├── table.tsx
│ │ ├── tabs.tsx
│ │ ├── textarea.tsx
│ │ ├── toast.tsx
│ │ ├── toaster.tsx
│ │ ├── toggle-group.tsx
│ │ ├── toggle.tsx
│ │ └── tooltip.tsx
├── config
│ ├── site.ts
│ └── subscription.ts
├── content
│ ├── about
│ │ ├── 1tech-stack.md
│ │ └── 2inspiration.md
│ └── changelog
│ │ ├── change-01.md
│ │ ├── change-02.md
│ │ ├── change-03.md
│ │ ├── change-04.md
│ │ ├── change-05.md
│ │ ├── change-06.md
│ │ └── change-07.md
├── hooks
│ ├── use-mobile.tsx
│ ├── use-scroll.ts
│ └── use-toast.ts
├── lib
│ ├── client
│ │ ├── safe-action.ts
│ │ └── uploadthing.ts
│ ├── server
│ │ ├── auth
│ │ │ ├── cookies.ts
│ │ │ ├── github.ts
│ │ │ ├── index.ts
│ │ │ └── session.ts
│ │ ├── db.ts
│ │ ├── mail.ts
│ │ ├── payment.ts
│ │ └── upload.ts
│ └── utils.ts
├── locales
│ ├── client.ts
│ ├── en.ts
│ ├── fr.ts
│ └── server.ts
├── middleware.ts
└── types
│ └── index.ts
├── tailwind.config.js
├── tsconfig.json
└── velite.config.ts
/.cursor/environment.json:
--------------------------------------------------------------------------------
1 | {
2 | "snapshot": "snapshot-20250519-348489b0-1643-41ed-a75d-af930cb71c75",
3 | "install": "pnpm i",
4 | "start": "pnpm dev",
5 | "terminals": []
6 | }
--------------------------------------------------------------------------------
/.cursor/mcp.json:
--------------------------------------------------------------------------------
1 | {
2 | "mcpServers": {
3 | "postgres": {
4 | "command": "npx",
5 | "args": [
6 | "-y",
7 | "@modelcontextprotocol/server-postgres",
8 | "postgres://postgres:postgres@localhost:5432/localdb"
9 | ]
10 | }
11 | }
12 | }
--------------------------------------------------------------------------------
/.env.vault:
--------------------------------------------------------------------------------
1 | #/-------------------.env.vault---------------------/
2 | #/ cloud-agnostic vaulting standard /
3 | #/ [how it works](https://dotenv.org/env-vault) /
4 | #/--------------------------------------------------/
5 |
6 | # production
7 | DOTENV_VAULT_PRODUCTION="wOnZOQxHcggPZLXlhbGJjLiJoCkyrqVn2eyFfdA2h6owapLexWX+HXV6NR3nMroU9RSG0OoqG7TF2OGidvimIMaHqrbq5f5vGoAyPgdpEs0TlmDnAQ1EdsONezWsAcrdwuBpXESbjWXGqilCBmQvwXtmNjXpuBZnKTMyBIZaVEn7TaX4AXdcbMzXuW84z85a21IAPKazBXJ8umQgJSin718Sa0AkRZ+m3q4cxORkieHLuyi9zvoFo5CdR8VeoiUE/iyAiI9vARQtLXULsdlx9nDX1WHXLlmq4ipYKM3WS5o3Y0B+5M1OjiyQE9gQhkVEDXT3zEjYJacAQwQEpyQWdwvJlBrT2Ds2eU5pkUH+7o972J4CnsHlZ5pErzIrK4My3sgGwOo2V0iZOPrhpkGuvL+ANW6m4IHs7ZJphQiM4Xy8/8zjVwhiOxPOafhF278U88oOUC3X1pQ9KKLPfJ1qG9Z/cOqFQeNvSA4UeqbVoKv0nXRlKr6QF7iNkwGZJ/aQYGhDBd7oanRVla/w8wjFCYpG+n62iW3WIm1bQePTjOyefuix5Pufs5Pz9OsO0nuTDmVIQVSl1jHjcpjFqa+IKZ6EcZDDDVf+9phRlBjQJ1KOurBKS8rieLBGnMHUU8RRkgVno6PPIsCMANwDIIoEWAZOXRP15pKQtrdveuqB5o8GTM+2jfprmDvi3nwpb0beCsludkI+M8GNap14ASD8KVPg1/XK4UvyXT25stIKru+XxUgOV4PPGWrgI2j6mDmBYkdJqK4yByRbNeQypyLZtCyi4fUZJCePsMOAQr7rYGnNzs4JaDc8uUdDzwCBZpZeFqJFCJlXxrIIdQ/SOxDb0myGxaJPpZJm78rLcYqzTgwr9/HRIOIgRBuLFjfBeWAXYuKYVtgfsAyO206vSsoHLIPpnnhzfO/9VpYGRrHnvtLVKn0PvFEynwDhqZJK38Y3KZKyKSR6LQrzE0kbG7J1dggFR/1vM2V783EsTmCISHRH9CVrURUXcrC7beQzeFi3E82hSBg0sqx/Fw+8m4SnMLUUnDI5SZDAwyJCOSGSgQLtnnn8/uN1VHHuHuA6a3G39yqMHp7DBzSiuntEV4Id0rA9WmC2c5nsqDIO6FA5/6N2NR5FClOIjkv6ErbuRsJ1WTsyRBzQYr1dFjRXK3xuXUORJS2fZ4pAL+67QS3THfPkKP95ckbzzyVWVPVmSBfncvLW0pY+fdnPVqocYb9z3aRX/+bkQOy55f8xaO4ND+zxQQ6TuTVkUDyo4QgT7mig7h2rKFwOmFNHPWr9DvXSyTrzTcZLSBKw4MHNlX1kZbVjMF2XUnwZ+7dhDb3+otQIipYnARmZu35nKhWjFoUAJw7T2PAW4q390ba6zH0u6sdLOdz6tin2STRzl0iqrBgo4b/pNDUj+0F8Hec+MUDN4l16A4bcDjGgL35ck4nwmDN5tyJjHq4eX2SX7MmQSIBZK3r7stdbDzCjtUjOjl8ONVd/Na8p1whT8qLm0r6kGPZ5qk8QAPV/uDuEUGxaDVd6792tNGq0ZqdOhu9th4v70X2LE6ZisJj345Sbu44pMC+gF4uF3m+fg+lhiMctX+RwYOFIvkH8SBvNlaNUZAX2+ZCkne0NdapP/uNsJJUPxnbHZMEXV98pmelBb+Q7RyJNEVtKp/j9/iEyhg352FNDXOygoki7QSdpJQ5dOxds0dWyZ3Xch2THUto+0UgSfIPK9JMdW5+0Bgma2zy/nHIZZ9i1h8y9Sp2wqDowrL8NN8mHokMRZ5jCTmGKCfTiHN8wS3e7guD0cTjQWedkQRb9ReH58t8LJfa8147zj92FPxwFY2kQd1FXQ9ihcEprWx62Hlnf3XF4Fii+zq10ewpm"
8 | DOTENV_VAULT_PRODUCTION_VERSION=5
9 |
10 | # development
11 | DOTENV_VAULT_DEVELOPMENT="hfrbD3KKbNDOxfc776GTjEIqlFjVLVO2Py74yoe4jk4E6OFYFE7+BgX/ckRs+yqzuOGsyrX7kS9e1Y7G43iX7UbSjfhw0FfIbUNcB454B2s4aAWf6cak8d3D0n6HwZz14/8hkWjfSEPn3IsopDy+2Xdx7nFBOv3fLU/NfY4T3xDf9F0jsIHVxYaqFbcMzZEOmnMtawgt/7JFel5YV2nCiOEXp+qB0P5/P+ie3f7irjZCOAlIdgyY6GB4O22IFfHqZ2hmXLy9zzvGlfVDhYSJs5ajYMMMS7qhmh4bU7FssMWTcb6l6YDoTUBTCIMVF0wl8+dGKgs07W7r+EgEBqrqDxRLIPp/L4aSHPvAZctuzhG+eNSEaYtreCZ+qYtvpTX/MgNzZ4sMkbweM81m5N+yuj31kDRd4R6CQDvoKGvVhmsPvKQBZdD4c4k2ObEWBxOG7IoEqidD0NOZQX5GmkYtcCQtgQ3Ssg3M1GibYIcVA8OtbiLb5fvHKy51ZCPvd9GGsUibO3Y+4BOnkORDWRmu+KgHLhR8n4YPyRKMlbZOYKp/9DMmbEHAUFPKhMueXqtWLBnrp+jhA0zbwCovjbZdRRxXHSah+cPn0HjqUp7Ly4hINnnPEhGkXZiakOjlOa5Qjzlu12yATHqKFnmvBRxfMOyc0KyLCx39yeOM/wNva9TZZw05K0bNSVsF9L09aWfCLZboIn8ZvR+97xomE8smJLINqks7lK8pbTmcMuA4BoMzkHvhntaVFNFlV7iu592EwF49BMNLE9ISerfp64kPjM16QTJtqwHAGw9ALYDqBxjOpWMtEpqM9caAKP/Thqph6gS/2rBlWVkyR0FeEUJe9AQ2Vu+vCjuKgAegO5jWt4sSe7nkZXU6Llg6lYw7Y+nw3ChaV55T5ywfo1X/r0A3aOucB/itGOLfRrMgPQdpDouk8YUjQv8vE3OsHJy9P98QwEk9aIf8FBIPueg5lV5VCFPzScDpa+UCYXQ8FVakPlszQJli/hM5/wbC2mH2AXeiE5d9fcnUIQaOf1exfIp+duWNbK42cv9PkH9mqeCoYAiw3t7fVlPedmwogzQuO7ttOCXONhpjM4vCjFgrZzMz3B9wHEbz4GESnaOvCThTchuaGjg+Dc31Rt3l8v8KiF2i5ZDXP8ExFPuvCcbZW0zszcpWZp838BDOVRFOZAsdYlF0ylrHLzETyFbzttlr6f8gdoDvoQYLLyoFPuhXT/UFBmOibCh1qMGbL/26y2qachliSqXCdm4Osyi4uo7tCS5NIGSYtZ1ouSq2RtmWFftFfTSbaEXvcbuiFMwmmjbsqHtW1nf/AJ5eprq29eMUL6gv7lvOPe1alx5GhSAzwrfC1kHStTE9lbuYLo2kq2dipsOPbno3FO5r42/r6aaMVQ4Dl0KCiezG/SGj3gcB6Qaqpmate3s+b2hq5HahV/xc+AEN40AU9JCQdG72DJFFNYUcZo7NS9v4Tgw2v0BEOeDK6qs82RjetyTOGCIWqhIYIWYFFSyDJNMLSANocnCHSiG2JLpC7n5HOyzoQddvQuFppjzushde3MftOgBP27WqlYiDtQLby+HCPkDJ5jJa4IGNgX5kg7ETN0sWmpTiIAM8zpBUyCaeRMhVinJK4MpmtFQRY4/Z+B8tqCPksDtr6gEcks/BOzg9eyd82TpoudIMTqbnS/RwV0/BVYVxySrNkp/f9u8OJw08MI/6SwPCR7zeJYH8zy439Jcpcs5AaBtWwWB20xLGFmznm5FeQUST1ZhBUYD676xiZH47WciMtkbI2+tTvlbhW+r2Xu00r5ETna0Q3dTGOfevIbAIkFU+uRa6bzpWkXznLACfsi3/hY1ulkfNt5Qlj+mTkRyXmUvS2VCeqrQVC3G5xX6rl647nbtxbIzi0EFLa7CIatIRgoj1T1gpa4myjBJPIANORNf1B7DFfEmFl814HRUxMnMQEkpgcJymo39kbh285hTSeeXGlre8YvSqjRKN1dvJl7CS5wPf71Zm3QplmFDgicdfvR5H9rCdiQg7DSptJ1n8KLBuRPaPrMuNTWFRfoQfKuQ2w8te+cYsCQ=="
12 | DOTENV_VAULT_DEVELOPMENT_VERSION=7
13 |
14 | #/----------------settings/metadata-----------------/
15 | DOTENV_VAULT="vlt_e36ffc3f8b8964da1fad7595c7cf6d75623620036e796b8ca0b3b755c6582431"
16 | DOTENV_API_URL="https://vault.dotenv.org"
17 | DOTENV_CLI="npx dotenv-vault@latest"
18 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | ui/
2 | *.d.ts
3 | *.config.*
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": [
3 | "next/core-web-vitals",
4 | "prettier"
5 | ]
6 | }
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | # To get started with Dependabot version updates, you'll need to specify which
2 | # package ecosystems to update and where the package manifests are located.
3 | # Please see the documentation for all configuration options:
4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
5 |
6 | version: 2
7 | updates:
8 | - package-ecosystem: "npm" # See documentation for possible values
9 | directory: "/" # Location of package manifests
10 | schedule:
11 | interval: "weekly"
12 |
--------------------------------------------------------------------------------
/.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 |
31 | # vercel
32 | .vercel
33 |
34 | # typescript
35 | *.tsbuildinfo
36 | next-env.d.ts
37 |
38 | # custom workspace settings
39 | .vscode
40 | .idea
41 |
42 | # worker
43 | sw.js
44 | workbox-*.*
45 | .react-email
46 | .env*
47 | .flaskenv*
48 | !.env.project
49 | !.env.vault
50 |
51 | .velite
52 |
53 | .cursorrules
54 | .envrc
--------------------------------------------------------------------------------
/.husky/pre-commit:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | if [ "$LEFTHOOK" = "0" ]; then
4 | exit 0
5 | fi
6 |
7 | call_lefthook()
8 | {
9 | dir="$(git rev-parse --show-toplevel)"
10 | osArch=$(uname | tr '[:upper:]' '[:lower:]')
11 | cpuArch=$(uname -m | sed 's/aarch64/arm64/')
12 |
13 | if lefthook -h >/dev/null 2>&1
14 | then
15 | lefthook "$@"
16 | elif test -f "$dir/node_modules/lefthook/bin/index.js"
17 | then
18 | "$dir/node_modules/lefthook/bin/index.js" "$@"
19 | elif test -f "$dir/node_modules/@evilmartians/lefthook/bin/lefthook_${osArch}_${cpuArch}/lefthook"
20 | then
21 | "$dir/node_modules/@evilmartians/lefthook/bin/lefthook_${osArch}_${cpuArch}/lefthook" "$@"
22 | elif test -f "$dir/node_modules/@evilmartians/lefthook-installer/bin/lefthook_${osArch}_${cpuArch}/lefthook"
23 | then
24 | "$dir/node_modules/@evilmartians/lefthook-installer/bin/lefthook_${osArch}_${cpuArch}/lefthook" "$@"
25 | elif bundle exec lefthook -h >/dev/null 2>&1
26 | then
27 | bundle exec lefthook "$@"
28 | elif yarn lefthook -h >/dev/null 2>&1
29 | then
30 | yarn lefthook "$@"
31 | elif pnpm lefthook -h >/dev/null 2>&1
32 | then
33 | pnpm lefthook "$@"
34 | elif command -v npx >/dev/null 2>&1
35 | then
36 | npx @evilmartians/lefthook "$@"
37 | else
38 | echo "Can't find lefthook in PATH"
39 | fi
40 | }
41 |
42 | call_lefthook run "pre-commit" "$@"
43 |
--------------------------------------------------------------------------------
/.husky/prepare-commit-msg:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | if [ "$LEFTHOOK" = "0" ]; then
4 | exit 0
5 | fi
6 |
7 | call_lefthook()
8 | {
9 | dir="$(git rev-parse --show-toplevel)"
10 | osArch=$(uname | tr '[:upper:]' '[:lower:]')
11 | cpuArch=$(uname -m | sed 's/aarch64/arm64/')
12 |
13 | if lefthook -h >/dev/null 2>&1
14 | then
15 | lefthook "$@"
16 | elif test -f "$dir/node_modules/lefthook/bin/index.js"
17 | then
18 | "$dir/node_modules/lefthook/bin/index.js" "$@"
19 | elif test -f "$dir/node_modules/@evilmartians/lefthook/bin/lefthook_${osArch}_${cpuArch}/lefthook"
20 | then
21 | "$dir/node_modules/@evilmartians/lefthook/bin/lefthook_${osArch}_${cpuArch}/lefthook" "$@"
22 | elif test -f "$dir/node_modules/@evilmartians/lefthook-installer/bin/lefthook_${osArch}_${cpuArch}/lefthook"
23 | then
24 | "$dir/node_modules/@evilmartians/lefthook-installer/bin/lefthook_${osArch}_${cpuArch}/lefthook" "$@"
25 | elif bundle exec lefthook -h >/dev/null 2>&1
26 | then
27 | bundle exec lefthook "$@"
28 | elif yarn lefthook -h >/dev/null 2>&1
29 | then
30 | yarn lefthook "$@"
31 | elif pnpm lefthook -h >/dev/null 2>&1
32 | then
33 | pnpm lefthook "$@"
34 | elif command -v npx >/dev/null 2>&1
35 | then
36 | npx @evilmartians/lefthook "$@"
37 | else
38 | echo "Can't find lefthook in PATH"
39 | fi
40 | }
41 |
42 | call_lefthook run "prepare-commit-msg" "$@"
43 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "semi": true,
3 | "trailingComma": "es5",
4 | "singleQuote": false,
5 | "printWidth": 80,
6 | "tabWidth": 2,
7 | "plugins": ["prettier-plugin-tailwindcss"]
8 | }
9 |
--------------------------------------------------------------------------------
/.release-it.json:
--------------------------------------------------------------------------------
1 | {
2 | "plugins": {
3 | "release-it-pnpm": {}
4 | },
5 | "git": {
6 | "commitMessage": "chore: release v${version}",
7 | "tagName": "v${version}",
8 | "push": true,
9 | "requireCleanWorkingDir": true
10 | },
11 | "github": {
12 | "release": true,
13 | "tokenRef": "GITHUB_TOKEN",
14 | "releaseNotes": "git log --pretty=format:'* %s (%h)' ${latestTag}...${tagName}"
15 | },
16 | "npm": {
17 | "publish": false
18 | }
19 | }
--------------------------------------------------------------------------------
/.windsurfrules:
--------------------------------------------------------------------------------
1 | You are an expert in TypeScript, Node.js, Next.js App Router, React, Shadcn UI, Radix UI, and Tailwind.
2 |
3 | Code Style and Structure
4 | - Write concise, technical TypeScript code with accurate examples.
5 | - Use functional and declarative programming patterns; avoid classes.
6 | - Prefer iteration and modularization over code duplication.
7 | - Use descriptive variable names with auxiliary verbs (e.g., isLoading, hasError).
8 | - Structure files: exported component, subcomponents, helpers, static content, types.
9 |
10 | Naming Conventions
11 | - Use lowercase with dashes for directories (e.g., components/auth-wizard).
12 | - Favor named exports for components.
13 |
14 | TypeScript Usage
15 | - Use TypeScript for all code; prefer interfaces over types.
16 | - Avoid enums; use maps instead.
17 | - Use functional components with TypeScript interfaces.
18 |
19 | Syntax and Formatting
20 | - Use the "function" keyword for pure functions.
21 | - Avoid unnecessary curly braces in conditionals; use concise syntax for simple statements.
22 | - Use declarative JSX.
23 |
24 | UI and Styling
25 | - Use Shadcn UI, Radix, and Tailwind for components and styling.
26 | - Implement responsive design with Tailwind CSS; use a mobile-first approach.
27 |
28 | Performance Optimization
29 | - Minimize 'use client', 'useEffect', and 'setState'; favor React Server Components (RSC).
30 | - Wrap client components in Suspense with fallback.
31 | - Use dynamic loading for non-critical components.
32 | - Optimize images: use WebP format, include size data, implement lazy loading.
33 |
34 | Key Conventions.
35 | - Optimize Web Vitals (LCP, CLS, FID).
36 | - Limit 'use client':
37 | - Favor server components and Next.js SSR.
38 | - Use only for Web API access in small components.
39 | - Avoid for data fetching or state management.
40 |
41 | Follow Next.js docs for Data Fetching, Rendering, and Routing.
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Moinul Moin
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 | 
2 |
3 | # ChadNext ✨
4 |
5 | ChadNext is a quick starter template for Next.js projects, designed to streamline development by providing essential features out of the box. 🚀
6 |
7 | ## Motivation 🌟
8 |
9 | ChadNext addresses common pain points, making it easier to:
10 |
11 | - Prototype and test ideas swiftly
12 | - Access a beautifully designed UI library
13 | - Implement simple authentication
14 | - Interact with databases effortlessly
15 | - Deploy with ease
16 |
17 | Save time and effort, and build performant apps with an excellent developer experience.
18 |
19 | ## Getting Started 🚀
20 |
21 | 1. Clone the repo.
22 | 2. Install dependencies: `pnpm install`
23 | 3. Copy `.env.example` file to `.env` file, then follow the instructions inside.
24 | 4. Run `pnpm prisma db push` to set up the database.
25 | 5. Start the dev server: `pnpm dev`
26 |
27 | ### Or
28 |
29 | [](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fmoinulmoin%2Fchadnext&env=DB_PRISMA_URL,DB_URL_NON_POOLING,GITHUB_CLIENT_ID,GITHUB_CLIENT_SECRET,NEXTAUTH_SECRET,NEXT_PUBLIC_APP_URL,RESEND_API_KEY,UPLOADTHING_SECRET,UPLOADTHING_APP_ID,UPLOADTHING_URL)
30 |
31 | ## Contributing 🤝
32 |
33 | 1. Fork the repo.
34 | 2. Create a branch.
35 | 3. Make changes and commit.
36 | 4. Push and create a pull request.
37 |
38 | ## License 📄
39 |
40 | [MIT License](https://github.com/moinulmoin/chadnext/blob/main/LICENSE)
41 |
42 | ## Author ✍️
43 |
44 | Moinul Moin ([@immoinulmoin](https://twitter.com/immoinulmoin))
45 |
--------------------------------------------------------------------------------
/components.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://ui.shadcn.com/schema.json",
3 | "style": "default",
4 | "rsc": true,
5 | "tsx": true,
6 | "tailwind": {
7 | "config": "tailwind.config.js",
8 | "css": "src/app/globals.css",
9 | "baseColor": "slate",
10 | "cssVariables": true,
11 | "prefix": ""
12 | },
13 | "aliases": {
14 | "components": "~/components",
15 | "utils": "~/lib/utils",
16 | "ui": "~/components/ui",
17 | "lib": "~/lib",
18 | "hooks": "~/hooks"
19 | },
20 | "iconLibrary": "lucide"
21 | }
--------------------------------------------------------------------------------
/emails/thanks.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Body,
3 | Button,
4 | Container,
5 | Head,
6 | Html,
7 | Preview,
8 | Section,
9 | Tailwind,
10 | Text,
11 | } from "@react-email/components";
12 |
13 | interface ThanksTemplateProps {
14 | userName: string;
15 | }
16 |
17 | const ThanksTemp: React.FC> = ({ userName }) => (
18 |
19 |
20 | Welcome to ChadNext.
21 |
22 |
23 |
24 |
25 | Hi {userName} 👋 ,
26 |
27 | Welcome to ChadNext. Now you can build your idea faster. You can
28 | star the project on GitHub. That would be very helpful.
29 |
30 |
31 |
39 |
40 | Best,
41 | ChadNext
42 |
43 |
44 |
45 |
46 |
47 | );
48 |
49 | export default ThanksTemp;
50 |
--------------------------------------------------------------------------------
/emails/verification.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Body,
3 | Container,
4 | Head,
5 | Html,
6 | Preview,
7 | Section,
8 | Tailwind,
9 | Text
10 | } from "@react-email/components";
11 |
12 | const siteUrl =
13 | process.env.NEXT_PUBLIC_URL ?? "https://chadnext.moinulmoin.com";
14 |
15 | interface VerificationTemplateProps {
16 | userName: string;
17 | code: string;
18 | }
19 |
20 | const VerificationTemp: React.FC> = ({
21 | userName = "X",
22 | code = "46590",
23 | }) => (
24 |
25 |
26 | Verify your email
27 |
28 |
29 |
30 | Hi, {userName.split(" ")[0]}
31 |
32 | Here is your verification code.
33 |
34 |
35 |
36 | {code}
37 |
38 |
39 | This code expires in 3 minutes and can only be used once.
40 |
41 |
42 |
43 | Best,
44 |
45 | ChadNext
46 |
47 |
48 |
49 |
50 |
51 | );
52 |
53 | export default VerificationTemp;
54 |
--------------------------------------------------------------------------------
/example.env:
--------------------------------------------------------------------------------
1 | # Create a free PostgreSQL database: https://vercel.com/postgres
2 | DB_PRISMA_URL=
3 | DB_URL_NON_POOLING=
4 |
5 | # Create github oauth app: https://github.com/settings/developers
6 | GITHUB_CLIENT_ID=
7 | GITHUB_CLIENT_SECRET=
8 |
9 | # Site Url
10 | NEXT_PUBLIC_APP_URL="http://localhost:3000"
11 | UPLOADTHING_URL="http://localhost:3000"
12 |
13 | # Create an account in resend and get the api key https://resend.com/
14 | RESEND_API_KEY=
15 |
16 | # Create a project in uploadthing and get api keys https://uploadthing.com/
17 | UPLOADTHING_SECRET=
18 | UPLOADTHING_APP_ID=
19 |
20 | #Stripe https://stripe.com/
21 | NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=
22 | STRIPE_SECRET_KEY=
23 | STRIPE_WEBHOOK_SECRET =
24 | STRIPE_PRO_PLAN_ID=
--------------------------------------------------------------------------------
/lefthook.yml:
--------------------------------------------------------------------------------
1 | # EXAMPLE USAGE:
2 | #
3 | # Refer for explanation to following link:
4 | # https://github.com/evilmartians/lefthook/blob/master/docs/configuration.md
5 | #
6 |
7 | pre-commit:
8 | commands:
9 | typecheck:
10 | glob: "src/**/*.{ts,tsx}"
11 | run: npx tsc --noEmit
12 | lint:
13 | glob: "src/**/*.{ts,tsx,js,jsx}"
14 | run: pnpm lint
15 | format:
16 | glob: "src/**/*.{ts,tsx,js,jsx,css}"
17 | run: pnpm prettier --write {staged_files} && git update-index --again
18 |
--------------------------------------------------------------------------------
/next.config.mjs:
--------------------------------------------------------------------------------
1 | import withSerwistInit from "@serwist/next";
2 |
3 | const withSerwist = withSerwistInit({
4 | // Note: This is only an example. If you use Pages Router,
5 | // use something else that works, such as "service-worker/index.ts".
6 | swSrc: "src/app/sw.ts",
7 | swDest: "public/sw.js",
8 | disable: process.env.NODE_ENV !== "production",
9 | });
10 |
11 | /**
12 | * @type {import('next').NextConfig}
13 | */
14 | const nextConfig = {
15 | redirects: async () => {
16 | return [
17 | {
18 | source: "/dashboard",
19 | destination: "/dashboard/projects",
20 | permanent: false,
21 | },
22 | ];
23 | },
24 | trailingSlash: true,
25 | eslint: {
26 | ignoreDuringBuilds: true,
27 | },
28 | }
29 |
30 | export default withSerwist(nextConfig);
31 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "chadnext",
3 | "version": "1.7.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "prisma generate && next dev --turbo",
7 | "dev:email": "email dev -p 9000",
8 | "dev:content": "velite build --watch",
9 | "build:content": "velite build --clean",
10 | "build": "pnpm build:content && prisma generate && prisma db push && next build",
11 | "start": "next start",
12 | "lint": "next lint --dir src",
13 | "format": "prettier --write 'src/**/*.{ts,tsx,js,jsx,css}'",
14 | "preinstall": "lefthook install",
15 | "release": "release-it"
16 | },
17 | "dependencies": {
18 | "@hookform/resolvers": "^3.10.0",
19 | "@oslojs/crypto": "^1.0.1",
20 | "@oslojs/encoding": "^1.1.0",
21 | "@prisma/client": "^6.2.1",
22 | "@radix-ui/react-accordion": "^1.2.3",
23 | "@radix-ui/react-alert-dialog": "^1.1.4",
24 | "@radix-ui/react-aspect-ratio": "^1.1.2",
25 | "@radix-ui/react-avatar": "^1.1.2",
26 | "@radix-ui/react-checkbox": "^1.1.4",
27 | "@radix-ui/react-collapsible": "^1.1.3",
28 | "@radix-ui/react-context-menu": "^2.2.6",
29 | "@radix-ui/react-dialog": "^1.1.4",
30 | "@radix-ui/react-dropdown-menu": "^2.1.4",
31 | "@radix-ui/react-hover-card": "^1.1.6",
32 | "@radix-ui/react-label": "^2.1.1",
33 | "@radix-ui/react-menubar": "^1.1.4",
34 | "@radix-ui/react-navigation-menu": "^1.2.5",
35 | "@radix-ui/react-popover": "^1.1.6",
36 | "@radix-ui/react-progress": "^1.1.2",
37 | "@radix-ui/react-radio-group": "^1.2.3",
38 | "@radix-ui/react-scroll-area": "^1.2.3",
39 | "@radix-ui/react-select": "^2.1.4",
40 | "@radix-ui/react-separator": "^1.1.2",
41 | "@radix-ui/react-slider": "^1.2.3",
42 | "@radix-ui/react-slot": "^1.1.1",
43 | "@radix-ui/react-switch": "^1.1.3",
44 | "@radix-ui/react-tabs": "^1.1.2",
45 | "@radix-ui/react-toast": "^1.2.4",
46 | "@radix-ui/react-toggle": "^1.1.2",
47 | "@radix-ui/react-toggle-group": "^1.1.2",
48 | "@radix-ui/react-tooltip": "^1.1.8",
49 | "@react-email/components": "^0.0.32",
50 | "@serwist/next": "^9.0.11",
51 | "@uploadthing/react": "^7.1.5",
52 | "arctic": "^3.1.3",
53 | "cmdk": "^1.1.1",
54 | "date-fns": "^4.1.0",
55 | "dayjs": "^1.11.13",
56 | "embla-carousel-react": "^8.5.2",
57 | "input-otp": "^1.4.2",
58 | "lucide-react": "^0.471.1",
59 | "next": "15.3.2",
60 | "next-international": "^1.3.1",
61 | "next-safe-action": "^7.10.2",
62 | "next-themes": "^0.4.4",
63 | "postcss": "8.5.1",
64 | "react": "19.1.0",
65 | "react-day-picker": "8.10.1",
66 | "react-dom": "19.1.0",
67 | "react-email": "3.0.6",
68 | "react-hook-form": "^7.54.2",
69 | "react-resizable-panels": "^2.1.7",
70 | "recharts": "^2.15.2",
71 | "resend": "^4.1.1",
72 | "sonner": "^2.0.3",
73 | "stripe": "^18.0.0",
74 | "uploadthing": "^7.4.4",
75 | "vaul": "^1.1.2",
76 | "zod": "^3.24.1"
77 | },
78 | "devDependencies": {
79 | "@tailwindcss/line-clamp": "^0.4.4",
80 | "@tailwindcss/typography": "^0.5.16",
81 | "@types/node": "22.15.18",
82 | "@types/react": "19.1.4",
83 | "@types/react-dom": "19.1.5",
84 | "autoprefixer": "10.4.20",
85 | "class-variance-authority": "^0.7.1",
86 | "clsx": "^2.1.1",
87 | "eslint": "9.24.0",
88 | "eslint-config-next": "15.3.2",
89 | "eslint-config-prettier": "^10.1.1",
90 | "lefthook": "^1.10.7",
91 | "prettier": "3.5.3",
92 | "prettier-plugin-tailwindcss": "^0.6.10",
93 | "prisma": "^6.2.1",
94 | "release-it": "^18.1.1",
95 | "release-it-pnpm": "^4.6.4",
96 | "serwist": "^9.0.11",
97 | "tailwind-merge": "^2.6.0",
98 | "tailwindcss": "3.4.17",
99 | "tailwindcss-animate": "^1.0.7",
100 | "typescript": "5.7.3",
101 | "velite": "0.2.2"
102 | },
103 | "packageManager": "pnpm@10.8.0+sha512.0e82714d1b5b43c74610193cb20734897c1d00de89d0e18420aebc5977fa13d780a9cb05734624e81ebd81cc876cd464794850641c48b9544326b5622ca29971"
104 | }
105 |
--------------------------------------------------------------------------------
/pnpm-workspace.yaml:
--------------------------------------------------------------------------------
1 | onlyBuiltDependencies:
2 | - '@prisma/client'
3 | - '@prisma/engines'
4 | - esbuild
5 | - lefthook
6 | - prisma
7 | - sharp
8 | - unrs-resolver
9 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/prisma/schema.prisma:
--------------------------------------------------------------------------------
1 | // learn more about it in the docs: https://pris.ly/d/prisma-schema
2 |
3 | generator client {
4 | provider = "prisma-client-js"
5 | }
6 |
7 | datasource db {
8 | provider = "postgresql"
9 | url = env("DB_PRISMA_URL") // uses connection pooling
10 | directUrl = env("DB_URL_NON_POOLING") // uses a direct connection
11 | }
12 |
13 | model Session {
14 | id String @id @default(cuid())
15 | userId String
16 | expiresAt DateTime
17 | user User @relation(references: [id], fields: [userId], onDelete: Cascade)
18 | }
19 |
20 | model EmailVerificationCode {
21 | id String @id @default(cuid())
22 | code String
23 | userId String
24 | email String
25 | expiresAt DateTime
26 | user User @relation(references: [id], fields: [userId], onDelete: Cascade)
27 | }
28 |
29 | model User {
30 | id String @id @unique @default(cuid())
31 | name String?
32 | email String? @unique
33 | emailVerified Boolean? @default(false)
34 | picture String?
35 | githubId Int? @unique
36 | sessions Session[]
37 | projects Project[]
38 | emailVerificationCodes EmailVerificationCode[]
39 |
40 | stripeCustomerId String? @unique @map(name: "stripe_customer_id")
41 | stripeSubscriptionId String? @unique @map(name: "stripe_subscription_id")
42 | stripePriceId String? @map(name: "stripe_price_id")
43 | stripeCurrentPeriodEnd DateTime? @map(name: "stripe_current_period_end")
44 |
45 | createdAt DateTime @default(now())
46 | updatedAt DateTime @updatedAt
47 | }
48 |
49 | model Project {
50 | id String @id @default(cuid())
51 | name String
52 | domain String
53 | userId String
54 | user User @relation(fields: [userId], references: [id], onDelete: Cascade)
55 | createdAt DateTime @default(now())
56 | updatedAt DateTime @updatedAt
57 | }
58 |
--------------------------------------------------------------------------------
/public/android-chrome-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/moinulmoin/chadnext/6860d82ee9ecade5c32d8806f7c2b5683b7bb725/public/android-chrome-192x192.png
--------------------------------------------------------------------------------
/public/android-chrome-512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/moinulmoin/chadnext/6860d82ee9ecade5c32d8806f7c2b5683b7bb725/public/android-chrome-512x512.png
--------------------------------------------------------------------------------
/public/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/moinulmoin/chadnext/6860d82ee9ecade5c32d8806f7c2b5683b7bb725/public/apple-touch-icon.png
--------------------------------------------------------------------------------
/public/chad-next.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/moinulmoin/chadnext/6860d82ee9ecade5c32d8806f7c2b5683b7bb725/public/chad-next.png
--------------------------------------------------------------------------------
/public/chadnext-homepage.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/moinulmoin/chadnext/6860d82ee9ecade5c32d8806f7c2b5683b7bb725/public/chadnext-homepage.png
--------------------------------------------------------------------------------
/public/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/moinulmoin/chadnext/6860d82ee9ecade5c32d8806f7c2b5683b7bb725/public/favicon-16x16.png
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/moinulmoin/chadnext/6860d82ee9ecade5c32d8806f7c2b5683b7bb725/public/favicon.ico
--------------------------------------------------------------------------------
/src/app/[locale]/(auth)/login/page.tsx:
--------------------------------------------------------------------------------
1 | import { redirect } from "next/navigation";
2 | import AuthForm from "~/components/layout/auth-form";
3 | import { Card } from "~/components/ui/card";
4 | import { getCurrentSession } from "~/lib/server/auth/session";
5 |
6 | export default async function Login() {
7 | const { session } = await getCurrentSession();
8 | if (session) return redirect("/dashboard");
9 | return (
10 |
11 |
12 |
13 |
14 |
15 | Login
16 |
17 |
18 |
19 |
20 |
21 |
22 | );
23 | }
24 |
--------------------------------------------------------------------------------
/src/app/[locale]/(marketing)/about/page.tsx:
--------------------------------------------------------------------------------
1 | import { abouts, type About } from "content";
2 | import { type Metadata } from "next";
3 |
4 | function AboutCard(about: About) {
5 | return (
6 |
7 |
8 | {about.title}
9 |
10 |
11 |
12 | );
13 | }
14 |
15 | export const metadata: Metadata = {
16 | title: "About",
17 | description: "Learn about the tech stack and inspiration behind ChadNext.",
18 | };
19 |
20 | export default function About() {
21 | return (
22 |
23 |
About
24 |
25 | Learn about the tech stack and inspiration behind ChadNext.
26 |
27 |
28 | {abouts.map((p, i) => (
29 |
30 | ))}
31 |
32 |
33 | );
34 | }
35 |
--------------------------------------------------------------------------------
/src/app/[locale]/(marketing)/changelog/page.tsx:
--------------------------------------------------------------------------------
1 | import { type Change, changes } from "content";
2 | import dayjs from "dayjs";
3 | import { type Metadata } from "next";
4 |
5 | function ChangeCard(change: Change) {
6 | return (
7 |
8 |
9 | {change.title}
10 |
11 |
14 |
15 |
16 | );
17 | }
18 |
19 | export const metadata: Metadata = {
20 | title: "Changelog",
21 | description: "All the latest updates, improvements, and fixes.",
22 | };
23 |
24 | export default function Changelog() {
25 | const posts = changes.sort((a, b) =>
26 | dayjs(a.date).isAfter(dayjs(b.date)) ? -1 : 1
27 | );
28 |
29 | return (
30 |
31 |
32 | Changelog
33 |
34 |
35 | All the latest updates, improvements, and fixes.
36 |
37 |
38 | {posts.map((change, idx) => (
39 |
40 | ))}
41 |
42 |
43 | );
44 | }
45 |
--------------------------------------------------------------------------------
/src/app/[locale]/@loginDialog/(.)login/page.tsx:
--------------------------------------------------------------------------------
1 | import { redirect } from "next/navigation";
2 | import LoginModal from "~/components/layout/login-modal";
3 | import { getCurrentSession } from "~/lib/server/auth/session";
4 |
5 | export default async function Login() {
6 | const { session } = await getCurrentSession();
7 | if (session) return redirect("/dashboard");
8 | return ;
9 | }
10 |
--------------------------------------------------------------------------------
/src/app/[locale]/@loginDialog/default.tsx:
--------------------------------------------------------------------------------
1 | export default function Default() {
2 | return null;
3 | }
4 |
--------------------------------------------------------------------------------
/src/app/[locale]/actions.ts:
--------------------------------------------------------------------------------
1 | "use server";
2 |
3 | import { revalidatePath } from "next/cache";
4 | import { redirect } from "next/navigation";
5 | import { authActionClient } from "~/lib/client/safe-action";
6 | import { deleteSessionTokenCookie } from "~/lib/server/auth/cookies";
7 | import { invalidateSession } from "~/lib/server/auth/session";
8 |
9 | export const logout = authActionClient.action(
10 | async ({ ctx: { sessionId } }) => {
11 | await invalidateSession(sessionId);
12 | deleteSessionTokenCookie();
13 | revalidatePath("/");
14 | return redirect("/login");
15 | }
16 | );
17 |
--------------------------------------------------------------------------------
/src/app/[locale]/dashboard/billing/loading.tsx:
--------------------------------------------------------------------------------
1 | import { Skeleton } from "~/components/ui/skeleton";
2 |
3 | export default function Loading() {
4 | return (
5 |
6 |
7 |
8 |
9 | );
10 | }
11 |
--------------------------------------------------------------------------------
/src/app/[locale]/dashboard/billing/page.tsx:
--------------------------------------------------------------------------------
1 | import { AlertTriangleIcon } from "lucide-react";
2 | import { BillingForm } from "~/components/billing-form";
3 | import { Alert, AlertDescription } from "~/components/ui/alert";
4 | import { getCurrentSession } from "~/lib/server/auth/session";
5 | import { getUserSubscriptionPlan, stripe } from "~/lib/server/payment";
6 |
7 | export default async function Billing() {
8 | const { user } = await getCurrentSession();
9 |
10 | const subscriptionPlan = await getUserSubscriptionPlan(user?.id as string);
11 |
12 | // If user has a pro plan, check cancel status on Stripe.
13 | let isCanceled = false;
14 | if (subscriptionPlan.isPro && subscriptionPlan.stripeSubscriptionId) {
15 | const stripePlan = await stripe.subscriptions.retrieve(
16 | subscriptionPlan.stripeSubscriptionId
17 | );
18 | isCanceled = stripePlan.cancel_at_period_end;
19 | }
20 | return (
21 |
22 |
23 |
24 |
25 |
26 |
27 | ChadNext just demonstrates how to use Stripe in
28 | Next.js App router. Please use test cards from{" "}
29 |
35 | Stripe docs
36 |
37 | .
38 |
39 |
40 |
41 |
42 |
48 |
49 | );
50 | }
51 |
--------------------------------------------------------------------------------
/src/app/[locale]/dashboard/error.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useEffect } from "react";
4 | import { Button } from "~/components/ui/button";
5 |
6 | export default function Error({
7 | error,
8 | reset,
9 | }: {
10 | error: Error;
11 | reset: () => void;
12 | }) {
13 | useEffect(() => {
14 | // Log the error to an error reporting service
15 | console.log(error);
16 | }, [error]);
17 |
18 | return (
19 |
20 |
21 | Oops, Something Went Wrong!
22 |
23 |
24 |
25 | );
26 | }
27 |
--------------------------------------------------------------------------------
/src/app/[locale]/dashboard/layout.tsx:
--------------------------------------------------------------------------------
1 | import { redirect } from "next/navigation";
2 | import SidebarNav from "~/components/layout/sidebar-nav";
3 | import { getCurrentSession } from "~/lib/server/auth/session";
4 |
5 | interface DashboardLayoutProps {
6 | children: React.ReactNode;
7 | }
8 |
9 | export default async function DashboardLayout({
10 | children,
11 | }: DashboardLayoutProps) {
12 | const { session } = await getCurrentSession();
13 | if (!session) redirect("/login");
14 | return (
15 |
16 |
17 |
20 |
{children}
21 |
22 |
23 | );
24 | }
25 |
--------------------------------------------------------------------------------
/src/app/[locale]/dashboard/projects/[projectId]/delete-card.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import { useTransition } from "react";
3 | import Icons from "~/components/shared/icons";
4 | import {
5 | AlertDialog,
6 | AlertDialogAction,
7 | AlertDialogCancel,
8 | AlertDialogContent,
9 | AlertDialogFooter,
10 | AlertDialogHeader,
11 | AlertDialogTitle,
12 | AlertDialogTrigger,
13 | } from "~/components/ui/alert-dialog";
14 | import { Button } from "~/components/ui/button";
15 | import { Card, CardDescription, CardTitle } from "~/components/ui/card";
16 | import { toast } from "~/hooks/use-toast";
17 | import { isRedirectError } from "~/lib/utils";
18 | import { deleteProjectById } from "../action";
19 |
20 | export default function DeleteCard({ id }: { id: string }) {
21 | const [pending, startTransition] = useTransition();
22 | const handleDelete = async () => {
23 | startTransition(() =>
24 | deleteProjectById(id)
25 | .then(() => {
26 | toast({
27 | title: "Project deleted successfully.",
28 | });
29 | })
30 | .catch((error) => {
31 | console.log(error);
32 | if (!isRedirectError(error)) {
33 | toast({
34 | title: "Error deleting project.",
35 | description: "Please try again.",
36 | variant: "destructive",
37 | });
38 | }
39 | })
40 | );
41 | };
42 | return (
43 |
44 |
45 | Delete Project
46 |
47 | The project will be permanently deleted. This action is irreversible
48 | and can not be undone.
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 | Are you absolutely sure?
59 |
60 |
61 | Cancel
62 |
63 |
69 |
70 |
71 |
72 |
73 |
74 | );
75 | }
76 |
--------------------------------------------------------------------------------
/src/app/[locale]/dashboard/projects/[projectId]/editable-details.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { zodResolver } from "@hookform/resolvers/zod";
4 | import { useForm } from "react-hook-form";
5 | import CopyButton from "~/components/copy-button";
6 | import Icons from "~/components/shared/icons";
7 | import { Button } from "~/components/ui/button";
8 | import {
9 | Form,
10 | FormControl,
11 | FormField,
12 | FormItem,
13 | FormLabel,
14 | FormMessage,
15 | } from "~/components/ui/form";
16 | import { Input } from "~/components/ui/input";
17 | import { toast } from "~/hooks/use-toast";
18 | import { updateProjectById } from "../action";
19 | import { projectSchema, type ProjectFormValues } from "../create-project-modal";
20 |
21 | export default function EditableDetails({
22 | initialValues,
23 | }: {
24 | initialValues: ProjectFormValues & { id: string };
25 | }) {
26 | const form = useForm({
27 | resolver: zodResolver(projectSchema),
28 | values: initialValues,
29 | });
30 |
31 | async function onSubmit(values: ProjectFormValues) {
32 | try {
33 | await updateProjectById(initialValues.id, values);
34 | toast({
35 | title: "Project Updated successfully.",
36 | });
37 | form.reset();
38 | } catch (error) {
39 | console.log(error);
40 | toast({
41 | title: "Error creating project.",
42 | description: "Please try again.",
43 | variant: "destructive",
44 | });
45 | }
46 | }
47 | return (
48 |
97 |
98 | );
99 | }
100 |
--------------------------------------------------------------------------------
/src/app/[locale]/dashboard/projects/[projectId]/layout.tsx:
--------------------------------------------------------------------------------
1 | import GoBack from "~/components/go-back";
2 |
3 | export default function SingleProjectLayout({
4 | children,
5 | }: {
6 | children: React.ReactNode;
7 | }) {
8 | return (
9 | <>
10 |
11 | {children}
12 | >
13 | );
14 | }
15 |
--------------------------------------------------------------------------------
/src/app/[locale]/dashboard/projects/[projectId]/page.tsx:
--------------------------------------------------------------------------------
1 | import { getProjectById } from "../action";
2 | import TabSections from "./tab-sections";
3 |
4 | export default async function SingleProject({
5 | params,
6 | }: {
7 | params: Promise<{ projectId: string }>;
8 | }) {
9 | const { projectId } = await params;
10 | const project = await getProjectById(projectId);
11 | return ;
12 | }
13 |
--------------------------------------------------------------------------------
/src/app/[locale]/dashboard/projects/[projectId]/tab-sections.tsx:
--------------------------------------------------------------------------------
1 | import { type Project } from "@prisma/client";
2 | import { Tabs, TabsContent, TabsList, TabsTrigger } from "~/components/ui/tabs";
3 | import DeleteCard from "./delete-card";
4 | import EditableDetails from "./editable-details";
5 |
6 | export default function TabSections({ project }: { project: Project }) {
7 | return (
8 |
9 |
10 | Details
11 | Settings
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | );
21 | }
22 |
--------------------------------------------------------------------------------
/src/app/[locale]/dashboard/projects/action.ts:
--------------------------------------------------------------------------------
1 | "use server";
2 |
3 | import { type Project } from "@prisma/client";
4 | import { revalidatePath } from "next/cache";
5 | import { redirect } from "next/navigation";
6 | import { getCurrentSession } from "~/lib/server/auth/session";
7 | import { prisma } from "~/lib/server/db";
8 | import { getUserSubscriptionPlan } from "~/lib/server/payment";
9 |
10 | interface Payload {
11 | name: string;
12 | domain: string;
13 | }
14 |
15 | export async function createProject(payload: Payload) {
16 | const { user } = await getCurrentSession();
17 |
18 | await prisma.project.create({
19 | data: {
20 | ...payload,
21 | user: {
22 | connect: {
23 | id: user?.id,
24 | },
25 | },
26 | },
27 | });
28 |
29 | revalidatePath(`/dashboard/projects`);
30 | }
31 |
32 | export async function checkIfFreePlanLimitReached() {
33 | const { user } = await getCurrentSession();
34 | const subscriptionPlan = await getUserSubscriptionPlan(user?.id as string);
35 |
36 | // If user is on a free plan.
37 | // Check if user has reached limit of 3 projects.
38 | if (subscriptionPlan?.isPro) return false;
39 |
40 | const count = await prisma.project.count({
41 | where: {
42 | userId: user?.id,
43 | },
44 | });
45 |
46 | return count >= 3;
47 | }
48 |
49 | export async function getProjects() {
50 | const { user } = await getCurrentSession();
51 | const projects = await prisma.project.findMany({
52 | where: {
53 | userId: user?.id,
54 | },
55 | orderBy: {
56 | createdAt: "desc",
57 | },
58 | });
59 | return projects as Project[];
60 | }
61 |
62 | export async function getProjectById(id: string) {
63 | const { user } = await getCurrentSession();
64 | const project = await prisma.project.findFirst({
65 | where: {
66 | id,
67 | userId: user?.id,
68 | },
69 | });
70 | return project as Project;
71 | }
72 |
73 | export async function updateProjectById(id: string, payload: Payload) {
74 | const { user } = await getCurrentSession();
75 | await prisma.project.update({
76 | where: {
77 | id,
78 | userId: user?.id,
79 | },
80 | data: payload,
81 | });
82 | revalidatePath(`/dashboard/projects`);
83 | }
84 |
85 | export async function deleteProjectById(id: string) {
86 | const { user } = await getCurrentSession();
87 | await prisma.project.delete({
88 | where: {
89 | id,
90 | userId: user?.id,
91 | },
92 | });
93 | revalidatePath(`/dashboard/projects`);
94 | redirect("/dashboard/projects");
95 | }
96 |
--------------------------------------------------------------------------------
/src/app/[locale]/dashboard/projects/loading.tsx:
--------------------------------------------------------------------------------
1 | import { Skeleton } from "~/components/ui/skeleton";
2 |
3 | export default function Loading() {
4 | return (
5 |
6 |
7 |
8 |
9 |
10 |
11 | );
12 | }
13 |
--------------------------------------------------------------------------------
/src/app/[locale]/dashboard/projects/page.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link";
2 | import { Card } from "~/components/ui/card";
3 | import { getProjects } from "./action";
4 | import CreateProjectModal from "./create-project-modal";
5 |
6 | export default async function Projects() {
7 | const projects = await getProjects();
8 |
9 | return (
10 |
11 |
12 | {projects.map((project) => (
13 |
18 | {project.name}
19 | {`https://${project.domain}`}
20 |
24 | View project details
25 |
26 |
27 | ))}
28 |
29 | );
30 | }
31 |
--------------------------------------------------------------------------------
/src/app/[locale]/dashboard/settings/actions.ts:
--------------------------------------------------------------------------------
1 | "use server";
2 |
3 | import { revalidatePath } from "next/cache";
4 | import { prisma } from "~/lib/server/db";
5 | import { utapi } from "~/lib/server/upload";
6 | import { getImageKeyFromUrl, isOurCdnUrl } from "~/lib/utils";
7 | import { type payload } from "~/types";
8 |
9 | export const updateUser = async (id: string, payload: payload) => {
10 | await prisma.user.update({
11 | where: { id },
12 | data: { ...payload },
13 | });
14 |
15 | revalidatePath("/dashboard/settings");
16 | };
17 |
18 | export async function removeUserOldImageFromCDN(
19 | newImageUrl: string,
20 | currentImageUrl: string
21 | ) {
22 | try {
23 | if (isOurCdnUrl(currentImageUrl)) {
24 | const currentImageFileKey = getImageKeyFromUrl(currentImageUrl);
25 |
26 | await utapi.deleteFiles(currentImageFileKey as string);
27 | }
28 | } catch (e) {
29 | console.log(e);
30 | const newImageFileKey = getImageKeyFromUrl(newImageUrl);
31 | await utapi.deleteFiles(newImageFileKey as string);
32 | }
33 | }
34 |
35 | export async function removeNewImageFromCDN(image: string) {
36 | const imageFileKey = getImageKeyFromUrl(image);
37 | await utapi.deleteFiles(imageFileKey as string);
38 | }
39 |
--------------------------------------------------------------------------------
/src/app/[locale]/dashboard/settings/error.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useEffect } from "react";
4 | import { Button } from "~/components/ui/button";
5 |
6 | export default function Error({
7 | error,
8 | reset,
9 | }: {
10 | error: Error;
11 | reset: () => void;
12 | }) {
13 | useEffect(() => {
14 | // Log the error to an error reporting service
15 | console.log(error);
16 | }, [error]);
17 |
18 | return (
19 |
20 |
21 | Something Went Wrong!
22 |
23 |
24 |
25 | );
26 | }
27 |
--------------------------------------------------------------------------------
/src/app/[locale]/dashboard/settings/loading.tsx:
--------------------------------------------------------------------------------
1 | import { Skeleton } from "~/components/ui/skeleton";
2 |
3 | export default function Loading() {
4 | return ;
5 | }
6 |
--------------------------------------------------------------------------------
/src/app/[locale]/dashboard/settings/page.tsx:
--------------------------------------------------------------------------------
1 | import { type Metadata } from "next";
2 | import { getCurrentSession } from "~/lib/server/auth/session";
3 | import SettingsForm from "./settings-form";
4 |
5 | export const metadata: Metadata = {
6 | title: "Settings",
7 | };
8 |
9 | export default async function Settings() {
10 | const { user } = await getCurrentSession();
11 | return ;
12 | }
13 |
--------------------------------------------------------------------------------
/src/app/[locale]/layout.tsx:
--------------------------------------------------------------------------------
1 | import { type Metadata } from "next";
2 | import { Inter } from "next/font/google";
3 | import localFont from "next/font/local";
4 | import Script from "next/script";
5 | import Footer from "~/components/layout/footer";
6 | import Header from "~/components/layout/header";
7 | import ThemeProvider from "~/components/shared/theme-provider";
8 | import { Toaster } from "~/components/ui/toaster";
9 | import { siteConfig, siteUrl } from "~/config/site";
10 | import { cn } from "~/lib/utils";
11 | import { I18nProviderClient } from "~/locales/client";
12 | import "../globals.css";
13 |
14 | type Props = {
15 | params: Promise<{ locale: string }>;
16 | searchParams: { [key: string]: string | string[] | undefined };
17 | };
18 |
19 | export async function generateMetadata({ params }: Props): Promise {
20 | const p = await params;
21 | const locale = p.locale;
22 | const site = siteConfig(locale);
23 |
24 | const siteOgImage = `${siteUrl}/api/og?locale=${locale}`;
25 |
26 | return {
27 | title: {
28 | default: site.name,
29 | template: `%s - ${site.name}`,
30 | },
31 | description: site.description,
32 | keywords: [
33 | "Next.js",
34 | "Shadcn/ui",
35 | "LuciaAuth",
36 | "Prisma",
37 | "Vercel",
38 | "Tailwind",
39 | "Radix UI",
40 | "Stripe",
41 | "Internationalization",
42 | "Postgres",
43 | ],
44 | authors: [
45 | {
46 | name: "moinulmoin",
47 | url: "https://moinulmoin.com",
48 | },
49 | ],
50 | creator: "Moinul Moin",
51 | openGraph: {
52 | type: "website",
53 | locale: locale,
54 | url: site.url,
55 | title: site.name,
56 | description: site.description,
57 | siteName: site.name,
58 | images: [
59 | {
60 | url: siteOgImage,
61 | width: 1200,
62 | height: 630,
63 | alt: site.name,
64 | },
65 | ],
66 | },
67 | twitter: {
68 | card: "summary_large_image",
69 | title: site.name,
70 | description: site.description,
71 | images: [siteOgImage],
72 | creator: "@immoinulmoin",
73 | },
74 | icons: {
75 | icon: "/favicon.ico",
76 | shortcut: "/favicon-16x16.png",
77 | apple: "/apple-touch-icon.png",
78 | },
79 | manifest: `${siteUrl}/manifest.json`,
80 | metadataBase: new URL(site.url),
81 | alternates: {
82 | canonical: "/",
83 | languages: {
84 | en: "/en",
85 | fr: "/fr",
86 | },
87 | },
88 | appleWebApp: {
89 | capable: true,
90 | statusBarStyle: "default",
91 | title: site.name,
92 | },
93 | };
94 | }
95 |
96 | export const viewport = {
97 | width: 1,
98 | themeColor: [
99 | { media: "(prefers-color-scheme: light)", color: "white" },
100 | { media: "(prefers-color-scheme: dark)", color: "black" },
101 | ],
102 | };
103 |
104 | const fontSans = Inter({
105 | subsets: ["latin"],
106 | variable: "--font-sans",
107 | });
108 |
109 | const fontHeading = localFont({
110 | src: "../../assets/fonts/CalSans-SemiBold.woff2",
111 | variable: "--font-heading",
112 | });
113 |
114 | export default async function RootLayout({
115 | children,
116 | loginDialog,
117 | params,
118 | }: {
119 | children: React.ReactNode;
120 | loginDialog: React.ReactNode;
121 | params: Promise<{ locale: string }>;
122 | }) {
123 | const { locale } = await params;
124 | return (
125 |
126 |
133 |
134 |
135 |
136 | {children}
137 | {loginDialog}
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 | {/* remove this line when you are working on your own project */}
146 | {process.env.NODE_ENV === "production" && (
147 |
151 | )}
152 |
153 | );
154 | }
155 |
--------------------------------------------------------------------------------
/src/app/[locale]/not-found.tsx:
--------------------------------------------------------------------------------
1 | import { getScopedI18n } from "~/locales/server";
2 |
3 | async function NotFound() {
4 | const t = await getScopedI18n("notFound");
5 | return (
6 |
7 |
8 |
404
{" "}
9 |
{t("title")}
10 |
11 |
12 | );
13 | }
14 |
15 | export default NotFound;
16 |
--------------------------------------------------------------------------------
/src/app/[locale]/page.tsx:
--------------------------------------------------------------------------------
1 | import FAQ from "~/components/sections/faq";
2 | import Features from "~/components/sections/features";
3 | import Hero from "~/components/sections/hero";
4 | import OpenSource from "~/components/sections/open-source";
5 | import Pricing from "~/components/sections/pricing";
6 | import Testimonials from "~/components/sections/testimonials";
7 |
8 | export default async function Home() {
9 | return (
10 | <>
11 |
12 |
13 |
14 |
15 |
16 |
17 | >
18 | );
19 | }
20 |
--------------------------------------------------------------------------------
/src/app/api/auth/login/github/callback/route.ts:
--------------------------------------------------------------------------------
1 | import { PrismaClientKnownRequestError } from "@prisma/client/runtime/library";
2 | import { ArcticFetchError, OAuth2RequestError } from "arctic";
3 | import { revalidatePath } from "next/cache";
4 | import { cookies } from "next/headers";
5 | import { sendWelcomeEmail } from "~/lib/server/mail";
6 | import { setSessionTokenCookie } from "~/lib/server/auth/cookies";
7 | import { github } from "~/lib/server/auth/github";
8 | import { createSession, generateSessionToken } from "~/lib/server/auth/session";
9 | import { prisma } from "~/lib/server/db";
10 |
11 | export const GET = async (request: Request) => {
12 | const url = new URL(request.url);
13 | const code = url.searchParams.get("code");
14 | const state = url.searchParams.get("state");
15 | const cookieStore = await cookies();
16 | const storedState = cookieStore.get("github_oauth_state")?.value ?? null;
17 | if (!code || !state || !storedState || state !== storedState) {
18 | return new Response(null, {
19 | status: 400,
20 | });
21 | }
22 |
23 | try {
24 | const tokens = await github.validateAuthorizationCode(code);
25 | const githubUserResponse = await fetch("https://api.github.com/user", {
26 | headers: {
27 | Authorization: `Bearer ${tokens.accessToken()}`,
28 | },
29 | });
30 | const githubUser: GitHubUser = await githubUserResponse.json();
31 |
32 | if (!githubUser.email) {
33 | const githubEmailsResponse = await fetch(
34 | "https://api.github.com/user/emails",
35 | {
36 | headers: {
37 | Authorization: `Bearer ${tokens.accessToken()}`,
38 | },
39 | }
40 | );
41 | const githubEmails: {
42 | email: string;
43 | primary: boolean;
44 | verified: boolean;
45 | }[] = await githubEmailsResponse.json();
46 | const verifiedEmail = githubEmails.find(
47 | (email) => email.primary && email.verified
48 | );
49 | if (verifiedEmail) githubUser.email = verifiedEmail.email;
50 | }
51 |
52 | const existingUser = await prisma.user.findFirst({
53 | where: {
54 | OR: [
55 | {
56 | githubId: githubUser.id,
57 | },
58 | {
59 | email: githubUser.email,
60 | },
61 | ],
62 | },
63 | });
64 |
65 | if (existingUser) {
66 | const sessionTokenCookie = generateSessionToken();
67 | const session = await createSession(sessionTokenCookie, existingUser.id);
68 | await setSessionTokenCookie(sessionTokenCookie, session.expiresAt);
69 | revalidatePath("/dashboard", "layout");
70 | return new Response(null, {
71 | status: 302,
72 | headers: {
73 | Location: "/dashboard",
74 | },
75 | });
76 | }
77 |
78 | const newUser = await prisma.user.create({
79 | data: {
80 | githubId: githubUser.id,
81 | name: githubUser.name,
82 | email: githubUser.email,
83 | picture: githubUser.avatar_url,
84 | emailVerified: Boolean(githubUser.email),
85 | },
86 | });
87 |
88 | if (githubUser.email) {
89 | sendWelcomeEmail({ toMail: newUser.email!, userName: newUser.name! });
90 | }
91 | const sessionTokenCookie = generateSessionToken();
92 | const session = await createSession(sessionTokenCookie, newUser.id);
93 | await setSessionTokenCookie(sessionTokenCookie, session.expiresAt);
94 | revalidatePath("/dashboard", "layout");
95 | return new Response(null, {
96 | status: 302,
97 | headers: {
98 | Location: "/dashboard",
99 | },
100 | });
101 | } catch (e) {
102 | console.log(JSON.stringify(e));
103 |
104 | // the specific error message depends on the provider
105 | if (e instanceof OAuth2RequestError) {
106 | // invalid code
107 | return new Response(e.description, {
108 | status: 400,
109 | });
110 | }
111 |
112 | if (e instanceof ArcticFetchError) {
113 | // invalid code
114 | return new Response(e.message, {
115 | status: 400,
116 | });
117 | }
118 |
119 | if (e instanceof PrismaClientKnownRequestError) {
120 | return new Response(e.message, {
121 | status: 400,
122 | });
123 | }
124 |
125 | return new Response("Internal Server Error", {
126 | status: 500,
127 | });
128 | }
129 | };
130 |
131 | interface GitHubUser {
132 | id: number;
133 | name: string;
134 | email: string;
135 | avatar_url: string;
136 | }
137 |
--------------------------------------------------------------------------------
/src/app/api/auth/login/github/route.ts:
--------------------------------------------------------------------------------
1 | import { generateState } from "arctic";
2 | import { cookies } from "next/headers";
3 | import { github } from "~/lib/server/auth/github";
4 |
5 | export const GET = async () => {
6 | const state = generateState();
7 | const url = github.createAuthorizationURL(state, ["read:user", "user:email"]);
8 |
9 | const cookieStore = await cookies();
10 | cookieStore.set("github_oauth_state", state, {
11 | path: "/",
12 | secure: process.env.NODE_ENV === "production",
13 | httpOnly: true,
14 | maxAge: 60 * 10,
15 | sameSite: "lax",
16 | });
17 |
18 | return Response.redirect(url.toString());
19 | };
20 |
--------------------------------------------------------------------------------
/src/app/api/auth/login/send-otp/route.ts:
--------------------------------------------------------------------------------
1 | import { generateEmailVerificationCode } from "~/lib/server/auth";
2 | import { prisma } from "~/lib/server/db";
3 | import { sendOTP } from "~/lib/server/mail";
4 |
5 | export const POST = async (req: Request) => {
6 | const body = await req.json();
7 |
8 | try {
9 | const user = await prisma.user.upsert({
10 | where: {
11 | email: body.email,
12 | },
13 | update: {},
14 | create: {
15 | email: body.email,
16 | emailVerified: false,
17 | },
18 | });
19 |
20 | const otp = await generateEmailVerificationCode(user.id, body.email);
21 | await sendOTP({
22 | toMail: body.email,
23 | code: otp,
24 | userName: user.name?.split(" ")[0] || "",
25 | });
26 |
27 | return new Response(null, {
28 | status: 200,
29 | });
30 | } catch (error) {
31 | console.log(error);
32 |
33 | return new Response(null, {
34 | status: 500,
35 | });
36 | }
37 | };
38 |
--------------------------------------------------------------------------------
/src/app/api/auth/login/verify-otp/route.ts:
--------------------------------------------------------------------------------
1 | import { revalidatePath } from "next/cache";
2 | import { verifyVerificationCode } from "~/lib/server/auth";
3 | import { setSessionTokenCookie } from "~/lib/server/auth/cookies";
4 | import {
5 | createSession,
6 | generateSessionToken,
7 | invalidateAllSessions,
8 | } from "~/lib/server/auth/session";
9 | import { prisma } from "~/lib/server/db";
10 |
11 | export const POST = async (req: Request, response: Response) => {
12 | const body = await req.json();
13 |
14 | try {
15 | const user = await prisma.user.findFirst({
16 | where: {
17 | email: body.email,
18 | },
19 | select: {
20 | id: true,
21 | email: true,
22 | emailVerified: true,
23 | },
24 | });
25 |
26 | if (!user) {
27 | return new Response("User not found", {
28 | status: 400,
29 | });
30 | }
31 |
32 | const isValid = await verifyVerificationCode(
33 | { id: user.id, email: user.email! },
34 | body.code
35 | );
36 |
37 | if (!isValid) {
38 | return new Response("Invalid OTP", {
39 | status: 400,
40 | });
41 | }
42 |
43 | await invalidateAllSessions(user.id);
44 |
45 | if (!user.emailVerified) {
46 | await prisma.user.update({
47 | where: {
48 | id: user.id,
49 | },
50 | data: {
51 | emailVerified: true,
52 | },
53 | });
54 | }
55 | const sessionToken = generateSessionToken();
56 | const session = await createSession(sessionToken, user.id);
57 | await setSessionTokenCookie(sessionToken, session.expiresAt);
58 | revalidatePath("/", "layout");
59 | return new Response(null, {
60 | status: 200,
61 | });
62 | } catch (error) {
63 | return new Response("Internal Server Error", {
64 | status: 500,
65 | });
66 | }
67 | };
68 |
--------------------------------------------------------------------------------
/src/app/api/og/route.ts:
--------------------------------------------------------------------------------
1 | import { ImageResponse } from "next/og";
2 | import { RenderIMGEl } from "~/components/OGImgEl";
3 | import { siteUrl } from "~/config/site";
4 | import Logo from "public/chad-next.png";
5 | import homepageImage from "public/chadnext-homepage.png";
6 |
7 | export const runtime = "edge";
8 |
9 | export async function GET(request: Request) {
10 | const { searchParams } = new URL(request.url);
11 | const hasLocale = searchParams.has("locale");
12 | const locale = hasLocale ? searchParams.get("locale") : "";
13 |
14 | try {
15 | return new ImageResponse(
16 | RenderIMGEl({
17 | logo: siteUrl + Logo.src,
18 | locale: locale as string,
19 | image: siteUrl + homepageImage.src,
20 | }),
21 | {
22 | width: 1200,
23 | height: 630,
24 | }
25 | );
26 | } catch (e) {
27 | console.log(e);
28 | return new Response(`Failed to generate the image`, {
29 | status: 500,
30 | });
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/app/api/stripe/route.ts:
--------------------------------------------------------------------------------
1 | import { revalidatePath } from "next/cache";
2 | import { type NextRequest } from "next/server";
3 | import { z } from "zod";
4 | import { siteConfig } from "~/config/site";
5 | import { proPlan } from "~/config/subscription";
6 | import { getCurrentSession } from "~/lib/server/auth/session";
7 | import { getUserSubscriptionPlan, stripe } from "~/lib/server/payment";
8 |
9 | export async function GET(req: NextRequest) {
10 | const locale = req.cookies.get("Next-Locale")?.value || "en";
11 |
12 | const billingUrl = siteConfig(locale).url + "/dashboard/billing/";
13 | try {
14 | const { user, session } = await getCurrentSession();
15 |
16 | if (!session) {
17 | return new Response("Unauthorized", { status: 401 });
18 | }
19 |
20 | const subscriptionPlan = await getUserSubscriptionPlan(user.id);
21 |
22 | // The user is on the pro plan.
23 | // Create a portal session to manage subscription.
24 | if (subscriptionPlan.isPro && subscriptionPlan.stripeCustomerId) {
25 | const stripeSession = await stripe.billingPortal.sessions.create({
26 | customer: subscriptionPlan.stripeCustomerId,
27 | return_url: billingUrl,
28 | });
29 |
30 | return Response.json({ url: stripeSession.url });
31 | }
32 |
33 | // The user is on the free plan.
34 | // Create a checkout session to upgrade.
35 | const stripeSession = await stripe.checkout.sessions.create({
36 | success_url: billingUrl,
37 | cancel_url: billingUrl,
38 | payment_method_types: ["card"],
39 | mode: "subscription",
40 | customer_email: user.email!,
41 | line_items: [
42 | {
43 | price: proPlan.stripePriceId,
44 | quantity: 1,
45 | },
46 | ],
47 | metadata: {
48 | userId: user.id,
49 | },
50 | });
51 | revalidatePath(`/dashboard/billing`);
52 | return new Response(JSON.stringify({ url: stripeSession.url }));
53 | } catch (error) {
54 | if (error instanceof z.ZodError) {
55 | return new Response(JSON.stringify(error.issues), { status: 422 });
56 | }
57 |
58 | return new Response(null, { status: 500 });
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/src/app/api/uploadthing/core.ts:
--------------------------------------------------------------------------------
1 | import { createUploadthing, type FileRouter } from "uploadthing/next";
2 | import { UploadThingError } from "uploadthing/server";
3 | import { getCurrentSession } from "~/lib/server/auth/session";
4 |
5 | const f = createUploadthing();
6 |
7 | // FileRouter for your app, can contain multiple FileRoutes
8 | export const ourFileRouter = {
9 | // Define as many FileRoutes as you like, each with a unique routeSlug
10 | imageUploader: f({ image: { maxFileSize: "4MB", maxFileCount: 1 } })
11 | // Set permissions and file types for this FileRoute
12 | .middleware(async ({ req }) => {
13 | // This code runs on your server before upload
14 | const { user, session } = await getCurrentSession();
15 |
16 | // If you throw, the user will not be able to upload
17 | if (!session) throw new UploadThingError("Unauthorized");
18 |
19 | // Whatever is returned here is accessible in onUploadComplete as `metadata`
20 | return { userId: user.id };
21 | })
22 | .onUploadComplete(async ({ metadata, file }) => {
23 | // This code RUNS ON YOUR SERVER after upload
24 | console.log("Upload complete for userId:", metadata.userId);
25 |
26 | console.log("file url", file.url);
27 |
28 | // !!! Whatever is returned here is sent to the clientside `onClientUploadComplete` callback
29 | return { url: file.url };
30 | }),
31 | } satisfies FileRouter;
32 |
33 | export type OurFileRouter = typeof ourFileRouter;
34 |
--------------------------------------------------------------------------------
/src/app/api/uploadthing/route.ts:
--------------------------------------------------------------------------------
1 | import { createRouteHandler } from "uploadthing/next";
2 | import { ourFileRouter } from "./core";
3 |
4 | export const runtime = "nodejs";
5 |
6 | // Export routes for Next App Router
7 | export const { GET, POST } = createRouteHandler({
8 | router: ourFileRouter,
9 | });
10 |
--------------------------------------------------------------------------------
/src/app/api/webhooks/stripe/route.ts:
--------------------------------------------------------------------------------
1 | import { type NextRequest } from "next/server";
2 | import { buffer } from "node:stream/consumers";
3 | import type Stripe from "stripe";
4 | import { prisma } from "~/lib/server/db";
5 | import { stripe } from "~/lib/server/payment";
6 |
7 | export async function POST(req: NextRequest) {
8 | //@ts-expect-error Argument of type 'ReadableStream' is not assignable to parameter of type 'ReadableStream | Readable | AsyncIterable'
9 | const body = await buffer(req.body);
10 | const headers = req.headers;
11 | const signature = headers.get("Stripe-Signature") as string;
12 |
13 | let event: Stripe.Event | undefined = undefined;
14 | try {
15 | event = stripe.webhooks.constructEvent(
16 | body,
17 | signature,
18 | process.env.STRIPE_WEBHOOK_SECRET as string
19 | );
20 | } catch (error) {
21 | if (error instanceof Error) {
22 | return new Response(`Webhook Error: ${error.message}`, { status: 400 });
23 | }
24 | }
25 |
26 | const eventData = event?.data.object as
27 | | Stripe.Checkout.Session
28 | | Stripe.Invoice;
29 |
30 | if (
31 | event?.type === "checkout.session.completed" ||
32 | event?.type === "checkout.session.async_payment_succeeded"
33 | ) {
34 | const session = eventData as Stripe.Checkout.Session;
35 | // Retrieve the subscription details from Stripe.
36 | const subscription = await stripe.subscriptions.retrieve(
37 | session.subscription as string
38 | );
39 |
40 | // Update the user stripe into in our database.
41 | // Since this is the initial subscription, we need to update
42 | // the subscription id and customer id.
43 | try {
44 | await prisma.user.update({
45 | where: {
46 | id: session?.metadata?.userId,
47 | },
48 | data: {
49 | stripeSubscriptionId: subscription.id,
50 | stripeCustomerId: subscription.customer as string,
51 | stripePriceId: subscription.items.data[0].price.id,
52 | stripeCurrentPeriodEnd: new Date(
53 | subscription.items.data[0].current_period_end! * 1000
54 | ),
55 | },
56 | });
57 | } catch (error) {
58 | console.error(error);
59 | }
60 | } else if (event?.type === "invoice.payment_succeeded") {
61 | const invoice = eventData as Stripe.Invoice;
62 |
63 | const subscriptionId = invoice.parent?.subscription_details?.subscription;
64 |
65 | // Retrieve the subscription details from Stripe.
66 | const subscription = await stripe.subscriptions.retrieve(
67 | subscriptionId as string
68 | );
69 |
70 | // Update the price id and set the new period end.
71 | try {
72 | await prisma.user.update({
73 | where: {
74 | stripeSubscriptionId: subscription.id,
75 | },
76 | data: {
77 | stripePriceId: subscription.items.data[0].price.id,
78 | stripeCurrentPeriodEnd: new Date(
79 | subscription.items.data[0].current_period_end! * 1000
80 | ),
81 | },
82 | });
83 | } catch (error) {
84 | console.error(error);
85 | }
86 | }
87 |
88 | return new Response(null, { status: 200 });
89 | }
90 |
--------------------------------------------------------------------------------
/src/app/global-error.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useEffect } from "react";
4 | import { Button } from "~/components/ui/button";
5 |
6 | export default function GlobalError({
7 | error,
8 | reset,
9 | }: {
10 | error: Error;
11 | reset: () => void;
12 | }) {
13 | useEffect(() => {
14 | // Log the error to an error reporting service
15 | console.log(error);
16 | }, [error]);
17 |
18 | return (
19 |
20 |
21 | Oops, Something Went Wrong!
22 |
23 |
24 |
25 | );
26 | }
27 |
--------------------------------------------------------------------------------
/src/app/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | @layer base {
6 | :root {
7 | --background: 0 0% 100%;
8 | --foreground: 222.2 84% 4.9%;
9 |
10 | --muted: 210 40% 96.1%;
11 | --muted-foreground: 215.4 16.3% 46.9%;
12 |
13 | --popover: 0 0% 100%;
14 | --popover-foreground: 222.2 84% 4.9%;
15 |
16 | --card: 0 0% 100%;
17 | --card-foreground: 222.2 84% 4.9%;
18 |
19 | --border: 214.3 31.8% 91.4%;
20 | --input: 214.3 31.8% 91.4%;
21 |
22 | --primary: 222.2 47.4% 11.2%;
23 | --primary-foreground: 210 40% 98%;
24 |
25 | --secondary: 210 40% 96.1%;
26 | --secondary-foreground: 222.2 47.4% 11.2%;
27 |
28 | --accent: 210 40% 96.1%;
29 | --accent-foreground: 222.2 47.4% 11.2%;
30 |
31 | --destructive: 0 84.2% 60.2%;
32 | --destructive-foreground: 210 40% 98%;
33 |
34 | --ring: 222.2 84% 4.9%;
35 |
36 | --radius: 0.5rem;
37 |
38 | --chart-1: 12 76% 61%;
39 |
40 | --chart-2: 173 58% 39%;
41 |
42 | --chart-3: 197 37% 24%;
43 |
44 | --chart-4: 43 74% 66%;
45 |
46 | --chart-5: 27 87% 67%;
47 |
48 | --sidebar-background: 0 0% 98%;
49 |
50 | --sidebar-foreground: 240 5.3% 26.1%;
51 |
52 | --sidebar-primary: 240 5.9% 10%;
53 |
54 | --sidebar-primary-foreground: 0 0% 98%;
55 |
56 | --sidebar-accent: 240 4.8% 95.9%;
57 |
58 | --sidebar-accent-foreground: 240 5.9% 10%;
59 |
60 | --sidebar-border: 220 13% 91%;
61 |
62 | --sidebar-ring: 217.2 91.2% 59.8%;
63 | }
64 |
65 | .dark {
66 | --background: 222.2 84% 4.9%;
67 | --foreground: 210 40% 98%;
68 |
69 | --muted: 217.2 32.6% 17.5%;
70 | --muted-foreground: 215 20.2% 65.1%;
71 |
72 | --popover: 222.2 84% 4.9%;
73 | --popover-foreground: 210 40% 98%;
74 |
75 | --card: 222.2 84% 4.9%;
76 | --card-foreground: 210 40% 98%;
77 |
78 | --border: 217.2 32.6% 17.5%;
79 | --input: 217.2 32.6% 17.5%;
80 |
81 | --primary: 210 40% 98%;
82 | --primary-foreground: 222.2 47.4% 11.2%;
83 |
84 | --secondary: 217.2 32.6% 17.5%;
85 | --secondary-foreground: 210 40% 98%;
86 |
87 | --accent: 217.2 32.6% 17.5%;
88 | --accent-foreground: 210 40% 98%;
89 |
90 | --destructive: 0 62.8% 30.6%;
91 | --destructive-foreground: 210 40% 98%;
92 |
93 | --ring: 212.7 26.8% 83.9%;
94 |
95 | --chart-1: 220 70% 50%;
96 |
97 | --chart-2: 160 60% 45%;
98 |
99 | --chart-3: 30 80% 55%;
100 |
101 | --chart-4: 280 65% 60%;
102 |
103 | --chart-5: 340 75% 55%;
104 |
105 | --sidebar-background: 240 5.9% 10%;
106 |
107 | --sidebar-foreground: 240 4.8% 95.9%;
108 |
109 | --sidebar-primary: 224.3 76.3% 48%;
110 |
111 | --sidebar-primary-foreground: 0 0% 100%;
112 |
113 | --sidebar-accent: 240 3.7% 15.9%;
114 |
115 | --sidebar-accent-foreground: 240 4.8% 95.9%;
116 |
117 | --sidebar-border: 240 3.7% 15.9%;
118 |
119 | --sidebar-ring: 217.2 91.2% 59.8%;
120 | }
121 | }
122 |
123 | @layer base {
124 | * {
125 | @apply border-border;
126 | }
127 | body {
128 | @apply bg-background text-foreground;
129 | }
130 | }
131 |
--------------------------------------------------------------------------------
/src/app/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ChadNext",
3 | "short_name": "chadnext",
4 | "icons": [
5 | {
6 | "src": "/android-chrome-192x192.png",
7 | "sizes": "192x192",
8 | "type": "image/png",
9 | "purpose": "maskable"
10 | },
11 | {
12 | "src": "/android-chrome-512x512.png",
13 | "sizes": "512x512",
14 | "type": "image/png"
15 | }
16 | ],
17 | "theme_color": "#ffffff",
18 | "background_color": "#ffffff",
19 | "display": "standalone",
20 | "start_url": "/",
21 | "orientation": "portrait"
22 | }
--------------------------------------------------------------------------------
/src/app/robots.ts:
--------------------------------------------------------------------------------
1 | import { type MetadataRoute } from "next";
2 | import { siteUrl } from "~/config/site";
3 |
4 | export default function robots(): MetadataRoute.Robots {
5 | return {
6 | rules: [
7 | {
8 | userAgent: "*",
9 | allow: "/",
10 | disallow: ["/api/", `/dashboard`],
11 | },
12 | ],
13 | sitemap: `${siteUrl}/sitemap.xml`,
14 | };
15 | }
16 |
--------------------------------------------------------------------------------
/src/app/sitemap.ts:
--------------------------------------------------------------------------------
1 | import { type MetadataRoute } from "next";
2 | import { siteUrl } from "~/config/site";
3 |
4 | export default function sitemap(): MetadataRoute.Sitemap {
5 | return [
6 | {
7 | url: siteUrl,
8 | lastModified: new Date(),
9 | changeFrequency: "weekly",
10 | priority: 1,
11 | alternates: {
12 | languages: {
13 | en: `${siteUrl}/en`,
14 | fr: `${siteUrl}/fr`,
15 | },
16 | },
17 | },
18 | {
19 | url: `${siteUrl}/login`,
20 | lastModified: new Date(),
21 | changeFrequency: "weekly",
22 | priority: 0.5,
23 | alternates: {
24 | languages: {
25 | en: `${siteUrl}/en/login`,
26 | fr: `${siteUrl}/fr/login`,
27 | },
28 | },
29 | },
30 | {
31 | url: `${siteUrl}/about`,
32 | lastModified: new Date(),
33 | changeFrequency: "weekly",
34 | priority: 0.5,
35 | alternates: {
36 | languages: {
37 | en: `${siteUrl}/en/about`,
38 | fr: `${siteUrl}/fr/about`,
39 | },
40 | },
41 | },
42 | {
43 | url: `${siteUrl}/changelog`,
44 | lastModified: new Date(),
45 | changeFrequency: "weekly",
46 | priority: 0.5,
47 | alternates: {
48 | languages: {
49 | en: `${siteUrl}/en/changelog`,
50 | fr: `${siteUrl}/fr/changelog`,
51 | },
52 | },
53 | },
54 | ];
55 | }
56 |
--------------------------------------------------------------------------------
/src/app/sw.ts:
--------------------------------------------------------------------------------
1 | import { defaultCache } from "@serwist/next/worker";
2 | import type { PrecacheEntry, SerwistGlobalConfig } from "serwist";
3 | import { Serwist } from "serwist";
4 |
5 | // This declares the value of `injectionPoint` to TypeScript.
6 | // `injectionPoint` is the string that will be replaced by the
7 | // actual precache manifest. By default, this string is set to
8 | // `"self.__SW_MANIFEST"`.
9 | declare global {
10 | interface WorkerGlobalScope extends SerwistGlobalConfig {
11 | __SW_MANIFEST: (PrecacheEntry | string)[] | undefined;
12 | }
13 | }
14 |
15 | declare const self: ServiceWorkerGlobalScope;
16 |
17 | const serwist = new Serwist({
18 | precacheEntries: self.__SW_MANIFEST,
19 | skipWaiting: true,
20 | clientsClaim: true,
21 | navigationPreload: true,
22 | runtimeCaching: defaultCache,
23 | });
24 |
25 | serwist.addEventListeners();
26 |
--------------------------------------------------------------------------------
/src/assets/fonts/CalSans-SemiBold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/moinulmoin/chadnext/6860d82ee9ecade5c32d8806f7c2b5683b7bb725/src/assets/fonts/CalSans-SemiBold.ttf
--------------------------------------------------------------------------------
/src/assets/fonts/CalSans-SemiBold.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/moinulmoin/chadnext/6860d82ee9ecade5c32d8806f7c2b5683b7bb725/src/assets/fonts/CalSans-SemiBold.woff
--------------------------------------------------------------------------------
/src/assets/fonts/CalSans-SemiBold.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/moinulmoin/chadnext/6860d82ee9ecade5c32d8806f7c2b5683b7bb725/src/assets/fonts/CalSans-SemiBold.woff2
--------------------------------------------------------------------------------
/src/components/OGImgEl.tsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable @next/next/no-img-element */
2 |
3 | export const RenderIMGEl = ({
4 | logo,
5 | image,
6 | locale,
7 | }: {
8 | logo: string;
9 | locale: string;
10 | image: string;
11 | }) => {
12 | return (
13 |
14 |
15 |

16 |
ChadNext
17 |
22 | {locale ? "/" + locale : ""}
23 |
24 |
25 |

26 |
27 | );
28 | };
29 |
--------------------------------------------------------------------------------
/src/components/billing-form.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useState } from "react";
4 | import Icons from "~/components/shared/icons";
5 | import { Button } from "~/components/ui/button";
6 | import {
7 | Card,
8 | CardContent,
9 | CardDescription,
10 | CardFooter,
11 | CardHeader,
12 | CardTitle,
13 | } from "~/components/ui/card";
14 | import { toast } from "~/hooks/use-toast";
15 | import { cn, formatDate } from "~/lib/utils";
16 | import { type UserSubscriptionPlan } from "~/types";
17 | interface BillingFormProps extends React.HTMLAttributes {
18 | subscriptionPlan: UserSubscriptionPlan & {
19 | isCanceled: boolean;
20 | };
21 | }
22 |
23 | export function BillingForm({
24 | subscriptionPlan,
25 | className,
26 | ...props
27 | }: BillingFormProps) {
28 | const [isLoading, setIsLoading] = useState(false);
29 |
30 | async function onSubmit(event: React.FormEvent) {
31 | event.preventDefault();
32 |
33 | setIsLoading(true);
34 |
35 | // Get a Stripe session URL.
36 | const response = await fetch("/api/stripe");
37 |
38 | if (!response?.ok) {
39 | return toast({
40 | title: "Something went wrong.",
41 | description: "Please refresh the page and try again.",
42 | variant: "destructive",
43 | });
44 | }
45 |
46 | // Redirect to the Stripe session.
47 | // This could be a checkout page for initial upgrade.
48 | // Or portal to manage existing subscription.
49 | const data = await response.json();
50 | if (data.url) {
51 | window.location.href = data.url;
52 | }
53 |
54 | setIsLoading(false);
55 | }
56 |
57 | return (
58 |
87 | );
88 | }
89 |
--------------------------------------------------------------------------------
/src/components/copy-button.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import Icons from "./shared/icons";
4 | import { Button } from "./ui/button";
5 | import { toast } from "~/hooks/use-toast";
6 |
7 | export default function CopyButton({ content }: { content: string }) {
8 | const copyToClipboard = (content: string) => {
9 | if (!navigator.clipboard) {
10 | toast({
11 | title: "Error copying!",
12 | description: "Please try again.",
13 | variant: "destructive",
14 | });
15 | }
16 | navigator.clipboard.writeText(content);
17 | toast({
18 | title: "Project ID copied!",
19 | });
20 | };
21 |
22 | return (
23 |
33 | );
34 | }
35 |
--------------------------------------------------------------------------------
/src/components/go-back.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import { useRouter } from "next/navigation";
3 | import Icons from "./shared/icons";
4 | import { Button } from "./ui/button";
5 |
6 | export default function GoBack() {
7 | const router = useRouter();
8 | return (
9 |
18 | );
19 | }
20 |
--------------------------------------------------------------------------------
/src/components/layout/cancel-confirm-modal.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | AlertDialog,
3 | AlertDialogAction,
4 | AlertDialogCancel,
5 | AlertDialogContent,
6 | AlertDialogFooter,
7 | AlertDialogHeader,
8 | AlertDialogTitle,
9 | AlertDialogTrigger,
10 | } from "~/components/ui/alert-dialog";
11 | import { Button } from "~/components/ui/button";
12 |
13 | interface CancelConfirmModalProps {
14 | reset: () => void;
15 | isDisabled: boolean;
16 | }
17 |
18 | export default function CancelConfirmModal({
19 | reset,
20 | isDisabled,
21 | }: CancelConfirmModalProps) {
22 | return (
23 |
24 |
25 |
28 |
29 |
30 |
31 |
32 | Are you sure to discard the changes?
33 |
34 |
35 |
36 | Yes
37 | No
38 |
39 |
40 |
41 | );
42 | }
43 |
--------------------------------------------------------------------------------
/src/components/layout/footer.tsx:
--------------------------------------------------------------------------------
1 | import Image from "next/image";
2 | import Link from "next/link";
3 | import { Suspense } from "react";
4 | import { siteConfig } from "~/config/site";
5 | import LocaleToggler from "../shared/locale-toggler";
6 | import ThemeToggle from "../shared/theme-toggle";
7 |
8 | export default function Footer() {
9 | return (
10 |
41 | );
42 | }
43 |
--------------------------------------------------------------------------------
/src/components/layout/header/index.tsx:
--------------------------------------------------------------------------------
1 | import { getCurrentSession } from "~/lib/server/auth/session";
2 | import { getScopedI18n } from "~/locales/server";
3 | import Navbar from "./navbar";
4 |
5 | export default async function Header() {
6 | const { session } = await getCurrentSession();
7 | const scopedT = await getScopedI18n("header");
8 | const headerText = {
9 | changelog: scopedT("changelog"),
10 | about: scopedT("about"),
11 | login: scopedT("login"),
12 | dashboard: scopedT("dashboard"),
13 | };
14 |
15 | return (
16 |
21 | );
22 | }
23 |
--------------------------------------------------------------------------------
/src/components/layout/header/navbar.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { Session } from "@prisma/client";
4 | import { MenuIcon } from "lucide-react";
5 | import Image from "next/image";
6 | import Link from "next/link";
7 | import { useState } from "react";
8 | import LogoutButton from "~/components/shared/logout-button";
9 | import { buttonVariants } from "~/components/ui/button";
10 | import { Sheet, SheetContent, SheetTrigger } from "~/components/ui/sheet";
11 | import { cn } from "~/lib/utils";
12 | export default function Navbar({
13 | session,
14 | headerText,
15 | }: {
16 | session: Session;
17 | headerText: {
18 | changelog: string;
19 | about: string;
20 | login: string;
21 | dashboard: string;
22 | [key: string]: string;
23 | };
24 | }) {
25 | const [isModalOpen, setIsModalOpen] = useState(false);
26 | return (
27 |
119 | );
120 | }
121 |
--------------------------------------------------------------------------------
/src/components/layout/login-modal.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { usePathname, useRouter } from "next/navigation";
4 | import {
5 | Dialog,
6 | DialogContent,
7 | DialogHeader,
8 | DialogTitle,
9 | } from "~/components/ui/dialog";
10 | import AuthForm from "./auth-form";
11 |
12 | export default function LoginModal() {
13 | const router = useRouter();
14 | const pathname = usePathname();
15 |
16 | const IsOpen = pathname.includes("/login");
17 | return (
18 |
30 | );
31 | }
32 |
--------------------------------------------------------------------------------
/src/components/layout/sidebar-nav.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import Link from "next/link";
4 | import { usePathname } from "next/navigation";
5 | import { buttonVariants } from "~/components/ui/button";
6 | import { cn } from "~/lib/utils";
7 | import Icons from "../shared/icons";
8 | import LogoutButton from "../shared/logout-button";
9 |
10 | const navItems = [
11 | {
12 | title: "Projects",
13 | href: "/dashboard/projects",
14 | icon: Icons.projectPlus,
15 | },
16 | {
17 | title: "Billing",
18 | href: "/dashboard/billing",
19 | icon: Icons.billing,
20 | },
21 | {
22 | title: "Settings",
23 | href: "/dashboard/settings",
24 | icon: Icons.settings,
25 | },
26 | ];
27 |
28 | interface SidebarNavProps extends React.HTMLAttributes {
29 | className?: string;
30 | }
31 |
32 | export default function SidebarNav({ className, ...props }: SidebarNavProps) {
33 | const pathname = usePathname();
34 | const isActive = (href: string) => pathname === href;
35 | return (
36 |
58 | );
59 | }
60 |
--------------------------------------------------------------------------------
/src/components/sections/cta.tsx:
--------------------------------------------------------------------------------
1 | import { Github } from "lucide-react";
2 | import Link from "next/link";
3 | import { buttonVariants } from "~/components/ui/button";
4 | import { siteConfig } from "~/config/site"; // Assuming you have site config for URLs
5 | import { cn } from "~/lib/utils";
6 |
7 | export default function CTA() {
8 | return (
9 |
10 |
11 | Ready to Get Started?
12 |
13 |
14 | Join thousands of developers building faster with our template. Sign up
15 | today or star us on GitHub!
16 |
17 |
18 |
19 | Sign Up Now
20 |
21 |
27 |
28 | Star on GitHub
29 |
30 |
31 |
32 | );
33 | }
34 |
--------------------------------------------------------------------------------
/src/components/sections/faq.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Accordion,
3 | AccordionContent,
4 | AccordionItem,
5 | AccordionTrigger,
6 | } from "~/components/ui/accordion";
7 |
8 | interface FAQItem {
9 | question: string;
10 | answer: string;
11 | }
12 |
13 | // Placeholder data - replace with actual FAQs
14 | const faqItems: FAQItem[] = [
15 | {
16 | question: "Is it easy to integrate?",
17 | answer:
18 | "Yes, integration is straightforward with our comprehensive documentation.",
19 | },
20 | {
21 | question: "What is the pricing model?",
22 | answer:
23 | "We offer various tiers, including a free plan. Check our pricing section for details.",
24 | },
25 | {
26 | question: "Can I customize the appearance?",
27 | answer:
28 | "Absolutely! The components are built with Tailwind CSS, making customization easy.",
29 | },
30 | {
31 | question: "Do you offer support?",
32 | answer:
33 | "Yes, we provide email support for all plans and priority support for premium tiers.",
34 | },
35 | ];
36 |
37 | export default function FAQ() {
38 | return (
39 |
40 |
41 | Frequently Asked Questions
42 |
43 |
44 | {faqItems.map((item, index) => (
45 |
46 | {item.question}
47 | {item.answer}
48 |
49 | ))}
50 |
51 |
52 | );
53 | }
54 |
--------------------------------------------------------------------------------
/src/components/sections/features.tsx:
--------------------------------------------------------------------------------
1 | import { LanguagesIcon } from "lucide-react";
2 | import { BrandIcons } from "../shared/brand-icons";
3 | import { Card } from "../ui/card";
4 | import { getScopedI18n } from "~/locales/server";
5 |
6 | export default async function Features() {
7 | const scopedT = await getScopedI18n("features");
8 | const scopedTlibs = await getScopedI18n("features.libs");
9 |
10 | return (
11 |
12 |
13 |
14 |
15 | {scopedT("top")}
16 |
17 |
18 | {scopedT("details")}
19 |
20 |
21 |
22 |
23 |
24 |
25 | {scopedTlibs("nextjs")}
26 |
27 |
28 |
29 |
30 |
31 |
32 | {scopedTlibs("tailwindcss")}
33 |
34 |
35 |
36 |
37 |
38 |
39 | {scopedTlibs("postgres")}
40 |
41 |
42 |
43 |
44 |
45 |
46 | {scopedTlibs("lucia")}
47 |
48 |
49 |
50 |
51 |
52 |
53 | {scopedTlibs("uploadthing")}
54 |
55 |
56 |
57 |
58 |
59 |
60 | {scopedTlibs("reactEmail")}
61 |
62 |
63 |
64 |
65 |
66 |
67 | {scopedTlibs("internationalization")}
68 |
69 |
70 |
71 |
72 |
73 |
74 | {scopedTlibs("stripe")}
75 |
76 |
77 |
78 |
79 |
80 |
81 | {scopedTlibs("vercel")}
82 |
83 |
84 |
85 |
86 |
87 | {scopedT('aboutMd')}
88 |
94 | Velite
95 | {" "}
96 | and Markdown.
97 |
98 |
99 |
100 |
101 | );
102 | }
103 |
--------------------------------------------------------------------------------
/src/components/sections/hero.tsx:
--------------------------------------------------------------------------------
1 | import { StarIcon } from "lucide-react";
2 | import Link from "next/link";
3 | import { BrandIcons } from "~/components/shared/brand-icons";
4 | import Icons from "~/components/shared/icons";
5 | import { buttonVariants } from "~/components/ui/button";
6 | import { nFormatter } from "~/lib/utils";
7 | import { getScopedI18n } from "~/locales/server";
8 |
9 | export default async function Hero() {
10 | const scopedT = await getScopedI18n("hero");
11 | const { stargazers_count: stars } = await fetch(
12 | "https://api.github.com/repos/moinulmoin/chadnext",
13 | {
14 | next: { revalidate: 3600 },
15 | }
16 | ).then((res) => res.json());
17 |
18 | return (
19 |
20 |
21 |
57 |
58 |
59 | {scopedT("tools")}
60 |
61 |
62 | {tools.map((t, i) => (
63 |
64 |
65 |
66 | ))}
67 |
68 |
69 |
70 |
71 | );
72 | }
73 |
74 | const tools = [
75 | {
76 | link: "https://www.typescriptlang.org/",
77 | icon: BrandIcons.ts,
78 | },
79 | {
80 | link: "https://nextjs.org/",
81 | icon: BrandIcons.nextjs,
82 | },
83 | {
84 | link: "https://tailwindcss.com/",
85 | icon: BrandIcons.tailwind,
86 | },
87 | {
88 | link: "https://www.prisma.io/",
89 | icon: BrandIcons.prisma,
90 | },
91 | {
92 | link: "https://vercel.com/",
93 | icon: BrandIcons.vercel,
94 | },
95 | ];
96 |
--------------------------------------------------------------------------------
/src/components/sections/open-source.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link";
2 | import { siteConfig } from "~/config/site";
3 |
4 | export default function OpenSource() {
5 | return (
6 |
7 |
8 |
9 |
10 | Proudly Open Source
11 |
12 |
13 | ChadNext is open source and powered by open source software. The
14 | code is available on GitHub.
15 |
16 |
22 |
Star me, Onii Chan {`>_<`}
23 |
24 |
25 |
26 |
27 | );
28 | }
29 |
--------------------------------------------------------------------------------
/src/components/sections/testimonials.tsx:
--------------------------------------------------------------------------------
1 | import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card";
2 |
3 | interface Testimonial {
4 | name: string;
5 | title: string;
6 | quote: string;
7 | avatar: string; // URL to avatar image
8 | }
9 |
10 | // Placeholder data - replace with actual testimonials
11 | const testimonials: Testimonial[] = [
12 | {
13 | name: "Jane Doe",
14 | title: "CEO, Example Inc.",
15 | quote: "This product transformed our workflow! Highly recommended.",
16 | avatar: "/images/avatars/placeholder.png", // Replace with actual path
17 | },
18 | {
19 | name: "John Smith",
20 | title: "Developer, Tech Solutions",
21 | quote: "Incredibly easy to set up and use. Saved us countless hours.",
22 | avatar: "/images/avatars/placeholder.png", // Replace with actual path
23 | },
24 | {
25 | name: "Alice Brown",
26 | title: "Marketing Manager, Startup Co.",
27 | quote: "The perfect solution we were looking for. Excellent support too!",
28 | avatar: "/images/avatars/placeholder.png", // Replace with actual path
29 | },
30 | ];
31 |
32 | export default function Testimonials() {
33 | return (
34 |
35 |
36 | What Our Users Say
37 |
38 |
39 | {testimonials.map((testimonial, index) => (
40 |
41 |
42 |
43 | {/* Placeholder for avatar - replace with
*/}
44 |
45 |
46 |
{testimonial.name}
47 |
48 | {testimonial.title}
49 |
50 |
51 |
52 |
53 |
54 | {`"${testimonial.quote}"`}
55 |
56 |
57 | ))}
58 |
59 |
60 | );
61 | }
62 |
--------------------------------------------------------------------------------
/src/components/shared/locale-toggler.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import { CheckIcon, LanguagesIcon } from "lucide-react";
3 | import {
4 | DropdownMenu,
5 | DropdownMenuContent,
6 | DropdownMenuItem,
7 | DropdownMenuShortcut,
8 | DropdownMenuTrigger,
9 | } from "~/components/ui/dropdown-menu";
10 | import { useChangeLocale, useCurrentLocale } from "~/locales/client";
11 | import { Button } from "../ui/button";
12 |
13 | const locales = [
14 | {
15 | name: "English",
16 | value: "en",
17 | },
18 | {
19 | name: "French",
20 | value: "fr",
21 | },
22 | ];
23 |
24 | export default function LocaleToggler() {
25 | const changeLocale = useChangeLocale({ preserveSearchParams: true });
26 | const currentLocale = useCurrentLocale();
27 |
28 | return (
29 |
30 |
31 |
35 |
36 |
37 | {locales.map((locale) => (
38 | changeLocale(locale.value as typeof currentLocale)}
41 | disabled={locale.value === currentLocale}
42 | >
43 | {locale.name}
44 | {locale.value === currentLocale ? (
45 |
46 |
47 |
48 | ) : null}
49 |
50 | ))}
51 |
52 |
53 | );
54 | }
55 |
--------------------------------------------------------------------------------
/src/components/shared/logout-button.tsx:
--------------------------------------------------------------------------------
1 | import { LogOutIcon } from "lucide-react";
2 | import { logout } from "~/app/[locale]/actions";
3 | import { Button } from "../ui/button";
4 |
5 | export default function LogoutButton({ className }: { className?: string }) {
6 | return (
7 |
8 |
18 |
19 | );
20 | }
21 |
--------------------------------------------------------------------------------
/src/components/shared/theme-provider.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { ThemeProvider as NextThemesProvider } from "next-themes";
4 | import { type ThemeProviderProps } from "next-themes";
5 |
6 | export default function ThemeProvider({
7 | children,
8 | ...props
9 | }: ThemeProviderProps) {
10 | return (
11 |
17 | {children}
18 |
19 | );
20 | }
21 |
--------------------------------------------------------------------------------
/src/components/shared/theme-toggle.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useTheme } from "next-themes";
4 | import Icons from "~/components/shared/icons";
5 | import { Button } from "~/components/ui/button";
6 | import {
7 | DropdownMenu,
8 | DropdownMenuContent,
9 | DropdownMenuItem,
10 | DropdownMenuTrigger,
11 | } from "~/components/ui/dropdown-menu";
12 |
13 | export default function ThemeToggle() {
14 | const { setTheme } = useTheme();
15 |
16 | return (
17 |
18 |
19 |
24 |
25 |
26 | setTheme("light")}>
27 |
28 | Light
29 |
30 | setTheme("dark")}>
31 |
32 | Dark
33 |
34 | setTheme("system")}>
35 |
36 | System
37 |
38 |
39 |
40 | );
41 | }
42 |
--------------------------------------------------------------------------------
/src/components/ui/accordion.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import * as AccordionPrimitive from "@radix-ui/react-accordion";
5 | import { ChevronDown } from "lucide-react";
6 |
7 | import { cn } from "~/lib/utils";
8 |
9 | const Accordion = AccordionPrimitive.Root;
10 |
11 | const AccordionItem = React.forwardRef<
12 | React.ElementRef,
13 | React.ComponentPropsWithoutRef
14 | >(({ className, ...props }, ref) => (
15 |
20 | ));
21 | AccordionItem.displayName = "AccordionItem";
22 |
23 | const AccordionTrigger = React.forwardRef<
24 | React.ElementRef,
25 | React.ComponentPropsWithoutRef
26 | >(({ className, children, ...props }, ref) => (
27 |
28 | svg]:rotate-180",
32 | className
33 | )}
34 | {...props}
35 | >
36 | {children}
37 |
38 |
39 |
40 | ));
41 | AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName;
42 |
43 | const AccordionContent = React.forwardRef<
44 | React.ElementRef,
45 | React.ComponentPropsWithoutRef
46 | >(({ className, children, ...props }, ref) => (
47 |
52 | {children}
53 |
54 | ));
55 |
56 | AccordionContent.displayName = AccordionPrimitive.Content.displayName;
57 |
58 | export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };
59 |
--------------------------------------------------------------------------------
/src/components/ui/alert.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { cva, type VariantProps } from "class-variance-authority";
3 |
4 | import { cn } from "~/lib/utils";
5 |
6 | const alertVariants = cva(
7 | "relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground",
8 | {
9 | variants: {
10 | variant: {
11 | default: "bg-background text-foreground",
12 | destructive:
13 | "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
14 | },
15 | },
16 | defaultVariants: {
17 | variant: "default",
18 | },
19 | }
20 | );
21 |
22 | const Alert = React.forwardRef<
23 | HTMLDivElement,
24 | React.HTMLAttributes & VariantProps
25 | >(({ className, variant, ...props }, ref) => (
26 |
32 | ));
33 | Alert.displayName = "Alert";
34 |
35 | const AlertTitle = React.forwardRef<
36 | HTMLParagraphElement,
37 | React.HTMLAttributes
38 | >(({ className, ...props }, ref) => (
39 |
44 | ));
45 | AlertTitle.displayName = "AlertTitle";
46 |
47 | const AlertDescription = React.forwardRef<
48 | HTMLParagraphElement,
49 | React.HTMLAttributes
50 | >(({ className, ...props }, ref) => (
51 |
56 | ));
57 | AlertDescription.displayName = "AlertDescription";
58 |
59 | export { Alert, AlertTitle, AlertDescription };
60 |
--------------------------------------------------------------------------------
/src/components/ui/aspect-ratio.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio";
4 |
5 | const AspectRatio = AspectRatioPrimitive.Root;
6 |
7 | export { AspectRatio };
8 |
--------------------------------------------------------------------------------
/src/components/ui/avatar.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import * as AvatarPrimitive from "@radix-ui/react-avatar";
5 |
6 | import { cn } from "~/lib/utils";
7 |
8 | const Avatar = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, ...props }, ref) => (
12 |
20 | ));
21 | Avatar.displayName = AvatarPrimitive.Root.displayName;
22 |
23 | const AvatarImage = React.forwardRef<
24 | React.ElementRef,
25 | React.ComponentPropsWithoutRef
26 | >(({ className, ...props }, ref) => (
27 |
32 | ));
33 | AvatarImage.displayName = AvatarPrimitive.Image.displayName;
34 |
35 | const AvatarFallback = React.forwardRef<
36 | React.ElementRef,
37 | React.ComponentPropsWithoutRef
38 | >(({ className, ...props }, ref) => (
39 |
47 | ));
48 | AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName;
49 |
50 | export { Avatar, AvatarImage, AvatarFallback };
51 |
--------------------------------------------------------------------------------
/src/components/ui/badge.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { cva, type VariantProps } from "class-variance-authority";
3 |
4 | import { cn } from "~/lib/utils";
5 |
6 | const badgeVariants = cva(
7 | "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
8 | {
9 | variants: {
10 | variant: {
11 | default:
12 | "border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
13 | secondary:
14 | "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
15 | destructive:
16 | "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
17 | outline: "text-foreground",
18 | },
19 | },
20 | defaultVariants: {
21 | variant: "default",
22 | },
23 | }
24 | );
25 |
26 | export interface BadgeProps
27 | extends React.HTMLAttributes,
28 | VariantProps {}
29 |
30 | function Badge({ className, variant, ...props }: BadgeProps) {
31 | return (
32 |
33 | );
34 | }
35 |
36 | export { Badge, badgeVariants };
37 |
--------------------------------------------------------------------------------
/src/components/ui/breadcrumb.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { Slot } from "@radix-ui/react-slot";
3 | import { ChevronRight, MoreHorizontal } from "lucide-react";
4 |
5 | import { cn } from "~/lib/utils";
6 |
7 | const Breadcrumb = React.forwardRef<
8 | HTMLElement,
9 | React.ComponentPropsWithoutRef<"nav"> & {
10 | separator?: React.ReactNode;
11 | }
12 | >(({ ...props }, ref) => );
13 | Breadcrumb.displayName = "Breadcrumb";
14 |
15 | const BreadcrumbList = React.forwardRef<
16 | HTMLOListElement,
17 | React.ComponentPropsWithoutRef<"ol">
18 | >(({ className, ...props }, ref) => (
19 |
27 | ));
28 | BreadcrumbList.displayName = "BreadcrumbList";
29 |
30 | const BreadcrumbItem = React.forwardRef<
31 | HTMLLIElement,
32 | React.ComponentPropsWithoutRef<"li">
33 | >(({ className, ...props }, ref) => (
34 |
39 | ));
40 | BreadcrumbItem.displayName = "BreadcrumbItem";
41 |
42 | const BreadcrumbLink = React.forwardRef<
43 | HTMLAnchorElement,
44 | React.ComponentPropsWithoutRef<"a"> & {
45 | asChild?: boolean;
46 | }
47 | >(({ asChild, className, ...props }, ref) => {
48 | const Comp = asChild ? Slot : "a";
49 |
50 | return (
51 |
56 | );
57 | });
58 | BreadcrumbLink.displayName = "BreadcrumbLink";
59 |
60 | const BreadcrumbPage = React.forwardRef<
61 | HTMLSpanElement,
62 | React.ComponentPropsWithoutRef<"span">
63 | >(({ className, ...props }, ref) => (
64 |
72 | ));
73 | BreadcrumbPage.displayName = "BreadcrumbPage";
74 |
75 | const BreadcrumbSeparator = ({
76 | children,
77 | className,
78 | ...props
79 | }: React.ComponentProps<"li">) => (
80 | svg]:h-3.5 [&>svg]:w-3.5", className)}
84 | {...props}
85 | >
86 | {children ?? }
87 |
88 | );
89 | BreadcrumbSeparator.displayName = "BreadcrumbSeparator";
90 |
91 | const BreadcrumbEllipsis = ({
92 | className,
93 | ...props
94 | }: React.ComponentProps<"span">) => (
95 |
101 |
102 | More
103 |
104 | );
105 | BreadcrumbEllipsis.displayName = "BreadcrumbElipssis";
106 |
107 | export {
108 | Breadcrumb,
109 | BreadcrumbList,
110 | BreadcrumbItem,
111 | BreadcrumbLink,
112 | BreadcrumbPage,
113 | BreadcrumbSeparator,
114 | BreadcrumbEllipsis,
115 | };
116 |
--------------------------------------------------------------------------------
/src/components/ui/button.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { Slot } from "@radix-ui/react-slot";
3 | import { cva, type VariantProps } from "class-variance-authority";
4 |
5 | import { cn } from "~/lib/utils";
6 |
7 | const buttonVariants = cva(
8 | "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
9 | {
10 | variants: {
11 | variant: {
12 | default: "bg-primary text-primary-foreground hover:bg-primary/90",
13 | destructive:
14 | "bg-destructive text-destructive-foreground hover:bg-destructive/90",
15 | outline:
16 | "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
17 | secondary:
18 | "bg-secondary text-secondary-foreground hover:bg-secondary/80",
19 | ghost: "hover:bg-accent hover:text-accent-foreground",
20 | link: "text-primary underline-offset-4 hover:underline",
21 | },
22 | size: {
23 | default: "h-10 px-4 py-2",
24 | sm: "h-9 rounded-md px-3",
25 | lg: "h-11 rounded-md px-8",
26 | icon: "h-10 w-10",
27 | },
28 | },
29 | defaultVariants: {
30 | variant: "default",
31 | size: "default",
32 | },
33 | }
34 | );
35 |
36 | export interface ButtonProps
37 | extends React.ButtonHTMLAttributes,
38 | VariantProps {
39 | asChild?: boolean;
40 | }
41 |
42 | const Button = React.forwardRef(
43 | ({ className, variant, size, asChild = false, ...props }, ref) => {
44 | const Comp = asChild ? Slot : "button";
45 | return (
46 |
51 | );
52 | }
53 | );
54 | Button.displayName = "Button";
55 |
56 | export { Button, buttonVariants };
57 |
--------------------------------------------------------------------------------
/src/components/ui/calendar.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import { ChevronLeft, ChevronRight } from "lucide-react";
5 | import { DayPicker } from "react-day-picker";
6 |
7 | import { cn } from "~/lib/utils";
8 | import { buttonVariants } from "~/components/ui/button";
9 |
10 | export type CalendarProps = React.ComponentProps;
11 |
12 | function Calendar({
13 | className,
14 | classNames,
15 | showOutsideDays = true,
16 | ...props
17 | }: CalendarProps) {
18 | return (
19 | (
58 |
59 | ),
60 | IconRight: ({ className, ...props }) => (
61 |
62 | ),
63 | }}
64 | {...props}
65 | />
66 | );
67 | }
68 | Calendar.displayName = "Calendar";
69 |
70 | export { Calendar };
71 |
--------------------------------------------------------------------------------
/src/components/ui/card.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 |
3 | import { cn } from "~/lib/utils";
4 |
5 | const Card = React.forwardRef<
6 | HTMLDivElement,
7 | React.HTMLAttributes
8 | >(({ className, ...props }, ref) => (
9 |
17 | ));
18 | Card.displayName = "Card";
19 |
20 | const CardHeader = React.forwardRef<
21 | HTMLDivElement,
22 | React.HTMLAttributes
23 | >(({ className, ...props }, ref) => (
24 |
29 | ));
30 | CardHeader.displayName = "CardHeader";
31 |
32 | const CardTitle = React.forwardRef<
33 | HTMLDivElement,
34 | React.HTMLAttributes
35 | >(({ className, ...props }, ref) => (
36 |
44 | ));
45 | CardTitle.displayName = "CardTitle";
46 |
47 | const CardDescription = React.forwardRef<
48 | HTMLDivElement,
49 | React.HTMLAttributes
50 | >(({ className, ...props }, ref) => (
51 |
56 | ));
57 | CardDescription.displayName = "CardDescription";
58 |
59 | const CardContent = React.forwardRef<
60 | HTMLDivElement,
61 | React.HTMLAttributes
62 | >(({ className, ...props }, ref) => (
63 |
64 | ));
65 | CardContent.displayName = "CardContent";
66 |
67 | const CardFooter = React.forwardRef<
68 | HTMLDivElement,
69 | React.HTMLAttributes
70 | >(({ className, ...props }, ref) => (
71 |
76 | ));
77 | CardFooter.displayName = "CardFooter";
78 |
79 | export {
80 | Card,
81 | CardHeader,
82 | CardFooter,
83 | CardTitle,
84 | CardDescription,
85 | CardContent,
86 | };
87 |
--------------------------------------------------------------------------------
/src/components/ui/checkbox.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
5 | import { Check } from "lucide-react";
6 |
7 | import { cn } from "~/lib/utils";
8 |
9 | const Checkbox = React.forwardRef<
10 | React.ElementRef,
11 | React.ComponentPropsWithoutRef
12 | >(({ className, ...props }, ref) => (
13 |
21 |
24 |
25 |
26 |
27 | ));
28 | Checkbox.displayName = CheckboxPrimitive.Root.displayName;
29 |
30 | export { Checkbox };
31 |
--------------------------------------------------------------------------------
/src/components/ui/collapsible.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as CollapsiblePrimitive from "@radix-ui/react-collapsible";
4 |
5 | const Collapsible = CollapsiblePrimitive.Root;
6 |
7 | const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger;
8 |
9 | const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent;
10 |
11 | export { Collapsible, CollapsibleTrigger, CollapsibleContent };
12 |
--------------------------------------------------------------------------------
/src/components/ui/dialog.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import * as DialogPrimitive from "@radix-ui/react-dialog";
5 | import { X } from "lucide-react";
6 |
7 | import { cn } from "~/lib/utils";
8 |
9 | const Dialog = DialogPrimitive.Root;
10 |
11 | const DialogTrigger = DialogPrimitive.Trigger;
12 |
13 | const DialogPortal = DialogPrimitive.Portal;
14 |
15 | const DialogClose = DialogPrimitive.Close;
16 |
17 | const DialogOverlay = React.forwardRef<
18 | React.ElementRef,
19 | React.ComponentPropsWithoutRef
20 | >(({ className, ...props }, ref) => (
21 |
29 | ));
30 | DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
31 |
32 | const DialogContent = React.forwardRef<
33 | React.ElementRef,
34 | React.ComponentPropsWithoutRef
35 | >(({ className, children, ...props }, ref) => (
36 |
37 |
38 |
46 | {children}
47 |
48 |
49 | Close
50 |
51 |
52 |
53 | ));
54 | DialogContent.displayName = DialogPrimitive.Content.displayName;
55 |
56 | const DialogHeader = ({
57 | className,
58 | ...props
59 | }: React.HTMLAttributes) => (
60 |
67 | );
68 | DialogHeader.displayName = "DialogHeader";
69 |
70 | const DialogFooter = ({
71 | className,
72 | ...props
73 | }: React.HTMLAttributes) => (
74 |
81 | );
82 | DialogFooter.displayName = "DialogFooter";
83 |
84 | const DialogTitle = React.forwardRef<
85 | React.ElementRef,
86 | React.ComponentPropsWithoutRef
87 | >(({ className, ...props }, ref) => (
88 |
96 | ));
97 | DialogTitle.displayName = DialogPrimitive.Title.displayName;
98 |
99 | const DialogDescription = React.forwardRef<
100 | React.ElementRef,
101 | React.ComponentPropsWithoutRef
102 | >(({ className, ...props }, ref) => (
103 |
108 | ));
109 | DialogDescription.displayName = DialogPrimitive.Description.displayName;
110 |
111 | export {
112 | Dialog,
113 | DialogPortal,
114 | DialogOverlay,
115 | DialogClose,
116 | DialogTrigger,
117 | DialogContent,
118 | DialogHeader,
119 | DialogFooter,
120 | DialogTitle,
121 | DialogDescription,
122 | };
123 |
--------------------------------------------------------------------------------
/src/components/ui/drawer.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import { Drawer as DrawerPrimitive } from "vaul";
5 |
6 | import { cn } from "~/lib/utils";
7 |
8 | const Drawer = ({
9 | shouldScaleBackground = true,
10 | ...props
11 | }: React.ComponentProps) => (
12 |
16 | );
17 | Drawer.displayName = "Drawer";
18 |
19 | const DrawerTrigger = DrawerPrimitive.Trigger;
20 |
21 | const DrawerPortal = DrawerPrimitive.Portal;
22 |
23 | const DrawerClose = DrawerPrimitive.Close;
24 |
25 | const DrawerOverlay = React.forwardRef<
26 | React.ElementRef,
27 | React.ComponentPropsWithoutRef
28 | >(({ className, ...props }, ref) => (
29 |
34 | ));
35 | DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName;
36 |
37 | const DrawerContent = React.forwardRef<
38 | React.ElementRef,
39 | React.ComponentPropsWithoutRef
40 | >(({ className, children, ...props }, ref) => (
41 |
42 |
43 |
51 |
52 | {children}
53 |
54 |
55 | ));
56 | DrawerContent.displayName = "DrawerContent";
57 |
58 | const DrawerHeader = ({
59 | className,
60 | ...props
61 | }: React.HTMLAttributes) => (
62 |
66 | );
67 | DrawerHeader.displayName = "DrawerHeader";
68 |
69 | const DrawerFooter = ({
70 | className,
71 | ...props
72 | }: React.HTMLAttributes) => (
73 |
77 | );
78 | DrawerFooter.displayName = "DrawerFooter";
79 |
80 | const DrawerTitle = React.forwardRef<
81 | React.ElementRef,
82 | React.ComponentPropsWithoutRef
83 | >(({ className, ...props }, ref) => (
84 |
92 | ));
93 | DrawerTitle.displayName = DrawerPrimitive.Title.displayName;
94 |
95 | const DrawerDescription = React.forwardRef<
96 | React.ElementRef,
97 | React.ComponentPropsWithoutRef
98 | >(({ className, ...props }, ref) => (
99 |
104 | ));
105 | DrawerDescription.displayName = DrawerPrimitive.Description.displayName;
106 |
107 | export {
108 | Drawer,
109 | DrawerPortal,
110 | DrawerOverlay,
111 | DrawerTrigger,
112 | DrawerClose,
113 | DrawerContent,
114 | DrawerHeader,
115 | DrawerFooter,
116 | DrawerTitle,
117 | DrawerDescription,
118 | };
119 |
--------------------------------------------------------------------------------
/src/components/ui/hover-card.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import * as HoverCardPrimitive from "@radix-ui/react-hover-card";
5 |
6 | import { cn } from "~/lib/utils";
7 |
8 | const HoverCard = HoverCardPrimitive.Root;
9 |
10 | const HoverCardTrigger = HoverCardPrimitive.Trigger;
11 |
12 | const HoverCardContent = React.forwardRef<
13 | React.ElementRef,
14 | React.ComponentPropsWithoutRef
15 | >(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
16 |
26 | ));
27 | HoverCardContent.displayName = HoverCardPrimitive.Content.displayName;
28 |
29 | export { HoverCard, HoverCardTrigger, HoverCardContent };
30 |
--------------------------------------------------------------------------------
/src/components/ui/input-otp.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import { OTPInput, OTPInputContext } from "input-otp";
5 | import { Dot } from "lucide-react";
6 |
7 | import { cn } from "~/lib/utils";
8 |
9 | const InputOTP = React.forwardRef<
10 | React.ElementRef,
11 | React.ComponentPropsWithoutRef
12 | >(({ className, containerClassName, ...props }, ref) => (
13 |
22 | ));
23 | InputOTP.displayName = "InputOTP";
24 |
25 | const InputOTPGroup = React.forwardRef<
26 | React.ElementRef<"div">,
27 | React.ComponentPropsWithoutRef<"div">
28 | >(({ className, ...props }, ref) => (
29 |
30 | ));
31 | InputOTPGroup.displayName = "InputOTPGroup";
32 |
33 | const InputOTPSlot = React.forwardRef<
34 | React.ElementRef<"div">,
35 | React.ComponentPropsWithoutRef<"div"> & { index: number }
36 | >(({ index, className, ...props }, ref) => {
37 | const inputOTPContext = React.useContext(OTPInputContext);
38 | const { char, hasFakeCaret, isActive } = inputOTPContext.slots[index];
39 |
40 | return (
41 |
50 | {char}
51 | {hasFakeCaret && (
52 |
55 | )}
56 |
57 | );
58 | });
59 | InputOTPSlot.displayName = "InputOTPSlot";
60 |
61 | const InputOTPSeparator = React.forwardRef<
62 | React.ElementRef<"div">,
63 | React.ComponentPropsWithoutRef<"div">
64 | >(({ ...props }, ref) => (
65 |
66 |
67 |
68 | ));
69 | InputOTPSeparator.displayName = "InputOTPSeparator";
70 |
71 | export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator };
72 |
--------------------------------------------------------------------------------
/src/components/ui/input.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 |
3 | import { cn } from "~/lib/utils";
4 |
5 | const Input = React.forwardRef>(
6 | ({ className, type, ...props }, ref) => {
7 | return (
8 |
17 | );
18 | }
19 | );
20 | Input.displayName = "Input";
21 |
22 | export { Input };
23 |
--------------------------------------------------------------------------------
/src/components/ui/label.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import * as LabelPrimitive from "@radix-ui/react-label";
5 | import { cva, type VariantProps } from "class-variance-authority";
6 |
7 | import { cn } from "~/lib/utils";
8 |
9 | const labelVariants = cva(
10 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
11 | );
12 |
13 | const Label = React.forwardRef<
14 | React.ElementRef,
15 | React.ComponentPropsWithoutRef &
16 | VariantProps
17 | >(({ className, ...props }, ref) => (
18 |
23 | ));
24 | Label.displayName = LabelPrimitive.Root.displayName;
25 |
26 | export { Label };
27 |
--------------------------------------------------------------------------------
/src/components/ui/pagination.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { ChevronLeft, ChevronRight, MoreHorizontal } from "lucide-react";
3 |
4 | import { cn } from "~/lib/utils";
5 | import { ButtonProps, buttonVariants } from "~/components/ui/button";
6 |
7 | const Pagination = ({ className, ...props }: React.ComponentProps<"nav">) => (
8 |
14 | );
15 | Pagination.displayName = "Pagination";
16 |
17 | const PaginationContent = React.forwardRef<
18 | HTMLUListElement,
19 | React.ComponentProps<"ul">
20 | >(({ className, ...props }, ref) => (
21 |
26 | ));
27 | PaginationContent.displayName = "PaginationContent";
28 |
29 | const PaginationItem = React.forwardRef<
30 | HTMLLIElement,
31 | React.ComponentProps<"li">
32 | >(({ className, ...props }, ref) => (
33 |
34 | ));
35 | PaginationItem.displayName = "PaginationItem";
36 |
37 | type PaginationLinkProps = {
38 | isActive?: boolean;
39 | } & Pick &
40 | React.ComponentProps<"a">;
41 |
42 | const PaginationLink = ({
43 | className,
44 | isActive,
45 | size = "icon",
46 | ...props
47 | }: PaginationLinkProps) => (
48 |
59 | );
60 | PaginationLink.displayName = "PaginationLink";
61 |
62 | const PaginationPrevious = ({
63 | className,
64 | ...props
65 | }: React.ComponentProps) => (
66 |
72 |
73 | Previous
74 |
75 | );
76 | PaginationPrevious.displayName = "PaginationPrevious";
77 |
78 | const PaginationNext = ({
79 | className,
80 | ...props
81 | }: React.ComponentProps) => (
82 |
88 | Next
89 |
90 |
91 | );
92 | PaginationNext.displayName = "PaginationNext";
93 |
94 | const PaginationEllipsis = ({
95 | className,
96 | ...props
97 | }: React.ComponentProps<"span">) => (
98 |
103 |
104 | More pages
105 |
106 | );
107 | PaginationEllipsis.displayName = "PaginationEllipsis";
108 |
109 | export {
110 | Pagination,
111 | PaginationContent,
112 | PaginationEllipsis,
113 | PaginationItem,
114 | PaginationLink,
115 | PaginationNext,
116 | PaginationPrevious,
117 | };
118 |
--------------------------------------------------------------------------------
/src/components/ui/popover.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import * as PopoverPrimitive from "@radix-ui/react-popover";
5 |
6 | import { cn } from "~/lib/utils";
7 |
8 | const Popover = PopoverPrimitive.Root;
9 |
10 | const PopoverTrigger = PopoverPrimitive.Trigger;
11 |
12 | const PopoverContent = React.forwardRef<
13 | React.ElementRef,
14 | React.ComponentPropsWithoutRef
15 | >(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
16 |
17 |
27 |
28 | ));
29 | PopoverContent.displayName = PopoverPrimitive.Content.displayName;
30 |
31 | export { Popover, PopoverTrigger, PopoverContent };
32 |
--------------------------------------------------------------------------------
/src/components/ui/progress.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import * as ProgressPrimitive from "@radix-ui/react-progress";
5 |
6 | import { cn } from "~/lib/utils";
7 |
8 | const Progress = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, value, ...props }, ref) => (
12 |
20 |
24 |
25 | ));
26 | Progress.displayName = ProgressPrimitive.Root.displayName;
27 |
28 | export { Progress };
29 |
--------------------------------------------------------------------------------
/src/components/ui/radio-group.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import * as RadioGroupPrimitive from "@radix-ui/react-radio-group";
5 | import { Circle } from "lucide-react";
6 |
7 | import { cn } from "~/lib/utils";
8 |
9 | const RadioGroup = React.forwardRef<
10 | React.ElementRef,
11 | React.ComponentPropsWithoutRef
12 | >(({ className, ...props }, ref) => {
13 | return (
14 |
19 | );
20 | });
21 | RadioGroup.displayName = RadioGroupPrimitive.Root.displayName;
22 |
23 | const RadioGroupItem = React.forwardRef<
24 | React.ElementRef,
25 | React.ComponentPropsWithoutRef
26 | >(({ className, ...props }, ref) => {
27 | return (
28 |
36 |
37 |
38 |
39 |
40 | );
41 | });
42 | RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName;
43 |
44 | export { RadioGroup, RadioGroupItem };
45 |
--------------------------------------------------------------------------------
/src/components/ui/resizable.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { GripVertical } from "lucide-react";
4 | import * as ResizablePrimitive from "react-resizable-panels";
5 |
6 | import { cn } from "~/lib/utils";
7 |
8 | const ResizablePanelGroup = ({
9 | className,
10 | ...props
11 | }: React.ComponentProps) => (
12 |
19 | );
20 |
21 | const ResizablePanel = ResizablePrimitive.Panel;
22 |
23 | const ResizableHandle = ({
24 | withHandle,
25 | className,
26 | ...props
27 | }: React.ComponentProps & {
28 | withHandle?: boolean;
29 | }) => (
30 | div]:rotate-90",
33 | className
34 | )}
35 | {...props}
36 | >
37 | {withHandle && (
38 |
39 |
40 |
41 | )}
42 |
43 | );
44 |
45 | export { ResizablePanelGroup, ResizablePanel, ResizableHandle };
46 |
--------------------------------------------------------------------------------
/src/components/ui/scroll-area.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area";
5 |
6 | import { cn } from "~/lib/utils";
7 |
8 | const ScrollArea = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, children, ...props }, ref) => (
12 |
17 |
18 | {children}
19 |
20 |
21 |
22 |
23 | ));
24 | ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName;
25 |
26 | const ScrollBar = React.forwardRef<
27 | React.ElementRef,
28 | React.ComponentPropsWithoutRef
29 | >(({ className, orientation = "vertical", ...props }, ref) => (
30 |
43 |
44 |
45 | ));
46 | ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName;
47 |
48 | export { ScrollArea, ScrollBar };
49 |
--------------------------------------------------------------------------------
/src/components/ui/separator.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import * as SeparatorPrimitive from "@radix-ui/react-separator";
5 |
6 | import { cn } from "~/lib/utils";
7 |
8 | const Separator = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(
12 | (
13 | { className, orientation = "horizontal", decorative = true, ...props },
14 | ref
15 | ) => (
16 |
27 | )
28 | );
29 | Separator.displayName = SeparatorPrimitive.Root.displayName;
30 |
31 | export { Separator };
32 |
--------------------------------------------------------------------------------
/src/components/ui/skeleton.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "~/lib/utils";
2 |
3 | function Skeleton({
4 | className,
5 | ...props
6 | }: React.HTMLAttributes) {
7 | return (
8 |
12 | );
13 | }
14 |
15 | export { Skeleton };
16 |
--------------------------------------------------------------------------------
/src/components/ui/slider.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import * as SliderPrimitive from "@radix-ui/react-slider";
5 |
6 | import { cn } from "~/lib/utils";
7 |
8 | const Slider = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, ...props }, ref) => (
12 |
20 |
21 |
22 |
23 |
24 |
25 | ));
26 | Slider.displayName = SliderPrimitive.Root.displayName;
27 |
28 | export { Slider };
29 |
--------------------------------------------------------------------------------
/src/components/ui/sonner.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useTheme } from "next-themes";
4 | import { Toaster as Sonner } from "sonner";
5 |
6 | type ToasterProps = React.ComponentProps;
7 |
8 | const Toaster = ({ ...props }: ToasterProps) => {
9 | const { theme = "system" } = useTheme();
10 |
11 | return (
12 |
28 | );
29 | };
30 |
31 | export { Toaster };
32 |
--------------------------------------------------------------------------------
/src/components/ui/switch.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import * as SwitchPrimitives from "@radix-ui/react-switch";
5 |
6 | import { cn } from "~/lib/utils";
7 |
8 | const Switch = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, ...props }, ref) => (
12 |
20 |
25 |
26 | ));
27 | Switch.displayName = SwitchPrimitives.Root.displayName;
28 |
29 | export { Switch };
30 |
--------------------------------------------------------------------------------
/src/components/ui/table.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 |
3 | import { cn } from "~/lib/utils";
4 |
5 | const Table = React.forwardRef<
6 | HTMLTableElement,
7 | React.HTMLAttributes
8 | >(({ className, ...props }, ref) => (
9 |
16 | ));
17 | Table.displayName = "Table";
18 |
19 | const TableHeader = React.forwardRef<
20 | HTMLTableSectionElement,
21 | React.HTMLAttributes
22 | >(({ className, ...props }, ref) => (
23 |
24 | ));
25 | TableHeader.displayName = "TableHeader";
26 |
27 | const TableBody = React.forwardRef<
28 | HTMLTableSectionElement,
29 | React.HTMLAttributes
30 | >(({ className, ...props }, ref) => (
31 |
36 | ));
37 | TableBody.displayName = "TableBody";
38 |
39 | const TableFooter = React.forwardRef<
40 | HTMLTableSectionElement,
41 | React.HTMLAttributes
42 | >(({ className, ...props }, ref) => (
43 | tr]:last:border-b-0",
47 | className
48 | )}
49 | {...props}
50 | />
51 | ));
52 | TableFooter.displayName = "TableFooter";
53 |
54 | const TableRow = React.forwardRef<
55 | HTMLTableRowElement,
56 | React.HTMLAttributes
57 | >(({ className, ...props }, ref) => (
58 |
66 | ));
67 | TableRow.displayName = "TableRow";
68 |
69 | const TableHead = React.forwardRef<
70 | HTMLTableCellElement,
71 | React.ThHTMLAttributes
72 | >(({ className, ...props }, ref) => (
73 | |
81 | ));
82 | TableHead.displayName = "TableHead";
83 |
84 | const TableCell = React.forwardRef<
85 | HTMLTableCellElement,
86 | React.TdHTMLAttributes
87 | >(({ className, ...props }, ref) => (
88 | |
93 | ));
94 | TableCell.displayName = "TableCell";
95 |
96 | const TableCaption = React.forwardRef<
97 | HTMLTableCaptionElement,
98 | React.HTMLAttributes
99 | >(({ className, ...props }, ref) => (
100 |
105 | ));
106 | TableCaption.displayName = "TableCaption";
107 |
108 | export {
109 | Table,
110 | TableHeader,
111 | TableBody,
112 | TableFooter,
113 | TableHead,
114 | TableRow,
115 | TableCell,
116 | TableCaption,
117 | };
118 |
--------------------------------------------------------------------------------
/src/components/ui/tabs.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import * as TabsPrimitive from "@radix-ui/react-tabs";
5 |
6 | import { cn } from "~/lib/utils";
7 |
8 | const Tabs = TabsPrimitive.Root;
9 |
10 | const TabsList = React.forwardRef<
11 | React.ElementRef,
12 | React.ComponentPropsWithoutRef
13 | >(({ className, ...props }, ref) => (
14 |
22 | ));
23 | TabsList.displayName = TabsPrimitive.List.displayName;
24 |
25 | const TabsTrigger = React.forwardRef<
26 | React.ElementRef,
27 | React.ComponentPropsWithoutRef
28 | >(({ className, ...props }, ref) => (
29 |
37 | ));
38 | TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
39 |
40 | const TabsContent = React.forwardRef<
41 | React.ElementRef,
42 | React.ComponentPropsWithoutRef
43 | >(({ className, ...props }, ref) => (
44 |
52 | ));
53 | TabsContent.displayName = TabsPrimitive.Content.displayName;
54 |
55 | export { Tabs, TabsList, TabsTrigger, TabsContent };
56 |
--------------------------------------------------------------------------------
/src/components/ui/textarea.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 |
3 | import { cn } from "~/lib/utils";
4 |
5 | const Textarea = React.forwardRef<
6 | HTMLTextAreaElement,
7 | React.ComponentProps<"textarea">
8 | >(({ className, ...props }, ref) => {
9 | return (
10 |
18 | );
19 | });
20 | Textarea.displayName = "Textarea";
21 |
22 | export { Textarea };
23 |
--------------------------------------------------------------------------------
/src/components/ui/toaster.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useToast } from "~/hooks/use-toast";
4 | import {
5 | Toast,
6 | ToastClose,
7 | ToastDescription,
8 | ToastProvider,
9 | ToastTitle,
10 | ToastViewport,
11 | } from "~/components/ui/toast";
12 |
13 | export function Toaster() {
14 | const { toasts } = useToast();
15 |
16 | return (
17 |
18 | {toasts.map(function ({ id, title, description, action, ...props }) {
19 | return (
20 |
21 |
22 | {title && {title}}
23 | {description && (
24 | {description}
25 | )}
26 |
27 | {action}
28 |
29 |
30 | );
31 | })}
32 |
33 |
34 | );
35 | }
36 |
--------------------------------------------------------------------------------
/src/components/ui/toggle-group.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group";
5 | import { type VariantProps } from "class-variance-authority";
6 |
7 | import { cn } from "~/lib/utils";
8 | import { toggleVariants } from "~/components/ui/toggle";
9 |
10 | const ToggleGroupContext = React.createContext<
11 | VariantProps
12 | >({
13 | size: "default",
14 | variant: "default",
15 | });
16 |
17 | const ToggleGroup = React.forwardRef<
18 | React.ElementRef,
19 | React.ComponentPropsWithoutRef &
20 | VariantProps
21 | >(({ className, variant, size, children, ...props }, ref) => (
22 |
27 |
28 | {children}
29 |
30 |
31 | ));
32 |
33 | ToggleGroup.displayName = ToggleGroupPrimitive.Root.displayName;
34 |
35 | const ToggleGroupItem = React.forwardRef<
36 | React.ElementRef,
37 | React.ComponentPropsWithoutRef &
38 | VariantProps
39 | >(({ className, children, variant, size, ...props }, ref) => {
40 | const context = React.useContext(ToggleGroupContext);
41 |
42 | return (
43 |
54 | {children}
55 |
56 | );
57 | });
58 |
59 | ToggleGroupItem.displayName = ToggleGroupPrimitive.Item.displayName;
60 |
61 | export { ToggleGroup, ToggleGroupItem };
62 |
--------------------------------------------------------------------------------
/src/components/ui/toggle.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import * as TogglePrimitive from "@radix-ui/react-toggle";
5 | import { cva, type VariantProps } from "class-variance-authority";
6 |
7 | import { cn } from "~/lib/utils";
8 |
9 | const toggleVariants = cva(
10 | "inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors hover:bg-muted hover:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 gap-2",
11 | {
12 | variants: {
13 | variant: {
14 | default: "bg-transparent",
15 | outline:
16 | "border border-input bg-transparent hover:bg-accent hover:text-accent-foreground",
17 | },
18 | size: {
19 | default: "h-10 px-3 min-w-10",
20 | sm: "h-9 px-2.5 min-w-9",
21 | lg: "h-11 px-5 min-w-11",
22 | },
23 | },
24 | defaultVariants: {
25 | variant: "default",
26 | size: "default",
27 | },
28 | }
29 | );
30 |
31 | const Toggle = React.forwardRef<
32 | React.ElementRef,
33 | React.ComponentPropsWithoutRef &
34 | VariantProps
35 | >(({ className, variant, size, ...props }, ref) => (
36 |
41 | ));
42 |
43 | Toggle.displayName = TogglePrimitive.Root.displayName;
44 |
45 | export { Toggle, toggleVariants };
46 |
--------------------------------------------------------------------------------
/src/components/ui/tooltip.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import * as TooltipPrimitive from "@radix-ui/react-tooltip";
5 |
6 | import { cn } from "~/lib/utils";
7 |
8 | const TooltipProvider = TooltipPrimitive.Provider;
9 |
10 | const Tooltip = TooltipPrimitive.Root;
11 |
12 | const TooltipTrigger = TooltipPrimitive.Trigger;
13 |
14 | const TooltipContent = React.forwardRef<
15 | React.ElementRef,
16 | React.ComponentPropsWithoutRef
17 | >(({ className, sideOffset = 4, ...props }, ref) => (
18 |
27 | ));
28 | TooltipContent.displayName = TooltipPrimitive.Content.displayName;
29 |
30 | export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
31 |
--------------------------------------------------------------------------------
/src/config/site.ts:
--------------------------------------------------------------------------------
1 | export const siteUrl =
2 | process.env.NEXT_PUBLIC_APP_URL || "https://chadnext.moinulmoin.com";
3 |
4 | export const siteConfig = (locale: string = "en") => ({
5 | name: "ChadNext",
6 | url: siteUrl + "/" + locale,
7 | ogImage: `${siteUrl}/${locale}/opengraph-image`,
8 | description: "Quick Starter Template for your Next project.",
9 | links: {
10 | twitter: "https://twitter.com/immoinulmoin",
11 | github: "https://github.com/moinulmoin/chadnext",
12 | },
13 | });
14 |
15 | export type SiteConfig = typeof siteConfig;
16 |
--------------------------------------------------------------------------------
/src/config/subscription.ts:
--------------------------------------------------------------------------------
1 | import { type SubscriptionPlan } from "~/types";
2 |
3 | export const freePlan: SubscriptionPlan = {
4 | name: "Free",
5 | description:
6 | "You can create up to 3 Projects. Upgrade to the PRO plan for unlimited projects.",
7 | stripePriceId: "",
8 | };
9 |
10 | export const proPlan: SubscriptionPlan = {
11 | name: "PRO",
12 | description: "Now you have unlimited projects!",
13 | stripePriceId: process.env.STRIPE_PRO_PLAN_ID as string,
14 | };
15 |
--------------------------------------------------------------------------------
/src/content/about/1tech-stack.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Tech Stack
3 | ---
4 |
5 | - Language: **[Typescript](https://www.typescriptlang.org/)**
6 | - Framework: **[Next.js](https://nextjs.org/)**
7 | - Styling: **[Tailwind CSS](https://tailwindcss.com/)**
8 | - Component Library: **[Shadcn/ui](https://ui.shadcn.com/)**
9 | - Database: **[Postgres](https://vercel.com/postgres)**
10 | - ORM: **[Prisma](https://www.prisma.io/)**
11 | - Authentication: **[LuciaAuth](https://lucia-auth.com/)**
12 | - Payment: **[Stripe](https://stripe.com/)**
13 | - Analytics: **[Umami](https://umami.is/)**
14 | - Internationalization: **[Next International](https://next-international.vercel.app/)**
15 | - Deployment: **[Vercel](https://vercel.com/)**
16 | - Email: **[Resend](https://resend.com/)**
17 | - Email Template : **[React Email](https://react.email/)**
18 | - File Storage: **[UploadThing](https://uploadthing.com/)**
19 | - Linting & Formatting: **[ESLint](https://eslint.org/)** & **[Prettier](https://prettier.io/)**
20 |
--------------------------------------------------------------------------------
/src/content/about/2inspiration.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Inspiration
3 | ---
4 |
5 | - **[Taxonomy](https://tx.shadcn.com/)**
6 | - **[Shadcn/ui](https://ui.shadcn.com/)**
7 | - **[Precedent](https://precedent.dev/)**
8 | - **[Dub](https://dub.sh/)**
9 | - **[Float UI](https://floatui.com/)**
10 |
--------------------------------------------------------------------------------
/src/content/changelog/change-01.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Introducing ChadNext
3 | date: 2023-05-25
4 | ---
5 |
6 | I have created ChadNext to help developers build and deploy their ideas quickly. I didn't find any template combining all of these together. That's why I connected the dots and made ChadNext.
7 |
8 | - **[Next.js](https://nextjs.org/)** for Fullstack Development.
9 | - **[Tailwind CSS](https://tailwindcss.com/)** for Styling.
10 | - **[Shadcn/ui](https://ui.shadcn.com/)** for UI components.
11 | - **[Typescript](https://www.typescriptlang.org/)** for type safety.
12 | - **[ESLint](https://eslint.org/)** for linting.
13 | - **[Prettier](https://prettier.io/)** for formatting.
14 | - **[Husky](https://typicode.github.io/husky/#/)** for git hooks.
15 | - **[Prisma](https://www.prisma.io/)** for Database ORM.
16 | - **[Postgres](https://vercel.com/postgres)** for Database.
17 | - **[NextAuth.js](https://next-auth.js.org/)** for Authentication.
18 | - **[Vercel](https://vercel.com/)** for Deployment.
19 | - Dark Mode Support.
20 |
--------------------------------------------------------------------------------
/src/content/changelog/change-02.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Added Welcome Mail for new users
3 | date: 2023-05-30
4 | ---
5 |
6 | - Integrated **[Resend](https://resend.com/)** to send emails.
7 | - Integrated **[React Email](https://react.email/)** to create email templates.
8 | - Styled email templates with **[Tailwind CSS](https://tailwindcss.com/)**.
9 |
--------------------------------------------------------------------------------
/src/content/changelog/change-03.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Added New Settings page and more
3 | date: 2023-06-15
4 | ---
5 |
6 | - Integrated **[React Hook Form](https://react-hook-form.com/)** & **[Zod](https://zod.dev/)** to manage form state and validation.
7 | - Used **[Server Actions](https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions)** to handle form submission.
8 | - Integrated **[UploadThing](https://uploadthing.com/)** to upload image effortlessly.
9 | - Added Custom Upload Image Component with dark mode support.
10 |
--------------------------------------------------------------------------------
/src/content/changelog/change-04.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Added Passwordless Auth and more
3 | date: 2023-06-28
4 | ---
5 |
6 | - Updated [Shadcn/ui](https://ui.shadcn.com/) lib to the latest version.
7 | - Added Magic Links Auth for registered users.
8 | - Fix opengraph image path issue.
9 | - Added Changelog Page using [Contentlayer](https://www.contentlayer.dev/).
10 | - Added PWA Support.
11 |
--------------------------------------------------------------------------------
/src/content/changelog/change-05.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Added Intercepting Signin route and more
3 | date: 2023-07-15
4 | ---
5 |
6 | - Added [Intercepting route](https://nextjs.org/docs/app/building-your-application/routing/intercepting-routes) for Signin Page.
7 | - Integrated [Uploadthing](https://uploadthing.com/) with [ReactHookForm](https://react-hook-form.com/) & [Zod](https://zod.dev/).
8 | - Added Custom Header in [Resend](https://resend.com/) mail to avoid stacking of mail in the inbox.
9 | - Upgraded [Prisma](https://www.prisma.io/) to 5.0.0.
10 | - Rewrote open graph image generation styles with [TailwindCSS](https://tailwindcss.com/).
11 | - and many more improvements and bug fixes.
12 |
--------------------------------------------------------------------------------
/src/content/changelog/change-06.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Homepage got updated!
3 | date: 2023-08-02
4 | ---
5 |
6 | - Added Features Section on Homepage.
7 | - Added Open Source Section on Homepage.
8 | - Added About Page.
9 | - Added Projects Page in Dashboard.
10 | - and many more improvements and bug fixes.
11 |
--------------------------------------------------------------------------------
/src/content/changelog/change-07.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: LuciaAuth v3, Stripe, Internationalization and more!
3 | date: 2024-01-05
4 | ---
5 |
6 | - Upgraded to **[Next.js](https://nextjs.org/)** v14
7 | - Move to **[LuciaAuth](https://lucia-auth.com/)** v2 and then upgraded to V3
8 | - Upgraded to **[UploadThing](https://uploadthing.com/)**
9 | - Added **[React Email](https://react.email/)** Preview in development.
10 | - Added **[Stripe](https://stripe.com/)** Integration.
11 | - Added **[Internationalization](https://next-international.vercel.app/)** support.
12 | - Improved the UI and refactored the Codebase.
13 |
--------------------------------------------------------------------------------
/src/hooks/use-mobile.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 |
3 | const MOBILE_BREAKPOINT = 768;
4 |
5 | export function useIsMobile() {
6 | const [isMobile, setIsMobile] = React.useState(
7 | undefined
8 | );
9 |
10 | React.useEffect(() => {
11 | const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);
12 | const onChange = () => {
13 | setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
14 | };
15 | mql.addEventListener("change", onChange);
16 | setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
17 | return () => mql.removeEventListener("change", onChange);
18 | }, []);
19 |
20 | return !!isMobile;
21 | }
22 |
--------------------------------------------------------------------------------
/src/hooks/use-scroll.ts:
--------------------------------------------------------------------------------
1 | import { useCallback, useEffect, useState } from "react";
2 |
3 | export default function useScroll(threshold: number) {
4 | const [scrolled, setScrolled] = useState(false);
5 |
6 | const onScroll = useCallback(() => {
7 | setScrolled(window.scrollY > threshold);
8 | }, [threshold]);
9 |
10 | useEffect(() => {
11 | onScroll();
12 | }, [onScroll]);
13 |
14 | useEffect(() => {
15 | window.addEventListener("scroll", onScroll);
16 | return () => window.removeEventListener("scroll", onScroll);
17 | }, [onScroll]);
18 |
19 | return scrolled;
20 | }
21 |
--------------------------------------------------------------------------------
/src/lib/client/safe-action.ts:
--------------------------------------------------------------------------------
1 | import { createSafeActionClient } from "next-safe-action";
2 | import { z } from "zod";
3 | import { authMiddleware } from "../server/auth";
4 |
5 | export const actionClient = createSafeActionClient({
6 | defineMetadataSchema: () =>
7 | z.object({
8 | actionName: z.string(),
9 | }),
10 | handleServerError: (e) => {
11 | console.error("Action error:", e.message);
12 | return {
13 | success: false,
14 | message: e.message,
15 | };
16 | },
17 | });
18 |
19 | export const authActionClient = actionClient.use(authMiddleware);
20 |
--------------------------------------------------------------------------------
/src/lib/client/uploadthing.ts:
--------------------------------------------------------------------------------
1 | import {
2 | generateReactHelpers,
3 | generateUploadButton,
4 | generateUploadDropzone,
5 | } from "@uploadthing/react";
6 |
7 | import type { OurFileRouter } from "~/app/api/uploadthing/core";
8 |
9 | export const { useUploadThing, uploadFiles } =
10 | generateReactHelpers();
11 |
12 | export const UploadButton = generateUploadButton();
13 | export const UploadDropzone = generateUploadDropzone();
14 |
--------------------------------------------------------------------------------
/src/lib/server/auth/cookies.ts:
--------------------------------------------------------------------------------
1 | import { cookies } from "next/headers";
2 |
3 | export async function setSessionTokenCookie(
4 | token: string,
5 | expiresAt: Date
6 | ): Promise {
7 | const cookieStore = await cookies();
8 | cookieStore.set("session", token, {
9 | httpOnly: true,
10 | sameSite: "lax",
11 | secure: process.env.NODE_ENV === "production",
12 | expires: expiresAt,
13 | path: "/",
14 | });
15 | }
16 |
17 | export async function deleteSessionTokenCookie(): Promise {
18 | const cookieStore = await cookies();
19 | cookieStore.set("session", "", {
20 | httpOnly: true,
21 | sameSite: "lax",
22 | secure: process.env.NODE_ENV === "production",
23 | maxAge: 0,
24 | path: "/",
25 | });
26 | }
27 |
--------------------------------------------------------------------------------
/src/lib/server/auth/github.ts:
--------------------------------------------------------------------------------
1 | import { GitHub } from "arctic";
2 |
3 | export const github = new GitHub(
4 | process.env.GITHUB_CLIENT_ID!,
5 | process.env.GITHUB_CLIENT_SECRET!,
6 | null
7 | );
8 |
--------------------------------------------------------------------------------
/src/lib/server/auth/index.ts:
--------------------------------------------------------------------------------
1 | import { generateRandomString, RandomReader } from "@oslojs/crypto/random";
2 | import { createMiddleware } from "next-safe-action";
3 | import { prisma } from "~/lib/server/db";
4 | import { getCurrentSession } from "./session";
5 |
6 | const digits = "0123456789";
7 |
8 | export async function generateEmailVerificationCode(
9 | userId: string,
10 | email: string
11 | ): Promise {
12 | await prisma.emailVerificationCode.deleteMany({
13 | where: {
14 | userId,
15 | },
16 | });
17 | const random: RandomReader = {
18 | read(bytes) {
19 | crypto.getRandomValues(bytes);
20 | },
21 | };
22 | const code = generateRandomString(random, digits, 6);
23 | await prisma.emailVerificationCode.create({
24 | data: {
25 | userId,
26 | email,
27 | code,
28 | expiresAt: new Date(Date.now() + 1000 * 60 * 3), // 3 minutes
29 | },
30 | });
31 | return code;
32 | }
33 |
34 | export async function verifyVerificationCode(
35 | user: { id: string; email: string },
36 | code: string
37 | ): Promise {
38 | return await prisma.$transaction(async (tx) => {
39 | const databaseCode = await tx.emailVerificationCode.findFirst({
40 | where: {
41 | userId: user.id,
42 | },
43 | });
44 |
45 | if (!databaseCode || databaseCode.code !== code) {
46 | return false;
47 | }
48 |
49 | await tx.emailVerificationCode.delete({
50 | where: {
51 | id: databaseCode.id,
52 | },
53 | });
54 |
55 | if (Date.now() > databaseCode.expiresAt.getTime()) {
56 | return false;
57 | }
58 |
59 | if (databaseCode.email !== user.email) {
60 | return false;
61 | }
62 |
63 | return true;
64 | });
65 | }
66 |
67 | export const authMiddleware = createMiddleware().define(async ({ next }) => {
68 | const { session, user } = await getCurrentSession();
69 | if (!session) {
70 | throw new Error("Unauthorized");
71 | }
72 | return next({ ctx: { userId: user.id, sessionId: session.id } });
73 | });
74 |
--------------------------------------------------------------------------------
/src/lib/server/auth/session.ts:
--------------------------------------------------------------------------------
1 | import { sha256 } from "@oslojs/crypto/sha2";
2 | import {
3 | encodeBase32LowerCaseNoPadding,
4 | encodeHexLowerCase,
5 | } from "@oslojs/encoding";
6 | import type { Session, User } from "@prisma/client";
7 | import { cookies } from "next/headers";
8 | import { prisma } from "~/lib/server/db";
9 |
10 | export function generateSessionToken(): string {
11 | const bytes = new Uint8Array(20);
12 | crypto.getRandomValues(bytes);
13 | const token = encodeBase32LowerCaseNoPadding(bytes);
14 | return token;
15 | }
16 |
17 | export async function createSession(
18 | token: string,
19 | userId: string
20 | ): Promise {
21 | const sessionId = encodeHexLowerCase(sha256(new TextEncoder().encode(token)));
22 | const session: Session = {
23 | id: sessionId,
24 | userId,
25 | expiresAt: new Date(Date.now() + 1000 * 60 * 60 * 24 * 30),
26 | };
27 | await prisma.session.create({
28 | data: session,
29 | });
30 | return session;
31 | }
32 |
33 | export async function validateSessionToken(
34 | token: string
35 | ): Promise {
36 | const sessionId = encodeHexLowerCase(sha256(new TextEncoder().encode(token)));
37 | const result = await prisma.session.findUnique({
38 | where: {
39 | id: sessionId,
40 | },
41 | include: {
42 | user: true,
43 | },
44 | });
45 | if (result === null) {
46 | return { session: null, user: null };
47 | }
48 | const { user, ...session } = result;
49 | if (Date.now() >= session.expiresAt.getTime()) {
50 | await prisma.session.delete({ where: { id: sessionId } });
51 | return { session: null, user: null };
52 | }
53 | if (Date.now() >= session.expiresAt.getTime() - 1000 * 60 * 60 * 24 * 15) {
54 | session.expiresAt = new Date(Date.now() + 1000 * 60 * 60 * 24 * 30);
55 | await prisma.session.update({
56 | where: {
57 | id: session.id,
58 | },
59 | data: {
60 | expiresAt: session.expiresAt,
61 | },
62 | });
63 | }
64 | return { session, user };
65 | }
66 |
67 | export const getCurrentSession = async (): Promise => {
68 | const cookieStore = await cookies();
69 | const token = cookieStore.get("session")?.value ?? null;
70 | if (token === null) {
71 | return { session: null, user: null };
72 | }
73 | return validateSessionToken(token);
74 | };
75 |
76 | export async function invalidateSession(sessionId: string): Promise {
77 | await prisma.session.delete({ where: { id: sessionId } });
78 | }
79 |
80 | export async function invalidateAllSessions(userId: string): Promise {
81 | await prisma.session.deleteMany({ where: { userId } });
82 | }
83 |
84 | export type SessionValidationResult =
85 | | { session: Session; user: User }
86 | | { session: null; user: null };
87 |
--------------------------------------------------------------------------------
/src/lib/server/db.ts:
--------------------------------------------------------------------------------
1 | import { PrismaClient } from "@prisma/client";
2 |
3 | declare global {
4 | // eslint-disable-next-line no-var
5 | var prisma: PrismaClient | undefined;
6 | }
7 |
8 | const prisma = global.prisma || new PrismaClient();
9 |
10 | if (process.env.NODE_ENV !== "production") global.prisma = prisma;
11 |
12 | export { prisma };
13 |
--------------------------------------------------------------------------------
/src/lib/server/mail.ts:
--------------------------------------------------------------------------------
1 | import ThanksTemp from "emails/thanks";
2 | import VerificationTemp from "emails/verification";
3 | import { Resend } from "resend";
4 | import { type SendOTPProps, type SendWelcomeEmailProps } from "~/types";
5 | import { generateId } from "../utils";
6 | import { ReactNode } from "react";
7 |
8 | export const resend = new Resend(process.env.RESEND_API_KEY);
9 |
10 | export const sendWelcomeEmail = async ({
11 | toMail,
12 | userName,
13 | }: SendWelcomeEmailProps) => {
14 | const subject = "Thanks for using ChadNext!";
15 | const temp = ThanksTemp({ userName }) as ReactNode;
16 |
17 | await resend.emails.send({
18 | from: `ChadNext App `,
19 | to: toMail,
20 | subject: subject,
21 | headers: {
22 | "X-Entity-Ref-ID": generateId(),
23 | },
24 | react: temp,
25 | text: "",
26 | });
27 | };
28 |
29 | export const sendOTP = async ({ toMail, code, userName }: SendOTPProps) => {
30 | const subject = "OTP for ChadNext";
31 | const temp = VerificationTemp({ userName, code }) as ReactNode;
32 |
33 | await resend.emails.send({
34 | from: `ChadNext App `,
35 | to: toMail,
36 | subject: subject,
37 | headers: {
38 | "X-Entity-Ref-ID": generateId(),
39 | },
40 | react: temp,
41 | text: "",
42 | });
43 | };
44 |
--------------------------------------------------------------------------------
/src/lib/server/payment.ts:
--------------------------------------------------------------------------------
1 | import Stripe from "stripe";
2 | import { freePlan, proPlan } from "~/config/subscription";
3 | import { prisma } from "~/lib/server/db";
4 | import { type UserSubscriptionPlan } from "~/types";
5 |
6 | export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY as string, {
7 | apiVersion: "2025-03-31.basil",
8 | typescript: true,
9 | });
10 |
11 | export async function getUserSubscriptionPlan(
12 | userId: string
13 | ): Promise {
14 | const user = await prisma.user.findFirst({
15 | where: {
16 | id: userId,
17 | },
18 | select: {
19 | stripeSubscriptionId: true,
20 | stripeCurrentPeriodEnd: true,
21 | stripeCustomerId: true,
22 | stripePriceId: true,
23 | },
24 | });
25 |
26 | if (!user) {
27 | throw new Error("User not found");
28 | }
29 |
30 | // Check if user is on a pro plan.
31 | const isPro = Boolean(
32 | user.stripePriceId &&
33 | user.stripeCurrentPeriodEnd?.getTime()! + 86_400_000 > Date.now()
34 | );
35 |
36 | const plan = isPro ? proPlan : freePlan;
37 |
38 | return {
39 | ...plan,
40 | ...user,
41 | stripeCurrentPeriodEnd: user.stripeCurrentPeriodEnd?.getTime()!,
42 | isPro,
43 | stripePriceId: user.stripePriceId || "",
44 | };
45 | }
46 |
--------------------------------------------------------------------------------
/src/lib/server/upload.ts:
--------------------------------------------------------------------------------
1 | import { UTApi } from "uploadthing/server";
2 |
3 | export const utapi = new UTApi();
4 |
--------------------------------------------------------------------------------
/src/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import { generateRandomString, type RandomReader } from "@oslojs/crypto/random";
2 | import { clsx, type ClassValue } from "clsx";
3 | import { twMerge } from "tailwind-merge";
4 |
5 | export function cn(...inputs: ClassValue[]) {
6 | return twMerge(clsx(inputs));
7 | }
8 |
9 | export function nFormatter(num: number, digits?: number) {
10 | if (!num) return "0";
11 | const lookup = [
12 | { value: 1, symbol: "" },
13 | { value: 1e3, symbol: "K" },
14 | { value: 1e6, symbol: "M" },
15 | { value: 1e9, symbol: "G" },
16 | { value: 1e12, symbol: "T" },
17 | { value: 1e15, symbol: "P" },
18 | { value: 1e18, symbol: "E" },
19 | ];
20 | const rx = /\.0+$|(\.[0-9]*[1-9])0+$/;
21 | const item = lookup
22 | .slice()
23 | .reverse()
24 | .find(function (item) {
25 | return num >= item.value;
26 | });
27 | return item
28 | ? (num / item.value).toFixed(digits || 1).replace(rx, "$1") + item.symbol
29 | : "0";
30 | }
31 |
32 | export function hasFileNameSpaces(fileName: string) {
33 | return /\s/.test(fileName);
34 | }
35 | export function formatDate(input: string | number): string {
36 | const date = new Date(input);
37 | return date.toLocaleDateString("en-US", {
38 | month: "long",
39 | day: "numeric",
40 | year: "numeric",
41 | });
42 | }
43 |
44 | export const isOurCdnUrl = (url: string) =>
45 | url?.includes("utfs.io") || url?.includes("uploadthing.com");
46 |
47 | export const getImageKeyFromUrl = (url: string) => {
48 | const parts = url.split("/");
49 | return parts.at(-1);
50 | };
51 |
52 | export class FreePlanLimitError extends Error {
53 | constructor(message = "Upgrade your plan!") {
54 | super(message);
55 | }
56 | }
57 |
58 | export function isRedirectError(error: unknown): boolean {
59 | return (
60 | error !== null &&
61 | typeof error === "object" &&
62 | "digest" in error &&
63 | typeof error.digest === "string" &&
64 | error.digest.includes("NEXT_REDIRECT")
65 | );
66 | }
67 |
68 | const alphanumeric =
69 | "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
70 |
71 | export function generateId(length = 10): string {
72 | const random: RandomReader = {
73 | read(bytes) {
74 | crypto.getRandomValues(bytes);
75 | },
76 | };
77 | return generateRandomString(random, alphanumeric, length);
78 | }
79 |
--------------------------------------------------------------------------------
/src/locales/client.ts:
--------------------------------------------------------------------------------
1 | "use client";
2 | import { createI18nClient } from "next-international/client";
3 |
4 | export const {
5 | useI18n,
6 | useScopedI18n,
7 | I18nProviderClient,
8 | useCurrentLocale,
9 | useChangeLocale,
10 | } = createI18nClient({
11 | en: () => import("./en"),
12 | fr: () => import("./fr"),
13 | });
14 |
--------------------------------------------------------------------------------
/src/locales/en.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | header: {
3 | changelog: "Changelog",
4 | about: "About",
5 | login: "Login",
6 | dashboard: "Dashboard",
7 | },
8 | hero: {
9 | top: "Introducing",
10 | main: "Quick Starter Template for your Next project",
11 | sub: "Packed with all necessary features to get started.",
12 | firstButton: "Get started",
13 | tools: "Built using Great Tools",
14 | on: "on",
15 | },
16 | features: {
17 | top: "Features",
18 | details:
19 | "This template comes with features like Authentication, API routes, File uploading and more in Next.js App dir.",
20 | libs: {
21 | nextjs:
22 | "App dir, Routing, Layouts, API routes, Server Components, Server actions.",
23 | tailwindcss:
24 | "UI components built using Radix UI and styled with Tailwind CSS.",
25 | postgres: "Using Postgres with Prisma ORM, hosted on Vercel Postgres.",
26 | lucia: "Authentication and Authorization using LuciaAuth v3.",
27 | uploadthing: "Upload and preview files effortlessly with UploadThing.",
28 | reactEmail: "Create emails using React Email and Send with Resend.",
29 | internationalization:
30 | "Internationalization support with type-safe Next-International.",
31 | stripe: "Receive and process payments with Stripe.",
32 | vercel: "Production and Preview deployments with Vercel.",
33 | },
34 | aboutMd: "ChadNext also includes Changelog & About page built using",
35 | },
36 | notFound: {
37 | title: "Page Not Found!",
38 | },
39 | } as const;
40 |
--------------------------------------------------------------------------------
/src/locales/fr.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | header: {
3 | changelog: "Journal des modifications",
4 | about: "Environ",
5 | login: "Se connecter",
6 | dashboard: "Tableau de bord",
7 | },
8 | hero: {
9 | top: "Présentation de",
10 | main: "Modèle de démarrage rapide pour votre prochain projet",
11 | sub: "Doté de toutes les fonctionnalités nécessaires pour commencer.",
12 | firstButton: "Commencer",
13 | tools: "Construit à l'aide d'excellents outils",
14 | on: "sur",
15 | },
16 | features: {
17 | top: "Caractéristiques",
18 | details:
19 | "Cette template comprend des fonctionnalités telles que l'authentification, les routes API, le téléchargement de fichiers et bien plus dans le répertoire App de Next.js.",
20 | libs: {
21 | nextjs:
22 | "Répertoire App, routage, mises en page, routes API, composants serveur, actions serveur.",
23 | tailwindcss:
24 | "Composants UI construits avec Radix UI et stylisés avec Tailwind CSS.",
25 | postgres:
26 | "Utilisation de Postgres avec Prisma ORM, hébergé sur Vercel Postgres.",
27 | lucia: "Authentification et autorisation avec LuciaAuth v3.",
28 | uploadthing:
29 | "Téléchargez et prévisualisez des fichiers facilement avec UploadThing.",
30 | reactEmail:
31 | "Créez des e-mails avec React Email et envoyez-les avec Resend.",
32 | internationalization:
33 | "Support d'internationalisation avec Next-International, sécurisé par typage.",
34 | stripe: "Recevez et traitez les paiements avec Stripe.",
35 | vercel: "Déploiements de production et de prévisualisation avec Vercel.",
36 | },
37 | aboutMd:
38 | "ChadNext inclut également une page de journal des modifications et une page À propos, construites avec ",
39 | },
40 | notFound: {
41 | title: "Page non trouvée!",
42 | },
43 | } as const;
44 |
--------------------------------------------------------------------------------
/src/locales/server.ts:
--------------------------------------------------------------------------------
1 | import { createI18nServer } from "next-international/server";
2 |
3 | export const { getI18n, getScopedI18n, getStaticParams, getCurrentLocale } =
4 | createI18nServer({
5 | en: () => import("./en"),
6 | fr: () => import("./fr"),
7 | });
8 |
--------------------------------------------------------------------------------
/src/middleware.ts:
--------------------------------------------------------------------------------
1 | import { createI18nMiddleware } from "next-international/middleware";
2 | import { type NextRequest } from "next/server";
3 |
4 | const I18nMiddleware = createI18nMiddleware({
5 | locales: ["en", "fr"],
6 | defaultLocale: "en",
7 | });
8 |
9 | export function middleware(request: NextRequest) {
10 | return I18nMiddleware(request);
11 | }
12 |
13 | export const config = {
14 | matcher: [
15 | "/((?!api|static|.*\\..*|_next|favicon.ico|sitemap.xml|robots.txt).*)",
16 | ],
17 | };
18 |
--------------------------------------------------------------------------------
/src/types/index.ts:
--------------------------------------------------------------------------------
1 | import { type User } from "@prisma/client";
2 | import { z } from "zod";
3 |
4 | export type CurrentUser = {
5 | id: string;
6 | name: string;
7 | email: string;
8 | picture: string;
9 | };
10 |
11 | export interface payload {
12 | name: string;
13 | email: string;
14 | picture?: string;
15 | }
16 |
17 | export const settingsSchema = z.object({
18 | picture: z.string().url(),
19 | name: z
20 | .string({
21 | required_error: "Please type your name.",
22 | })
23 | .min(3, {
24 | message: "Name must be at least 3 characters.",
25 | })
26 | .max(50, {
27 | message: "Name must be at most 50 characters.",
28 | }),
29 | email: z.string().email(),
30 | shortBio: z.string().optional(),
31 | });
32 |
33 | export type SettingsValues = z.infer;
34 |
35 | export type SubscriptionPlan = {
36 | name: string;
37 | description: string;
38 | stripePriceId: string;
39 | };
40 |
41 | export type UserSubscriptionPlan = SubscriptionPlan &
42 | Pick & {
43 | stripeCurrentPeriodEnd: number;
44 | isPro: boolean;
45 | };
46 |
47 | export interface SendWelcomeEmailProps {
48 | toMail: string;
49 | userName: string;
50 | }
51 |
52 | export interface SendOTPProps extends SendWelcomeEmailProps {
53 | code: string;
54 | }
55 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | const { fontFamily } = require("tailwindcss/defaultTheme");
2 |
3 | /** @type {import('tailwindcss').Config} */
4 | module.exports = {
5 | darkMode: ["class"],
6 | content: ["./src/**/*.{ts,tsx}"],
7 | theme: {
8 | container: {
9 | center: true,
10 | padding: '1.5rem',
11 | screens: {
12 | '2xl': '1400px'
13 | }
14 | },
15 | extend: {
16 | colors: {
17 | border: 'hsl(var(--border))',
18 | input: 'hsl(var(--input))',
19 | ring: 'hsl(var(--ring))',
20 | background: 'hsl(var(--background))',
21 | foreground: 'hsl(var(--foreground))',
22 | primary: {
23 | DEFAULT: 'hsl(var(--primary))',
24 | foreground: 'hsl(var(--primary-foreground))'
25 | },
26 | secondary: {
27 | DEFAULT: 'hsl(var(--secondary))',
28 | foreground: 'hsl(var(--secondary-foreground))'
29 | },
30 | destructive: {
31 | DEFAULT: 'hsl(var(--destructive))',
32 | foreground: 'hsl(var(--destructive-foreground))'
33 | },
34 | muted: {
35 | DEFAULT: 'hsl(var(--muted))',
36 | foreground: 'hsl(var(--muted-foreground))'
37 | },
38 | accent: {
39 | DEFAULT: 'hsl(var(--accent))',
40 | foreground: 'hsl(var(--accent-foreground))'
41 | },
42 | popover: {
43 | DEFAULT: 'hsl(var(--popover))',
44 | foreground: 'hsl(var(--popover-foreground))'
45 | },
46 | card: {
47 | DEFAULT: 'hsl(var(--card))',
48 | foreground: 'hsl(var(--card-foreground))'
49 | },
50 | chart: {
51 | '1': 'hsl(var(--chart-1))',
52 | '2': 'hsl(var(--chart-2))',
53 | '3': 'hsl(var(--chart-3))',
54 | '4': 'hsl(var(--chart-4))',
55 | '5': 'hsl(var(--chart-5))'
56 | },
57 | sidebar: {
58 | DEFAULT: 'hsl(var(--sidebar-background))',
59 | foreground: 'hsl(var(--sidebar-foreground))',
60 | primary: 'hsl(var(--sidebar-primary))',
61 | 'primary-foreground': 'hsl(var(--sidebar-primary-foreground))',
62 | accent: 'hsl(var(--sidebar-accent))',
63 | 'accent-foreground': 'hsl(var(--sidebar-accent-foreground))',
64 | border: 'hsl(var(--sidebar-border))',
65 | ring: 'hsl(var(--sidebar-ring))'
66 | }
67 | },
68 | borderRadius: {
69 | lg: 'var(--radius)',
70 | md: 'calc(var(--radius) - 2px)',
71 | sm: 'calc(var(--radius) - 4px)'
72 | },
73 | fontFamily: {
74 | sans: [
75 | 'var(--font-sans)',
76 | ...fontFamily.sans
77 | ],
78 | heading: [
79 | 'var(--font-heading)',
80 | ...fontFamily.sans
81 | ]
82 | },
83 | keyframes: {
84 | 'caret-blink': {
85 | '0%,70%,100%': {
86 | opacity: '1'
87 | },
88 | '20%,50%': {
89 | opacity: '0'
90 | }
91 | },
92 | 'accordion-down': {
93 | from: {
94 | height: '0'
95 | },
96 | to: {
97 | height: 'var(--radix-accordion-content-height)'
98 | }
99 | },
100 | 'accordion-up': {
101 | from: {
102 | height: 'var(--radix-accordion-content-height)'
103 | },
104 | to: {
105 | height: '0'
106 | }
107 | }
108 | },
109 | animation: {
110 | 'caret-blink': 'caret-blink 1.25s ease-out infinite',
111 | 'accordion-down': 'accordion-down 0.2s ease-out',
112 | 'accordion-up': 'accordion-up 0.2s ease-out'
113 | }
114 | }
115 | },
116 | plugins: [require("tailwindcss-animate"), require("@tailwindcss/typography")],
117 | };
118 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es2017",
4 | "types": [
5 | "@serwist/next/typings"
6 | ],
7 | "lib": [
8 | "dom",
9 | "dom.iterable",
10 | "esnext",
11 | "webworker"
12 | ],
13 | "allowJs": true,
14 | "skipLibCheck": true,
15 | "strict": true,
16 | "forceConsistentCasingInFileNames": true,
17 | "noEmit": true,
18 | "esModuleInterop": true,
19 | "module": "esnext",
20 | "moduleResolution": "node",
21 | "resolveJsonModule": true,
22 | "isolatedModules": true,
23 | "jsx": "preserve",
24 | "incremental": true,
25 | "plugins": [
26 | {
27 | "name": "next"
28 | }
29 | ],
30 | "baseUrl": ".",
31 | "paths": {
32 | "~/*": [
33 | "./src/*"
34 | ],
35 | "content": [
36 | "./.velite"
37 | ]
38 | }
39 | },
40 | "include": [
41 | "next-env.d.ts",
42 | "**/*.ts",
43 | "**/*.tsx",
44 | ".next/types/**/*.ts"
45 | ],
46 | "exclude": [
47 | "node_modules",
48 | ".next",
49 | ".velite",
50 | "public/sw.js"
51 | ]
52 | }
--------------------------------------------------------------------------------
/velite.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig, s } from 'velite'
2 |
3 | // `s` is extended from Zod with some custom schemas,
4 | // you can also import re-exported `z` from `velite` if you don't need these extension schemas.
5 |
6 | export default defineConfig({
7 | root: './src/content',
8 | collections: {
9 | changes: {
10 | name: 'Change', // collection type name
11 | pattern: 'changelog/**/*.md', // content files glob pattern
12 | schema: s
13 | .object({
14 | title: s.string(),
15 | date: s.isodate(), // input Date-like string, output ISO Date string.
16 | content: s.markdown() // transform markdown to html
17 | })
18 | },
19 | abouts: {
20 | name: 'About', // collection type name
21 | pattern: 'about/**/*.md', // content files glob pattern
22 | schema: s
23 | .object({
24 | title: s.string(),
25 | content: s.markdown() // transform markdown to html
26 | })
27 | },
28 | }
29 | })
--------------------------------------------------------------------------------