├── .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 | ![ChadNext – Quick Starter Template for your Next.js project](https://repository-images.githubusercontent.com/644861240/7dfaac30-9ee9-4e52-a4f2-daa2b1944d4f) 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 | [![Deploy with Vercel](https://vercel.com/button)](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 |
49 | 50 | 51 | ID 52 | 53 |
54 | 55 | 56 |
57 |
58 | 59 |
60 | 61 | ( 65 | 66 | Name 67 | 68 | 69 | 70 | 71 | 72 | )} 73 | /> 74 | ( 78 | 79 | Domain 80 | 81 | 82 | 83 | 84 | 85 | )} 86 | /> 87 | 96 | 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 |