├── src ├── server │ ├── db │ │ ├── schemas │ │ │ ├── index.ts │ │ │ └── auth.ts │ │ └── index.ts │ └── Actions │ │ └── mailAction.ts ├── app │ ├── page.info.ts │ ├── (auth) │ │ ├── sign-in │ │ │ ├── page.info.ts │ │ │ └── page.tsx │ │ ├── sign-up │ │ │ ├── page.info.ts │ │ │ └── page.tsx │ │ ├── email-verified │ │ │ ├── page.info.ts │ │ │ └── page.tsx │ │ ├── forgot-password │ │ │ ├── page.info.ts │ │ │ └── page.tsx │ │ └── reset-password │ │ │ ├── page.info.ts │ │ │ └── page.tsx │ ├── (dashboard) │ │ ├── admin │ │ │ ├── loading.tsx │ │ │ ├── page.info.ts │ │ │ ├── page.tsx │ │ │ ├── not-found.tsx │ │ │ └── error.tsx │ │ └── user │ │ │ ├── loading.tsx │ │ │ ├── page.info.ts │ │ │ ├── page.tsx │ │ │ ├── not-found.tsx │ │ │ └── error.tsx │ ├── favicon.ico │ ├── api │ │ └── auth │ │ │ └── [...all] │ │ │ ├── route.info.ts │ │ │ └── route.ts │ ├── layout.tsx │ └── page.tsx ├── styles │ ├── index.ts │ └── globals.css ├── lib │ ├── utils.ts │ └── auth │ │ ├── auth-client.ts │ │ ├── schema.ts │ │ └── auth.ts ├── components │ ├── providers │ │ └── ReactQuery.tsx │ ├── ui │ │ ├── sonner.tsx │ │ ├── label.tsx │ │ ├── input.tsx │ │ ├── avatar.tsx │ │ ├── button.tsx │ │ ├── card.tsx │ │ ├── form.tsx │ │ └── dropdown-menu.tsx │ ├── custom │ │ ├── LogoutButton.tsx │ │ ├── ReactPortal.tsx │ │ └── logo.tsx │ └── section │ │ ├── Footer.tsx │ │ └── Header.tsx ├── routes │ ├── index.ts │ ├── hooks.ts │ ├── utils.ts │ ├── README.md │ └── makeRoute.tsx └── env.ts ├── postcss.config.mjs ├── declarative-routing.config.json ├── public ├── vercel.svg ├── images │ └── 77627641.jpg ├── window.svg ├── file.svg ├── globe.svg └── next.svg ├── .github ├── delete-merged-branch-config.yml ├── workflows │ ├── delete-merged-branches.yml │ └── labeler.yml ├── auto_assign.yml └── labeler.yml ├── drizzle.config.ts ├── .vscode └── settings.json ├── _example_env ├── components.json ├── .gitignore ├── tsconfig.json ├── next.config.ts ├── prettier.config.mjs ├── renovate.json ├── README.md ├── eslint.config.mjs └── package.json /src/server/db/schemas/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./auth"; 2 | -------------------------------------------------------------------------------- /postcss.config.mjs: -------------------------------------------------------------------------------- 1 | const config = { 2 | plugins: ["@tailwindcss/postcss"], 3 | }; 4 | 5 | export default config; 6 | -------------------------------------------------------------------------------- /declarative-routing.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "mode": "nextjs", 3 | "src": "./src/app", 4 | "routes": "./src/routes" 5 | } 6 | -------------------------------------------------------------------------------- /src/app/page.info.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | export const Route = { 4 | name: "Home", 5 | params: z.object({}), 6 | }; 7 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.github/delete-merged-branch-config.yml: -------------------------------------------------------------------------------- 1 | exclude: 2 | - dev 3 | - main 4 | - dev* 5 | - feature-* 6 | - release 7 | delete_closed_pr: false 8 | -------------------------------------------------------------------------------- /src/app/(auth)/sign-in/page.info.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | export const Route = { 4 | name: "AuthSignIn", 5 | params: z.object({}), 6 | }; 7 | -------------------------------------------------------------------------------- /src/app/(auth)/sign-up/page.info.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | export const Route = { 4 | name: "AuthSignUp", 5 | params: z.object({}), 6 | }; 7 | -------------------------------------------------------------------------------- /src/app/(dashboard)/admin/loading.tsx: -------------------------------------------------------------------------------- 1 | export default function Loading() { 2 | // Or a custom loading skeleton component 3 | return

Loading...

; 4 | } 5 | -------------------------------------------------------------------------------- /src/app/(dashboard)/user/loading.tsx: -------------------------------------------------------------------------------- 1 | export default function Loading() { 2 | // Or a custom loading skeleton component 3 | return

Loading...

; 4 | } 5 | -------------------------------------------------------------------------------- /src/app/(dashboard)/user/page.info.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | export const Route = { 4 | name: "DashboardUser", 5 | params: z.object({}), 6 | }; 7 | -------------------------------------------------------------------------------- /src/app/(dashboard)/admin/page.info.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | export const Route = { 4 | name: "DashboardAdmin", 5 | params: z.object({}), 6 | }; 7 | -------------------------------------------------------------------------------- /src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Its-Satyajit/nextjs-typescript-tailwind-shadcn-postgresql-drizzle-orm-better-auth-template/HEAD/src/app/favicon.ico -------------------------------------------------------------------------------- /src/app/(auth)/email-verified/page.info.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | export const Route = { 4 | name: "AuthEmailVerified", 5 | params: z.object({}), 6 | }; 7 | -------------------------------------------------------------------------------- /src/app/(auth)/forgot-password/page.info.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | export const Route = { 4 | name: "AuthForgotPassword", 5 | params: z.object({}), 6 | }; 7 | -------------------------------------------------------------------------------- /src/app/(auth)/reset-password/page.info.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | export const Route = { 4 | name: "AuthResetPassword", 5 | params: z.object({}), 6 | }; 7 | -------------------------------------------------------------------------------- /public/images/77627641.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Its-Satyajit/nextjs-typescript-tailwind-shadcn-postgresql-drizzle-orm-better-auth-template/HEAD/public/images/77627641.jpg -------------------------------------------------------------------------------- /src/app/(dashboard)/admin/page.tsx: -------------------------------------------------------------------------------- 1 | export default function AdminPage() { 2 | return ( 3 |
4 |

this is the admin dashboard

5 |
6 | ); 7 | } 8 | -------------------------------------------------------------------------------- /src/app/(dashboard)/user/page.tsx: -------------------------------------------------------------------------------- 1 | export default function UserDashBoard() { 2 | return ( 3 |
4 |

this is the user dashboard

5 |
6 | ); 7 | } 8 | -------------------------------------------------------------------------------- /src/app/api/auth/[...all]/route.info.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | export const Route = { 4 | name: "ApiAuthAll", 5 | params: z.object({ 6 | all: z.string().array(), 7 | }), 8 | }; 9 | -------------------------------------------------------------------------------- /src/app/api/auth/[...all]/route.ts: -------------------------------------------------------------------------------- 1 | import { toNextJsHandler } from "better-auth/next-js"; 2 | 3 | import { auth } from "@/lib/auth/auth"; 4 | 5 | export const { POST, GET } = toNextJsHandler(auth); 6 | -------------------------------------------------------------------------------- /src/styles/index.ts: -------------------------------------------------------------------------------- 1 | import "@fontsource-variable/inter"; 2 | import "@fontsource-variable/inter/wght-italic.css"; 3 | import "@fontsource-variable/inter/wght.css"; 4 | 5 | import "./globals.css"; 6 | -------------------------------------------------------------------------------- /src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { type ClassValue, clsx } from "clsx"; 2 | import { twMerge } from "tailwind-merge"; 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)); 6 | } 7 | -------------------------------------------------------------------------------- /drizzle.config.ts: -------------------------------------------------------------------------------- 1 | import env from "@/env"; 2 | import type { Config } from "drizzle-kit"; 3 | 4 | export default { 5 | schema: "./src/server/db/schemas/", 6 | dialect: "postgresql", 7 | dbCredentials: { 8 | url: env.DATABASE_URL, 9 | }, 10 | } satisfies Config; 11 | -------------------------------------------------------------------------------- /src/app/(dashboard)/admin/not-found.tsx: -------------------------------------------------------------------------------- 1 | import { Home } from "@/routes"; 2 | 3 | export default function NotFound() { 4 | return ( 5 |
6 |

Not Found

7 |

Could not find requested resource

8 | Return Home 9 |
10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /src/app/(dashboard)/user/not-found.tsx: -------------------------------------------------------------------------------- 1 | import { Home } from "@/routes"; 2 | 3 | export default function NotFound() { 4 | return ( 5 |
6 |

Not Found

7 |

Could not find requested resource

8 | Return Home 9 |
10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib", 3 | "cSpell.words": [ 4 | "elems", 5 | "requireds", 6 | "Satyajit", 7 | "Shadcn", 8 | "sonner", 9 | "trivago" 10 | ], 11 | "prettier.configPath": "./prettier.config.mjs", 12 | "eslint.useFlatConfig": true 13 | } 14 | -------------------------------------------------------------------------------- /src/lib/auth/auth-client.ts: -------------------------------------------------------------------------------- 1 | import env from "@/env"; 2 | import { createAuthClient } from "better-auth/react"; 3 | 4 | export const { 5 | signIn, 6 | signUp, 7 | useSession, 8 | signOut, 9 | forgetPassword, 10 | resetPassword, 11 | } = createAuthClient({ 12 | baseURL: env.NEXT_PUBLIC_BETTER_AUTH_URL, 13 | }); 14 | -------------------------------------------------------------------------------- /.github/workflows/delete-merged-branches.yml: -------------------------------------------------------------------------------- 1 | name: delete branch on close pr 2 | on: 3 | pull_request: 4 | types: [closed] 5 | 6 | jobs: 7 | delete-branch: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: delete branch 11 | uses: SvanBoxel/delete-merged-branch@main 12 | env: 13 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 14 | -------------------------------------------------------------------------------- /public/window.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/file.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/providers/ReactQuery.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; 4 | 5 | const queryClient = new QueryClient(); 6 | 7 | type Props = { 8 | children: React.ReactNode; 9 | }; 10 | 11 | export default function ReactQueryProvider({ children }: Props) { 12 | return ( 13 | {children} 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /.github/workflows/labeler.yml: -------------------------------------------------------------------------------- 1 | name: PR Labeler 2 | permissions: 3 | contents: read 4 | pull-requests: write 5 | 6 | on: 7 | pull_request: 8 | types: [opened, synchronize, reopened] 9 | 10 | jobs: 11 | label: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout Code 15 | uses: actions/checkout@v4 16 | 17 | - name: Run Labeler 18 | uses: actions/labeler@v5 19 | with: 20 | repo-token: ${{ secrets.GITHUB_TOKEN }} 21 | -------------------------------------------------------------------------------- /.github/auto_assign.yml: -------------------------------------------------------------------------------- 1 | # Set to true to add reviewers to pull requests 2 | addReviewers: true 3 | 4 | # Set to true to add assignees to pull requests 5 | addAssignees: true 6 | 7 | # A list of reviewers to be added to pull requests (GitHub user name) 8 | reviewers: 9 | - Its-Satyajit 10 | # A list of keywords to be skipped the process that add reviewers if pull requests include it 11 | skipKeywords: 12 | - wip 13 | 14 | # A number of reviewers added to the pull request 15 | # Set 0 to add all the reviewers (default: 0) 16 | numberOfReviewers: 0 17 | -------------------------------------------------------------------------------- /_example_env: -------------------------------------------------------------------------------- 1 | #Server 2 | NODE_ENV=development 3 | DATABASE_URL=postgresql://postgres:password@localhost:5432/default 4 | BETTER_AUTH_SECRET=secret 5 | BETTER_AUTH_URL=http://localhost:3000 6 | REACT_EDITOR=atom 7 | SKIP_BUILD_CHECKS=false 8 | 9 | 10 | MAIL_HOST=gmail 11 | MAIL_USERNAME=email@gmail.com 12 | MAIL_PASSWORD=password 13 | MAIL_FROM=email@gmail.com 14 | EMAIL_VERIFICATION_CALLBACK_URL=http://localhost:3000 15 | 16 | GITHUB_CLIENT_ID=secret 17 | GITHUB_CLIENT_SECRET=secret 18 | 19 | #Client 20 | NEXT_PUBLIC_BETTER_AUTH_URL=http://localhost:3000 -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "src/styles/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 | } 22 | -------------------------------------------------------------------------------- /src/server/db/index.ts: -------------------------------------------------------------------------------- 1 | import env from "@/env"; 2 | import { drizzle } from "drizzle-orm/postgres-js"; 3 | import postgres from "postgres"; 4 | 5 | import * as schema from "./schemas"; 6 | 7 | /** 8 | * Cache the database connection in development. This avoids creating a new connection on every HMR 9 | * update. 10 | */ 11 | const globalForDb = globalThis as unknown as { 12 | conn: postgres.Sql | undefined; 13 | }; 14 | 15 | const conn = globalForDb.conn ?? postgres(env.DATABASE_URL); 16 | if (env.NODE_ENV !== "production") globalForDb.conn = conn; 17 | 18 | export const db = drizzle(conn, { schema }); 19 | -------------------------------------------------------------------------------- /.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.* 7 | .yarn/* 8 | !.yarn/patches 9 | !.yarn/plugins 10 | !.yarn/releases 11 | !.yarn/versions 12 | 13 | # testing 14 | /coverage 15 | 16 | # next.js 17 | /.next/ 18 | /out/ 19 | 20 | # production 21 | /build 22 | 23 | # misc 24 | .DS_Store 25 | *.pem 26 | 27 | # debug 28 | npm-debug.log* 29 | yarn-debug.log* 30 | yarn-error.log* 31 | .pnpm-debug.log* 32 | 33 | # env files (can opt-in for committing if needed) 34 | .env* 35 | 36 | # vercel 37 | .vercel 38 | 39 | # typescript 40 | *.tsbuildinfo 41 | next-env.d.ts 42 | -------------------------------------------------------------------------------- /src/app/(dashboard)/admin/error.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useEffect } from "react"; 4 | 5 | export default function Error({ 6 | error, 7 | reset, 8 | }: { 9 | error: Error & { digest?: string }; 10 | reset: () => void; 11 | }) { 12 | useEffect(() => { 13 | // Log the error to an error reporting service 14 | console.error(error); 15 | }, [error]); 16 | 17 | return ( 18 |
19 |

Something went wrong!

20 | 28 |
29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /src/app/(dashboard)/user/error.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useEffect } from "react"; 4 | 5 | export default function Error({ 6 | error, 7 | reset, 8 | }: { 9 | error: Error & { digest?: string }; 10 | reset: () => void; 11 | }) { 12 | useEffect(() => { 13 | // Log the error to an error reporting service 14 | console.error(error); 15 | }, [error]); 16 | 17 | return ( 18 |
19 |

Something went wrong!

20 | 28 |
29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /src/components/ui/sonner.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useTheme } from "next-themes"; 4 | 5 | import { Toaster as Sonner, type ToasterProps } from "sonner"; 6 | 7 | const Toaster = ({ ...props }: ToasterProps) => { 8 | const { theme = "system" } = useTheme(); 9 | 10 | return ( 11 | 23 | ); 24 | }; 25 | 26 | export { Toaster }; 27 | -------------------------------------------------------------------------------- /src/app/(auth)/email-verified/page.tsx: -------------------------------------------------------------------------------- 1 | import { Home } from "@/routes"; 2 | 3 | import { buttonVariants } from "@/components/ui/button"; 4 | 5 | export default async function EmailVerifiedPage() { 6 | return ( 7 |
8 |

9 | Email Verified! 10 |

11 |

12 | Your email has been successfully verified. 13 |

14 | 19 | Go to home 20 | 21 |
22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /src/components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | 5 | import * as LabelPrimitive from "@radix-ui/react-label"; 6 | 7 | import { cn } from "@/lib/utils"; 8 | 9 | function Label({ 10 | className, 11 | ...props 12 | }: React.ComponentProps) { 13 | return ( 14 | 22 | ); 23 | } 24 | 25 | export { Label }; 26 | -------------------------------------------------------------------------------- /src/components/custom/LogoutButton.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useRouter } from "next/navigation"; 4 | 5 | import { Home } from "@/routes"; 6 | 7 | import { signOut } from "@/lib/auth/auth-client"; 8 | 9 | import { Button } from "../ui/button"; 10 | 11 | interface Props { 12 | className?: string; 13 | } 14 | export default function LogoutButton({ className }: Props) { 15 | const router = useRouter(); 16 | return ( 17 | 30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2017", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "noEmit": true, 9 | "esModuleInterop": true, 10 | "erasableSyntaxOnly": true, 11 | "module": "esnext", 12 | "moduleResolution": "bundler", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true, 17 | "plugins": [ 18 | { 19 | "name": "next" 20 | } 21 | ], 22 | "paths": { 23 | "@/*": ["./src/*"], 24 | "$/*": ["./"] 25 | } 26 | }, 27 | "include": [ 28 | "next-env.d.ts", 29 | "**/*.ts", 30 | "**/*.tsx", 31 | "**/*.js", 32 | "**/*.mjs", 33 | ".next/types/**/*.ts" 34 | ], 35 | "exclude": ["node_modules"] 36 | } 37 | -------------------------------------------------------------------------------- /next.config.ts: -------------------------------------------------------------------------------- 1 | import type { NextConfig } from "next"; 2 | 3 | import env from "@/env"; 4 | 5 | const nextConfig: NextConfig = { 6 | experimental: { 7 | reactCompiler: env.NODE_ENV === "production", 8 | 9 | turbo: { 10 | resolveExtensions: [".ts", ".tsx", ".js", ".jsx", ".mjs"], 11 | }, 12 | }, 13 | eslint: { 14 | ignoreDuringBuilds: env.SKIP_BUILD_CHECKS === "true", 15 | }, 16 | typescript: { 17 | ignoreBuildErrors: env.SKIP_BUILD_CHECKS === "true", 18 | }, 19 | images: { 20 | remotePatterns: [ 21 | { 22 | protocol: "https", 23 | hostname: "images.unsplash.com", 24 | }, 25 | { 26 | hostname: "avatars.githubusercontent.com", 27 | }, 28 | { 29 | protocol: "http", 30 | hostname: "localhost", 31 | }, 32 | ], 33 | }, 34 | }; 35 | 36 | export default nextConfig; 37 | -------------------------------------------------------------------------------- /prettier.config.mjs: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-anonymous-default-export */ 2 | /** @type {import('prettier').Config & import('prettier-plugin-tailwindcss').PluginOptions} */ 3 | export default { 4 | plugins: [ 5 | "@trivago/prettier-plugin-sort-imports", 6 | "prettier-plugin-organize-attributes", 7 | "prettier-plugin-tailwindcss", 8 | ], 9 | 10 | importOrder: [ 11 | "^server-only$", 12 | "^client-only$", 13 | "^@/components/shared/providers/ReactScan$", 14 | "^react-scan$", 15 | "^(react|next)", 16 | "", 17 | "^@/app/(.*)$", 18 | "^@/features/(.*)$", 19 | "^@/components/(.*)$", 20 | "^@/data/(.*)$", 21 | "^@/lib/(.*)$", 22 | "^@/hooks/(.*)$", 23 | "^@/types/(.*)$", 24 | "^@/styles/(.*)$", 25 | "^[./]", 26 | ], 27 | importOrderSeparation: true, 28 | importOrderSortSpecifiers: true, 29 | importOrderGroupNamespaceSpecifiers: true, 30 | }; 31 | -------------------------------------------------------------------------------- /public/globe.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/ui/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import { cn } from "@/lib/utils"; 4 | 5 | function Input({ className, type, ...props }: React.ComponentProps<"input">) { 6 | return ( 7 | 18 | ); 19 | } 20 | 21 | export { Input }; 22 | -------------------------------------------------------------------------------- /src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | 3 | import env from "@/env"; 4 | import "@/styles"; 5 | import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; 6 | 7 | import ReactQueryProvider from "@/components/providers/ReactQuery"; 8 | import Footer from "@/components/section/Footer"; 9 | import Header from "@/components/section/Header"; 10 | import { Toaster } from "@/components/ui/sonner"; 11 | 12 | export const metadata: Metadata = { 13 | title: "Create Next App", 14 | description: "Generated by create next app", 15 | }; 16 | 17 | export default function RootLayout({ 18 | children, 19 | }: Readonly<{ 20 | children: React.ReactNode; 21 | }>) { 22 | return ( 23 | 24 | 25 | 26 |
27 |
28 |
{children}
29 |
30 |
31 | {env.NODE_ENV === "development" ? : null} 32 | 33 |
34 | 35 | 36 | ); 37 | } 38 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:recommended" 4 | ], 5 | "baseBranches": [ 6 | "dev" 7 | ], 8 | "packageRules": [ 9 | { 10 | "matchDepTypes": ["dependencies", "devDependencies"], 11 | "matchUpdateTypes": ["major"], 12 | "automerge": false, 13 | "enabled": true 14 | }, 15 | 16 | { 17 | "matchDepTypes": ["devDependencies"], 18 | "automerge": true, 19 | "matchUpdateTypes": [ 20 | "minor", 21 | "patch" 22 | ] 23 | }, 24 | 25 | { 26 | "matchDepTypes": ["dependencies"], 27 | "automerge": true, 28 | "matchUpdateTypes": ["patch"], 29 | "stabilityDays": 3 30 | }, 31 | 32 | { 33 | "matchDepTypes": ["dependencies"], 34 | "matchUpdateTypes": ["minor"], 35 | "automerge": false 36 | }, 37 | { 38 | "matchDepTypes": ["dependencies", "devDependencies"], 39 | "matchUpdateTypes": ["patch", "minor"], 40 | "groupName": "minor-patch-updates", 41 | "groupSlug": "minor-patch-updates", 42 | "automerge": true 43 | } 44 | ], 45 | "dependencyDashboard": true, 46 | "schedule": ["every weekend"], 47 | "timezone": "Asia/Kolkata", 48 | "branchPrefix": "renovate/", 49 | "labels": [ 50 | "dependencies", 51 | "auto-update" 52 | ] 53 | } 54 | -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/ui/avatar.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | 5 | import * as AvatarPrimitive from "@radix-ui/react-avatar"; 6 | 7 | import { cn } from "@/lib/utils"; 8 | 9 | function Avatar({ 10 | className, 11 | ...props 12 | }: React.ComponentProps) { 13 | return ( 14 | 22 | ); 23 | } 24 | 25 | function AvatarImage({ 26 | className, 27 | ...props 28 | }: React.ComponentProps) { 29 | return ( 30 | 35 | ); 36 | } 37 | 38 | function AvatarFallback({ 39 | className, 40 | ...props 41 | }: React.ComponentProps) { 42 | return ( 43 | 51 | ); 52 | } 53 | 54 | export { Avatar, AvatarImage, AvatarFallback }; 55 | -------------------------------------------------------------------------------- /src/components/custom/ReactPortal.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useEffect, useState } from "react"; 4 | import { createPortal } from "react-dom"; 5 | 6 | 7 | 8 | function createWrapperAndAppendToBody(wrapperId: string): HTMLElement { 9 | const wrapperElement = document.createElement("div"); 10 | wrapperElement.setAttribute("id", wrapperId); 11 | document.body.appendChild(wrapperElement); 12 | return wrapperElement; 13 | } 14 | 15 | interface ReactPortalProps { 16 | children: React.ReactNode; 17 | wrapperId?: string; 18 | } 19 | 20 | const ReactPortal: React.FC = ({ 21 | children, 22 | wrapperId = crypto.randomUUID(), 23 | }) => { 24 | const [wrapperElement, setWrapperElement] = useState( 25 | null, 26 | ); 27 | 28 | useEffect(() => { 29 | let isMounted = true; 30 | let element = document.getElementById(wrapperId); 31 | if (!element) { 32 | element = createWrapperAndAppendToBody(wrapperId); 33 | } 34 | if (isMounted) { 35 | setWrapperElement(element); 36 | } 37 | return () => { 38 | isMounted = false; 39 | if (element?.parentNode) { 40 | element.parentNode.removeChild(element); 41 | } 42 | }; 43 | }, [wrapperId]); 44 | 45 | if (!wrapperElement) return null; 46 | return createPortal(children, wrapperElement); 47 | }; 48 | 49 | export default ReactPortal; 50 | -------------------------------------------------------------------------------- /src/server/Actions/mailAction.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import "server-only"; 4 | 5 | import env from "@/env"; 6 | import nodemailer from "nodemailer"; 7 | import winston from "winston"; 8 | 9 | const logger = winston.createLogger({ 10 | level: "debug", 11 | format: winston.format.json(), 12 | transports: [new winston.transports.Console()], 13 | }); 14 | 15 | if (env.NODE_ENV !== "production") { 16 | logger.add( 17 | new winston.transports.Console({ 18 | format: winston.format.simple(), 19 | }), 20 | ); 21 | } 22 | 23 | export async function sendMail({ 24 | to, 25 | subject, 26 | html, 27 | }: { 28 | to: string; 29 | subject: string; 30 | html: string; 31 | }): Promise { 32 | const transporter = nodemailer.createTransport({ 33 | service: process.env.MAIL_HOST, 34 | auth: { user: env.MAIL_USERNAME, pass: env.MAIL_PASSWORD }, 35 | }); 36 | 37 | const mailOptions = { 38 | from: env.MAIL_FROM, 39 | to, 40 | subject, 41 | html, 42 | }; 43 | 44 | logger.info(`Sending mail to - ${to}`); 45 | 46 | try { 47 | const info = await transporter.sendMail(mailOptions); 48 | logger.info(`Email sent: ${info.response}`); 49 | } catch (error: unknown) { 50 | if (error instanceof Error) { 51 | logger.error(`Error sending email: ${error.message}`); 52 | } else { 53 | logger.error("An unknown error occurred while sending the email."); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/lib/auth/schema.ts: -------------------------------------------------------------------------------- 1 | import { object, string } from "zod"; 2 | 3 | const getPasswordSchema = (type: "password" | "confirmPassword") => 4 | string({ required_error: `${type} is required` }) 5 | .min(8, `${type} must be atleast 8 characters`) 6 | .max(32, `${type} can not exceed 32 characters`); 7 | 8 | const getEmailSchema = () => 9 | string({ required_error: "Email is required" }) 10 | .min(1, "Email is required") 11 | .email("Invalid email"); 12 | 13 | const getNameSchema = () => 14 | string({ required_error: "Name is required" }) 15 | .min(1, "Name is required") 16 | .max(50, "Name must be less than 50 characters"); 17 | 18 | export const signUpSchema = object({ 19 | name: getNameSchema(), 20 | email: getEmailSchema(), 21 | password: getPasswordSchema("password"), 22 | confirmPassword: getPasswordSchema("confirmPassword"), 23 | }).refine((data) => data.password === data.confirmPassword, { 24 | message: "Passwords don't match", 25 | path: ["confirmPassword"], 26 | }); 27 | 28 | export const signInSchema = object({ 29 | email: getEmailSchema(), 30 | password: getPasswordSchema("password"), 31 | }); 32 | 33 | export const forgotPasswordSchema = object({ 34 | email: getEmailSchema(), 35 | }); 36 | 37 | export const resetPasswordSchema = object({ 38 | password: getPasswordSchema("password"), 39 | confirmPassword: getPasswordSchema("confirmPassword"), 40 | }).refine((data) => data.password === data.confirmPassword, { 41 | message: "Passwords don't match", 42 | path: ["confirmPassword"], 43 | }); 44 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | # or 12 | pnpm dev 13 | # or 14 | bun dev 15 | ``` 16 | 17 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 18 | 19 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. 20 | 21 | This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. 22 | 23 | ## Learn More 24 | 25 | To learn more about Next.js, take a look at the following resources: 26 | 27 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 28 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 29 | 30 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! 31 | 32 | ## Deploy on Vercel 33 | 34 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 35 | 36 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. 37 | -------------------------------------------------------------------------------- /src/components/section/Footer.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | 3 | export default function Footer() { 4 | return ( 5 |
6 |
7 |
8 | © {new Date().getFullYear()}{" "} 9 | 14 | Its-Satyajit 15 | 16 |
17 |
18 | Made with ❤️ by 19 | 24 | {" "} 25 | Its-Satyajit 26 | {" "} 27 | and the community. 28 |
29 | 30 |
31 | 35 | Documentation 36 | 37 | 41 | GitHub 42 | 43 |
44 |
45 |
46 | ); 47 | } 48 | -------------------------------------------------------------------------------- /.github/labeler.yml: -------------------------------------------------------------------------------- 1 | # Add 'backend' label to changes in backend-related files 2 | backend: 3 | - changed-files: 4 | - any-glob-to-any-file: 5 | - "src/lib/**" 6 | - "src/routes/**" 7 | - "src/types/**" 8 | - "src/env.ts" 9 | 10 | # Add 'frontend' label to changes in frontend-related files 11 | frontend: 12 | - changed-files: 13 | - any-glob-to-any-file: 14 | - "src/app/**" 15 | - "src/components/**" 16 | - "src/features/**" 17 | - "src/data/**" 18 | 19 | # Add 'utilities' label to changes in utility functions 20 | utilities: 21 | - changed-files: 22 | - any-glob-to-any-file: 23 | - "src/lib/utilities/**" 24 | 25 | # Add 'mock-data' label to changes in mock data 26 | mock-data: 27 | - changed-files: 28 | - any-glob-to-any-file: 29 | - "src/data/mock/**" 30 | 31 | # Add 'tests' label to changes in test files 32 | tests: 33 | - changed-files: 34 | - any-glob-to-any-file: 35 | - "tests/**" 36 | 37 | # Add 'documentation' label to changes in documentation files 38 | documentation: 39 | - changed-files: 40 | - any-glob-to-any-file: 41 | - "docs/**" 42 | - "**/*.md" 43 | 44 | # Add 'ci/cd' label to changes in CI/CD or GitHub workflow files 45 | ci/cd: 46 | - changed-files: 47 | - any-glob-to-any-file: 48 | - ".github/workflows/**" 49 | 50 | # Add 'dependencies' label to changes in dependency files 51 | dependencies: 52 | - changed-files: 53 | - any-glob-to-any-file: 54 | - "package.json" 55 | - "package-lock.json" 56 | - "pnpm-lock.yaml" 57 | - "yarn.lock" 58 | 59 | # Add 'environment' label to changes in environment configuration 60 | environment: 61 | - changed-files: 62 | - any-glob-to-any-file: 63 | - "src/env.ts" 64 | 65 | # Add 'configuration' label to changes in configuration files 66 | configuration: 67 | - changed-files: 68 | - any-glob-to-any-file: 69 | - ".eslintrc.js" 70 | - ".prettierrc" 71 | - "tsconfig.json" 72 | -------------------------------------------------------------------------------- /src/server/db/schemas/auth.ts: -------------------------------------------------------------------------------- 1 | import { boolean, pgEnum, pgTable, text, timestamp } from "drizzle-orm/pg-core"; 2 | 3 | export const userRoleEnums = pgEnum("Role", ["user", "admin", "superAdmin"]); 4 | 5 | export const user = pgTable("user", { 6 | id: text("id").primaryKey(), 7 | name: text("name").notNull(), 8 | email: text("email").notNull().unique(), 9 | emailVerified: boolean("email_verified").notNull(), 10 | image: text("image"), 11 | createdAt: timestamp("created_at").notNull(), 12 | updatedAt: timestamp("updated_at").notNull(), 13 | role: userRoleEnums("role").default("user").notNull(), 14 | banned: boolean("banned"), 15 | banReason: text("ban_reason"), 16 | banExpires: timestamp("ban_expires"), 17 | }); 18 | 19 | export const session = pgTable("session", { 20 | id: text("id").primaryKey(), 21 | expiresAt: timestamp("expires_at").notNull(), 22 | token: text("token").notNull().unique(), 23 | createdAt: timestamp("created_at").notNull(), 24 | updatedAt: timestamp("updated_at").notNull(), 25 | ipAddress: text("ip_address"), 26 | userAgent: text("user_agent"), 27 | userId: text("user_id") 28 | .notNull() 29 | .references(() => user.id), 30 | impersonatedBy: text("impersonated_by"), 31 | }); 32 | 33 | export const account = pgTable("account", { 34 | id: text("id").primaryKey(), 35 | accountId: text("account_id").notNull(), 36 | providerId: text("provider_id").notNull(), 37 | userId: text("user_id") 38 | .notNull() 39 | .references(() => user.id), 40 | accessToken: text("access_token"), 41 | refreshToken: text("refresh_token"), 42 | idToken: text("id_token"), 43 | accessTokenExpiresAt: timestamp("access_token_expires_at"), 44 | refreshTokenExpiresAt: timestamp("refresh_token_expires_at"), 45 | scope: text("scope"), 46 | password: text("password"), 47 | createdAt: timestamp("created_at").notNull(), 48 | updatedAt: timestamp("updated_at").notNull(), 49 | }); 50 | 51 | export const verification = pgTable("verification", { 52 | id: text("id").primaryKey(), 53 | identifier: text("identifier").notNull(), 54 | value: text("value").notNull(), 55 | expiresAt: timestamp("expires_at").notNull(), 56 | createdAt: timestamp("created_at"), 57 | updatedAt: timestamp("updated_at"), 58 | }); 59 | -------------------------------------------------------------------------------- /src/routes/index.ts: -------------------------------------------------------------------------------- 1 | // Automatically generated by declarative-routing, do NOT edit 2 | import { z } from "zod"; 3 | import { makeRoute } from "./makeRoute"; 4 | 5 | const defaultInfo = { 6 | search: z.object({}) 7 | }; 8 | 9 | import * as HomeRoute from "@/app/page.info"; 10 | import * as AuthEmailVerifiedRoute from "@/app/(auth)/email-verified/page.info"; 11 | import * as AuthForgotPasswordRoute from "@/app/(auth)/forgot-password/page.info"; 12 | import * as AuthResetPasswordRoute from "@/app/(auth)/reset-password/page.info"; 13 | import * as AuthSignInRoute from "@/app/(auth)/sign-in/page.info"; 14 | import * as AuthSignUpRoute from "@/app/(auth)/sign-up/page.info"; 15 | import * as DashboardAdminRoute from "@/app/(dashboard)/admin/page.info"; 16 | import * as DashboardUserRoute from "@/app/(dashboard)/user/page.info"; 17 | import * as ApiAuthAllRoute from "@/app/api/auth/[...all]/route.info"; 18 | 19 | export const Home = makeRoute( 20 | "/", 21 | { 22 | ...defaultInfo, 23 | ...HomeRoute.Route 24 | } 25 | ); 26 | export const AuthEmailVerified = makeRoute( 27 | "/(auth)/email-verified", 28 | { 29 | ...defaultInfo, 30 | ...AuthEmailVerifiedRoute.Route 31 | } 32 | ); 33 | export const AuthForgotPassword = makeRoute( 34 | "/(auth)/forgot-password", 35 | { 36 | ...defaultInfo, 37 | ...AuthForgotPasswordRoute.Route 38 | } 39 | ); 40 | export const AuthResetPassword = makeRoute( 41 | "/(auth)/reset-password", 42 | { 43 | ...defaultInfo, 44 | ...AuthResetPasswordRoute.Route 45 | } 46 | ); 47 | export const AuthSignIn = makeRoute( 48 | "/(auth)/sign-in", 49 | { 50 | ...defaultInfo, 51 | ...AuthSignInRoute.Route 52 | } 53 | ); 54 | export const AuthSignUp = makeRoute( 55 | "/(auth)/sign-up", 56 | { 57 | ...defaultInfo, 58 | ...AuthSignUpRoute.Route 59 | } 60 | ); 61 | export const DashboardAdmin = makeRoute( 62 | "/(dashboard)/admin", 63 | { 64 | ...defaultInfo, 65 | ...DashboardAdminRoute.Route 66 | } 67 | ); 68 | export const DashboardUser = makeRoute( 69 | "/(dashboard)/user", 70 | { 71 | ...defaultInfo, 72 | ...DashboardUserRoute.Route 73 | } 74 | ); 75 | export const ApiAuthAll = makeRoute( 76 | "/api/auth/[...all]", 77 | { 78 | ...defaultInfo, 79 | ...ApiAuthAllRoute.Route 80 | } 81 | ); 82 | 83 | -------------------------------------------------------------------------------- /src/routes/hooks.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unsafe-return */ 2 | /* eslint-disable @typescript-eslint/unbound-method */ 3 | import { 4 | useParams as useNextParams, 5 | useSearchParams as useNextSearchParams, 6 | useRouter, 7 | } from "next/navigation"; 8 | 9 | import { z } from "zod"; 10 | 11 | import type { RouteBuilder } from "./makeRoute"; 12 | 13 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 14 | const emptySchema = z.object({}); 15 | 16 | type PushOptions = Parameters["push"]>[1]; 17 | 18 | export function usePush< 19 | Params extends z.ZodSchema, 20 | Search extends z.ZodSchema = typeof emptySchema, 21 | >(builder: RouteBuilder) { 22 | const { push } = useRouter(); 23 | return ( 24 | p: z.input, 25 | search?: z.input, 26 | options?: PushOptions, 27 | ) => { 28 | push(builder(p, search), options); 29 | }; 30 | } 31 | 32 | export function useParams< 33 | Params extends z.ZodSchema, 34 | Search extends z.ZodSchema = typeof emptySchema, 35 | >(builder: RouteBuilder): z.output { 36 | const res = builder.paramsSchema.safeParse(useNextParams()); 37 | if (!res.success) { 38 | throw new Error( 39 | `Invalid route params for route ${builder.routeName}: ${res.error.message}`, 40 | ); 41 | } 42 | return res.data; 43 | } 44 | 45 | export function useSearchParams< 46 | Params extends z.ZodSchema, 47 | Search extends z.ZodSchema = typeof emptySchema, 48 | >(builder: RouteBuilder): z.output { 49 | const res = builder.searchSchema.safeParse( 50 | convertURLSearchParamsToObject(useNextSearchParams()), 51 | ); 52 | if (!res.success) { 53 | throw new Error( 54 | `Invalid search params for route ${builder.routeName}: ${res.error.message}`, 55 | ); 56 | } 57 | return res.data; 58 | } 59 | 60 | function convertURLSearchParamsToObject( 61 | params: Readonly | null, 62 | ): Record { 63 | if (!params) { 64 | return {}; 65 | } 66 | 67 | const obj: Record = {}; 68 | 69 | for (const [key, value] of params.entries()) { 70 | if (params.getAll(key).length > 1) { 71 | obj[key] = params.getAll(key); 72 | } else { 73 | obj[key] = value; 74 | } 75 | } 76 | return obj; 77 | } 78 | -------------------------------------------------------------------------------- /src/components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import { Slot } from "@radix-ui/react-slot"; 4 | import { type VariantProps, cva } from "class-variance-authority"; 5 | 6 | import { cn } from "@/lib/utils"; 7 | 8 | const buttonVariants = cva( 9 | "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", 10 | { 11 | variants: { 12 | variant: { 13 | default: 14 | "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90", 15 | destructive: 16 | "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", 17 | outline: 18 | "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50", 19 | secondary: 20 | "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80", 21 | ghost: 22 | "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", 23 | link: "text-primary underline-offset-4 hover:underline", 24 | }, 25 | size: { 26 | default: "h-9 px-4 py-2 has-[>svg]:px-3", 27 | sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5", 28 | lg: "h-10 rounded-md px-6 has-[>svg]:px-4", 29 | icon: "size-9", 30 | }, 31 | }, 32 | defaultVariants: { 33 | variant: "default", 34 | size: "default", 35 | }, 36 | }, 37 | ); 38 | 39 | function Button({ 40 | className, 41 | variant, 42 | size, 43 | asChild = false, 44 | ...props 45 | }: React.ComponentProps<"button"> & 46 | VariantProps & { 47 | asChild?: boolean; 48 | }) { 49 | const Comp = asChild ? Slot : "button"; 50 | 51 | return ( 52 | 57 | ); 58 | } 59 | 60 | export { Button, buttonVariants }; 61 | -------------------------------------------------------------------------------- /src/components/ui/card.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import { cn } from "@/lib/utils"; 4 | 5 | function Card({ className, ...props }: React.ComponentProps<"div">) { 6 | return ( 7 |
15 | ); 16 | } 17 | 18 | function CardHeader({ className, ...props }: React.ComponentProps<"div">) { 19 | return ( 20 |
28 | ); 29 | } 30 | 31 | function CardTitle({ className, ...props }: React.ComponentProps<"div">) { 32 | return ( 33 |
38 | ); 39 | } 40 | 41 | function CardDescription({ className, ...props }: React.ComponentProps<"div">) { 42 | return ( 43 |
48 | ); 49 | } 50 | 51 | function CardAction({ className, ...props }: React.ComponentProps<"div">) { 52 | return ( 53 |
61 | ); 62 | } 63 | 64 | function CardContent({ className, ...props }: React.ComponentProps<"div">) { 65 | return ( 66 |
71 | ); 72 | } 73 | 74 | function CardFooter({ className, ...props }: React.ComponentProps<"div">) { 75 | return ( 76 |
81 | ); 82 | } 83 | 84 | export { 85 | Card, 86 | CardHeader, 87 | CardFooter, 88 | CardTitle, 89 | CardAction, 90 | CardDescription, 91 | CardContent, 92 | }; 93 | -------------------------------------------------------------------------------- /src/env.ts: -------------------------------------------------------------------------------- 1 | import { createEnv } from "@t3-oss/env-nextjs"; 2 | import { z } from "zod"; 3 | 4 | const env = createEnv({ 5 | /** 6 | * Specify your server-side environment variables schema here. This way you can ensure the app 7 | * isn't built with invalid env vars. 8 | */ 9 | server: { 10 | NODE_ENV: z.enum(["development", "test", "production"]), 11 | DATABASE_URL: z.string(), 12 | BETTER_AUTH_SECRET: z.string(), 13 | BETTER_AUTH_URL: z.string(), 14 | SKIP_BUILD_CHECKS: z.string(), 15 | MAIL_HOST: z.string(), 16 | MAIL_USERNAME: z.string(), 17 | MAIL_PASSWORD: z.string(), 18 | MAIL_FROM: z.string().email(), 19 | 20 | GITHUB_CLIENT_ID: z.string(), 21 | GITHUB_CLIENT_SECRET: z.string(), 22 | EMAIL_VERIFICATION_CALLBACK_URL: z.string(), 23 | }, 24 | 25 | /** 26 | * Specify your client-side environment variables schema here. This way you can ensure the app 27 | * isn't built with invalid env vars. To expose them to the client, prefix them with 28 | * `NEXT_PUBLIC_`. 29 | */ 30 | client: { 31 | NEXT_PUBLIC_BETTER_AUTH_URL: z.string(), 32 | }, 33 | 34 | /** 35 | * You can't destruct `process.env` as a regular object in the Next.js edge runtimes (e.g. 36 | * middlewares) or client-side so we need to destruct manually. 37 | */ 38 | runtimeEnv: { 39 | SKIP_BUILD_CHECKS: process.env.SKIP_BUILD_CHECKS, 40 | BETTER_AUTH_SECRET: process.env.BETTER_AUTH_SECRET, 41 | DATABASE_URL: process.env.DATABASE_URL, 42 | NODE_ENV: process.env.NODE_ENV, 43 | BETTER_AUTH_URL: process.env.BETTER_AUTH_URL, 44 | NEXT_PUBLIC_BETTER_AUTH_URL: process.env.NEXT_PUBLIC_BETTER_AUTH_URL, 45 | 46 | GITHUB_CLIENT_ID: process.env.GITHUB_CLIENT_ID, 47 | GITHUB_CLIENT_SECRET: process.env.GITHUB_CLIENT_SECRET, 48 | EMAIL_VERIFICATION_CALLBACK_URL: 49 | process.env.EMAIL_VERIFICATION_CALLBACK_URL, 50 | 51 | MAIL_HOST: process.env.MAIL_HOST, 52 | MAIL_USERNAME: process.env.MAIL_USERNAME, 53 | MAIL_PASSWORD: process.env.MAIL_PASSWORD, 54 | MAIL_FROM: process.env.MAIL_FROM, 55 | }, 56 | /** 57 | * Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. This is especially 58 | * useful for Docker builds. 59 | */ 60 | skipValidation: !!process.env.SKIP_ENV_VALIDATION, 61 | /** 62 | * Makes it so that empty strings are treated as undefined. `SOME_VAR: z.string()` and 63 | * `SOME_VAR=''` will throw an error. 64 | */ 65 | emptyStringAsUndefined: true, 66 | }); 67 | 68 | export default env; 69 | -------------------------------------------------------------------------------- /src/lib/auth/auth.ts: -------------------------------------------------------------------------------- 1 | import "server-only"; 2 | 3 | import env from "@/env"; 4 | import { sendMail } from "@/server/Actions/mailAction"; 5 | import { db } from "@/server/db"; 6 | import { betterAuth } from "better-auth"; 7 | import { drizzleAdapter } from "better-auth/adapters/drizzle"; 8 | import { nextCookies } from "better-auth/next-js"; 9 | import { admin, openAPI } from "better-auth/plugins"; 10 | 11 | export type Session = typeof auth.$Infer.Session; 12 | 13 | export const auth = betterAuth({ 14 | database: drizzleAdapter(db, { 15 | provider: "pg", 16 | }), 17 | 18 | plugins: [openAPI(), admin(), nextCookies()], //nextCookies() should be last plugin in the list 19 | session: { 20 | expiresIn: 60 * 60 * 24 * 7, // 7 days 21 | updateAge: 60 * 60 * 24, // 1 day (every 1 day the session expiration is updated) 22 | cookieCache: { 23 | enabled: true, 24 | maxAge: 5 * 60, // Cache duration in seconds 25 | }, 26 | }, 27 | user: { 28 | additionalFields: { 29 | role: { 30 | type: "string", 31 | default: "user", 32 | required: false, 33 | defaultValue: "user", 34 | }, 35 | }, 36 | changeEmail: { 37 | enabled: true, 38 | sendChangeEmailVerification: async ({ newEmail, url }) => { 39 | await sendMail({ 40 | to: newEmail, 41 | subject: "Verify your email change", 42 | html: `

Click the link to verify: ${url}

`, 43 | }); 44 | }, 45 | }, 46 | }, 47 | socialProviders: { 48 | github: { 49 | clientId: env.GITHUB_CLIENT_ID, 50 | clientSecret: env.GITHUB_CLIENT_SECRET, 51 | }, 52 | }, 53 | 54 | emailAndPassword: { 55 | enabled: true, 56 | requireEmailVerification: true, 57 | sendResetPassword: async ({ user, url }) => { 58 | await sendMail({ 59 | to: user.email, 60 | subject: "Reset your password", 61 | html: `

Click the link to reset your password: ${url}

`, 62 | }); 63 | }, 64 | }, 65 | emailVerification: { 66 | sendOnSignUp: true, 67 | autoSignInAfterVerification: true, 68 | sendVerificationEmail: async ({ user, token }) => { 69 | const verificationUrl = `${env.BETTER_AUTH_URL}/api/auth/verify-email?token=${token}&callbackURL=${env.EMAIL_VERIFICATION_CALLBACK_URL}`; 70 | await sendMail({ 71 | to: user.email, 72 | subject: "Verify your email address", 73 | html: `

Click the link to verify your email: ${verificationUrl}

`, 74 | }); 75 | }, 76 | }, 77 | }); 78 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import { FlatCompat } from "@eslint/eslintrc"; 2 | import js from "@eslint/js"; 3 | import pluginQuery from "@tanstack/eslint-plugin-query"; 4 | import typescriptEslint from "@typescript-eslint/eslint-plugin"; 5 | import tsParser from "@typescript-eslint/parser"; 6 | import drizzlePlugin from "eslint-plugin-drizzle"; 7 | import path from "node:path"; 8 | import { fileURLToPath } from "node:url"; 9 | 10 | const __filename = fileURLToPath(import.meta.url); 11 | const __dirname = path.dirname(__filename); 12 | 13 | const compat = new FlatCompat({ 14 | baseDirectory: __dirname, 15 | recommendedConfig: js.configs.recommended, 16 | allConfig: js.configs.all, 17 | }); 18 | 19 | const eslintConfig = [ 20 | ...pluginQuery.configs["flat/recommended"], 21 | ...compat.extends( 22 | "next/core-web-vitals", 23 | "plugin:@typescript-eslint/recommended-type-checked", 24 | "plugin:@typescript-eslint/stylistic-type-checked", 25 | ), 26 | { 27 | plugins: { 28 | "@typescript-eslint": typescriptEslint, 29 | // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment 30 | drizzle: drizzlePlugin, 31 | }, 32 | languageOptions: { 33 | parser: tsParser, 34 | ecmaVersion: 5, 35 | sourceType: "script", 36 | parserOptions: { 37 | project: true, 38 | }, 39 | }, 40 | rules: { 41 | "@typescript-eslint/no-unsafe-assignment": "warn", 42 | "@typescript-eslint/no-unsafe-call": "warn", 43 | "@typescript-eslint/array-type": "off", 44 | "@typescript-eslint/consistent-type-definitions": "off", 45 | "@typescript-eslint/consistent-type-imports": [ 46 | "warn", 47 | { 48 | prefer: "type-imports", 49 | fixStyle: "inline-type-imports", 50 | }, 51 | ], 52 | "@typescript-eslint/no-unused-vars": [ 53 | "warn", 54 | { 55 | argsIgnorePattern: "^_", 56 | }, 57 | ], 58 | "@typescript-eslint/require-await": "off", 59 | "@typescript-eslint/no-misused-promises": [ 60 | "error", 61 | { 62 | checksVoidReturn: { 63 | attributes: false, 64 | }, 65 | }, 66 | ], 67 | "drizzle/enforce-delete-with-where": [ 68 | "error", 69 | { 70 | drizzleObjectName: ["db", "ctx.db"], 71 | }, 72 | ], 73 | "drizzle/enforce-update-with-where": [ 74 | "error", 75 | { 76 | drizzleObjectName: ["db", "ctx.db"], 77 | }, 78 | ], 79 | }, 80 | }, 81 | ]; 82 | 83 | export default eslintConfig; 84 | -------------------------------------------------------------------------------- /src/components/section/Header.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useRouter } from "next/navigation"; 4 | 5 | import { AuthSignIn, AuthSignUp, DashboardAdmin, Home } from "@/routes"; 6 | 7 | import { 8 | DropdownMenu, 9 | DropdownMenuContent, 10 | DropdownMenuItem, 11 | DropdownMenuLabel, 12 | DropdownMenuSeparator, 13 | DropdownMenuTrigger, 14 | } from "@/components/ui/dropdown-menu"; 15 | 16 | import { signOut, useSession } from "@/lib/auth/auth-client"; 17 | 18 | import Logo from "../custom/logo"; 19 | import { Avatar, AvatarFallback, AvatarImage } from "../ui/avatar"; 20 | import { Button } from "../ui/button"; 21 | 22 | export default function Header() { 23 | const router = useRouter(); 24 | const { data: session } = useSession(); 25 | return ( 26 |
27 |
28 | 29 | 30 | 31 |
32 | {session ? ( 33 | <> 34 | 35 | 36 | 37 | 41 | Its-Satyajit 42 | 43 | 44 | 45 | {session.user.name} 46 | 47 | 48 | DashBoard 49 | 50 | 53 | signOut({ 54 | fetchOptions: { 55 | onSuccess: () => router.push(Home()), 56 | }, 57 | }) 58 | } 59 | > 60 | Sign Out 61 | 62 | 63 | 64 | 65 | ) : ( 66 | <> 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | )} 75 |
76 |
77 |
78 | ); 79 | } 80 | -------------------------------------------------------------------------------- /src/app/(auth)/forgot-password/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useState } from "react"; 4 | import { useForm } from "react-hook-form"; 5 | 6 | import { zodResolver } from "@hookform/resolvers/zod"; 7 | import { toast } from "sonner"; 8 | import type { z } from "zod"; 9 | 10 | import { Button } from "@/components/ui/button"; 11 | import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; 12 | import { 13 | Form, 14 | FormControl, 15 | FormField, 16 | FormItem, 17 | FormLabel, 18 | FormMessage, 19 | } from "@/components/ui/form"; 20 | import { Input } from "@/components/ui/input"; 21 | 22 | import { forgetPassword } from "@/lib/auth/auth-client"; 23 | import { forgotPasswordSchema } from "@/lib/auth/schema"; 24 | 25 | export default function ForgotPassword() { 26 | const [isPending, setIsPending] = useState(false); 27 | 28 | const form = useForm>({ 29 | resolver: zodResolver(forgotPasswordSchema), 30 | defaultValues: { 31 | email: "", 32 | }, 33 | }); 34 | 35 | const onSubmit = async (data: z.infer) => { 36 | setIsPending(true); 37 | 38 | const { error } = await forgetPassword({ 39 | email: data.email, 40 | redirectTo: "/reset-password", 41 | }); 42 | 43 | if (error) { 44 | toast.error(error.message); 45 | } else { 46 | toast( 47 | "If an account exists with this email, you will receive a password reset link.", 48 | ); 49 | } 50 | setIsPending(false); 51 | }; 52 | 53 | return ( 54 |
55 | 56 | 57 | 58 | Forgot Password 59 | 60 | 61 | 62 |
63 | 64 | ( 68 | 69 | Email 70 | 71 | 77 | 78 | 79 | 80 | )} 81 | /> 82 | 83 | 84 | 85 |
86 |
87 |
88 | ); 89 | } 90 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "new", 3 | "version": "0.1.0", 4 | "private": true, 5 | "type": "module", 6 | "scripts": { 7 | "docs:generate": "typedoc", 8 | "build": "pnpm dr:build && pnpm next build", 9 | "check": "next lint && tsc --noEmit", 10 | "db:generate": "drizzle-kit generate", 11 | "db:migrate": "drizzle-kit migrate", 12 | "db:push": "drizzle-kit push", 13 | "db:studio": "drizzle-kit studio", 14 | "dev:turbo": "next dev --turbo", 15 | "lint": "next lint", 16 | "lint:fix": "next lint --fix", 17 | "preview": "next build && next start", 18 | "start": "next start", 19 | "typecheck": "tsc --noEmit", 20 | "format:write": "prettier --write \"**/*.{ts,tsx,js,jsx,mdx,cjs,mjs}\" --cache", 21 | "format:check": "prettier --check \"**/*.{ts,tsx,js,jsx,mdx,cjs,mjs}\" --cache", 22 | "dr:build": "npx declarative-routing build", 23 | "dr:build:watch": "npx declarative-routing build --watch", 24 | "format:write:watch": "pnpm onchange -j 12 -d 1500 -f change -f add \"**/*\" --exclude-path .gitignore -- prettier -w \"**/*.{ts,tsx,js,jsx,mdx,cjs,mjs,json}\" --cache --log-level warn", 25 | "dev": "concurrently -c \"#604CC3, #8FD14F ,#FF6600,#FFA600\" \"pnpm dev:turbo\" \"pnpm dr:build:watch\" \"pnpm format:write:watch\" --names \"Next.js,Routing,Prettier\"" 26 | }, 27 | "dependencies": { 28 | "@better-fetch/fetch": "^1.1.17", 29 | "@fontsource-variable/inter": "^5.2.5", 30 | "@hookform/resolvers": "^4.1.3", 31 | "@icons-pack/react-simple-icons": "^12.3.0", 32 | "@radix-ui/react-avatar": "^1.1.3", 33 | "@radix-ui/react-dropdown-menu": "^2.1.6", 34 | "@radix-ui/react-label": "^2.1.2", 35 | "@radix-ui/react-slot": "^1.1.2", 36 | "@t3-oss/env-nextjs": "^0.12.0", 37 | "@tailwindcss/typography": "^0.5.16", 38 | "@tanstack/react-query": "^5.69.0", 39 | "@tanstack/react-query-devtools": "^5.69.0", 40 | "babel-plugin-react-compiler": "beta", 41 | "better-auth": "^1.2.4", 42 | "class-variance-authority": "^0.7.1", 43 | "clsx": "^2.1.1", 44 | "concurrently": "^9.1.2", 45 | "declarative-routing": "^0.1.20", 46 | "dotenv": "^16.4.7", 47 | "drizzle-orm": "^0.41.0", 48 | "eslint-plugin-drizzle": "^0.2.3", 49 | "lucide-react": "^0.483.0", 50 | "next": "15.2.4", 51 | "next-themes": "^0.4.6", 52 | "nodemailer": "^6.10.0", 53 | "onchange": "^7.1.0", 54 | "postgres": "^3.4.5", 55 | "query-string": "^9.1.1", 56 | "react": "^19.0.0", 57 | "react-dom": "^19.0.0", 58 | "react-hook-form": "^7.54.2", 59 | "server-only": "^0.0.1", 60 | "sonner": "^2.0.1", 61 | "tailwind-merge": "^3.0.2", 62 | "tailwindcss-animate": "^1.0.7", 63 | "winston": "^3.17.0", 64 | "zod": "^3.24.2" 65 | }, 66 | "devDependencies": { 67 | "@eslint/eslintrc": "^3.3.1", 68 | "@eslint/js": "^9.23.0", 69 | "@next/eslint-plugin-next": "^15.2.4", 70 | "@tailwindcss/postcss": "^4.0.15", 71 | "@tanstack/eslint-plugin-query": "^5.68.0", 72 | "@trivago/prettier-plugin-sort-imports": "^5.2.2", 73 | "@types/node": "^22.13.13", 74 | "@types/nodemailer": "^6.4.17", 75 | "@types/react": "^19.0.12", 76 | "@types/react-dom": "^19.0.4", 77 | "@typescript-eslint/eslint-plugin": "^8.28.0", 78 | "@typescript-eslint/parser": "^8.28.0", 79 | "drizzle-kit": "^0.30.5", 80 | "eslint": "^9.23.0", 81 | "eslint-config-next": "15.2.4", 82 | "eslint-plugin-react-hooks": "^5.2.0", 83 | "prettier": "^3.5.3", 84 | "prettier-plugin-organize-attributes": "^1.0.0", 85 | "prettier-plugin-tailwindcss": "^0.6.11", 86 | "tailwindcss": "^4.0.15", 87 | "tsx": "^4.19.3", 88 | "typescript": "^5.8.2" 89 | }, 90 | "pnpm": { 91 | "onlyBuiltDependencies": [ 92 | "esbuild", 93 | "sharp" 94 | ] 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/app/(auth)/sign-up/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useRouter } from "next/navigation"; 4 | import { useState } from "react"; 5 | import { useForm } from "react-hook-form"; 6 | 7 | import { AuthSignIn, Home } from "@/routes"; 8 | import { zodResolver } from "@hookform/resolvers/zod"; 9 | import { toast } from "sonner"; 10 | import type { z } from "zod"; 11 | 12 | import { Button } from "@/components/ui/button"; 13 | import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; 14 | import { 15 | Form, 16 | FormControl, 17 | FormField, 18 | FormItem, 19 | FormLabel, 20 | FormMessage, 21 | } from "@/components/ui/form"; 22 | import { Input } from "@/components/ui/input"; 23 | 24 | import { signUp } from "@/lib/auth/auth-client"; 25 | import { signUpSchema } from "@/lib/auth/schema"; 26 | 27 | export default function SignUp() { 28 | const router = useRouter(); 29 | const [pending, setPending] = useState(false); 30 | 31 | const form = useForm>({ 32 | resolver: zodResolver(signUpSchema), 33 | defaultValues: { 34 | name: "", 35 | email: "", 36 | password: "", 37 | confirmPassword: "", 38 | }, 39 | }); 40 | 41 | const onSubmit = async (values: z.infer) => { 42 | await signUp.email( 43 | { 44 | email: values.email, 45 | password: values.password, 46 | name: values.name, 47 | }, 48 | { 49 | onRequest: () => { 50 | setPending(true); 51 | }, 52 | onSuccess: () => { 53 | toast.success("Successfully signed up!", { 54 | description: 55 | "You have successfully signed up! Please check your email for verification.", 56 | }); 57 | 58 | router.push(Home()); 59 | router.refresh(); 60 | }, 61 | onError: (ctx) => { 62 | toast.error("Something went wrong!", { 63 | description: ctx.error.message ?? "Something went wrong.", 64 | }); 65 | }, 66 | }, 67 | ); 68 | setPending(false); 69 | }; 70 | 71 | return ( 72 |
73 | 74 | 75 | 76 | Create Account 77 | 78 | 79 | 80 |
81 | 82 | {["name", "email", "password", "confirmPassword"].map((field) => ( 83 | } 87 | render={({ field: fieldProps }) => ( 88 | 89 | 90 | {field.charAt(0).toUpperCase() + field.slice(1)} 91 | 92 | 93 | 105 | 106 | 107 | 108 | )} 109 | /> 110 | ))} 111 | 112 | 115 | 116 | 117 |
118 | 119 | Already have an account? Sign in 120 | 121 |
122 |
123 |
124 |
125 | ); 126 | } 127 | -------------------------------------------------------------------------------- /src/styles/globals.css: -------------------------------------------------------------------------------- 1 | @import "tailwindcss"; 2 | 3 | @plugin '@tailwindcss/typography'; 4 | @plugin 'tailwindcss-animate'; 5 | 6 | @custom-variant dark (&:is(.dark *)); 7 | 8 | @theme { 9 | 10 | 11 | --color-background: hsl(var(--background)); 12 | --color-foreground: hsl(var(--foreground)); 13 | 14 | --color-card: hsl(var(--card)); 15 | --color-card-foreground: hsl(var(--card-foreground)); 16 | 17 | --color-popover: hsl(var(--popover)); 18 | --color-popover-foreground: hsl(var(--popover-foreground)); 19 | 20 | --color-primary: hsl(var(--primary)); 21 | --color-primary-foreground: hsl(var(--primary-foreground)); 22 | 23 | --color-secondary: hsl(var(--secondary)); 24 | --color-secondary-foreground: hsl(var(--secondary-foreground)); 25 | 26 | --color-muted: hsl(var(--muted)); 27 | --color-muted-foreground: hsl(var(--muted-foreground)); 28 | 29 | --color-accent: hsl(var(--accent)); 30 | --color-accent-foreground: hsl(var(--accent-foreground)); 31 | 32 | --color-destructive: hsl(var(--destructive)); 33 | --color-destructive-foreground: hsl(var(--destructive-foreground)); 34 | 35 | --color-border: hsl(var(--border)); 36 | --color-input: hsl(var(--input)); 37 | --color-ring: hsl(var(--ring)); 38 | 39 | --color-chart-1: hsl(var(--chart-1)); 40 | --color-chart-2: hsl(var(--chart-2)); 41 | --color-chart-3: hsl(var(--chart-3)); 42 | --color-chart-4: hsl(var(--chart-4)); 43 | --color-chart-5: hsl(var(--chart-5)); 44 | 45 | --radius-lg: var(--radius); 46 | --radius-md: calc(var(--radius) - 2px); 47 | --radius-sm: calc(var(--radius) - 4px); 48 | } 49 | 50 | /* 51 | The default border color has changed to `currentColor` in Tailwind CSS v4, 52 | so we've added these compatibility styles to make sure everything still 53 | looks the same as it did with Tailwind CSS v3. 54 | 55 | If we ever want to remove these styles, we need to add an explicit border 56 | color utility to any element that depends on these defaults. 57 | */ 58 | @layer base { 59 | *, 60 | ::after, 61 | ::before, 62 | ::backdrop, 63 | ::file-selector-button { 64 | border-color: var(--color-gray-200, currentColor); 65 | } 66 | } 67 | 68 | @layer utilities { 69 | body { 70 | font-family: "Inter Variable"; 71 | } 72 | } 73 | 74 | @layer base { 75 | :root { 76 | --background: 0 0% 100%; 77 | --foreground: 222.2 84% 4.9%; 78 | --card: 0 0% 100%; 79 | --card-foreground: 222.2 84% 4.9%; 80 | --popover: 0 0% 100%; 81 | --popover-foreground: 222.2 84% 4.9%; 82 | --primary: 222.2 47.4% 11.2%; 83 | --primary-foreground: 210 40% 98%; 84 | --secondary: 210 40% 96.1%; 85 | --secondary-foreground: 222.2 47.4% 11.2%; 86 | --muted: 210 40% 96.1%; 87 | --muted-foreground: 215.4 16.3% 46.9%; 88 | --accent: 210 40% 96.1%; 89 | --accent-foreground: 222.2 47.4% 11.2%; 90 | --destructive: 0 84.2% 60.2%; 91 | --destructive-foreground: 210 40% 98%; 92 | --border: 214.3 31.8% 91.4%; 93 | --input: 214.3 31.8% 91.4%; 94 | --ring: 222.2 84% 4.9%; 95 | --chart-1: 12 76% 61%; 96 | --chart-2: 173 58% 39%; 97 | --chart-3: 197 37% 24%; 98 | --chart-4: 43 74% 66%; 99 | --chart-5: 27 87% 67%; 100 | --radius: 0.5rem; 101 | } 102 | 103 | .dark { 104 | --background: 222.2 84% 4.9%; 105 | --foreground: 210 40% 98%; 106 | --card: 222.2 84% 4.9%; 107 | --card-foreground: 210 40% 98%; 108 | --popover: 222.2 84% 4.9%; 109 | --popover-foreground: 210 40% 98%; 110 | --primary: 210 40% 98%; 111 | --primary-foreground: 222.2 47.4% 11.2%; 112 | --secondary: 217.2 32.6% 17.5%; 113 | --secondary-foreground: 210 40% 98%; 114 | --muted: 217.2 32.6% 17.5%; 115 | --muted-foreground: 215 20.2% 65.1%; 116 | --accent: 217.2 32.6% 17.5%; 117 | --accent-foreground: 210 40% 98%; 118 | --destructive: 0 62.8% 30.6%; 119 | --destructive-foreground: 210 40% 98%; 120 | --border: 217.2 32.6% 17.5%; 121 | --input: 217.2 32.6% 17.5%; 122 | --ring: 212.7 26.8% 83.9%; 123 | --chart-1: 220 70% 50%; 124 | --chart-2: 160 60% 45%; 125 | --chart-3: 30 80% 55%; 126 | --chart-4: 280 65% 60%; 127 | --chart-5: 340 75% 55%; 128 | } 129 | } 130 | 131 | @layer base { 132 | * { 133 | @apply border-border; 134 | } 135 | 136 | body { 137 | @apply bg-background text-foreground; 138 | 139 | } 140 | 141 | h1, 142 | h2, 143 | h3, 144 | h4, 145 | h5, 146 | h6 { 147 | @apply text-balance; 148 | } 149 | 150 | p { 151 | @apply text-pretty; 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /src/components/ui/form.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import { 5 | Controller, 6 | type ControllerProps, 7 | type FieldPath, 8 | type FieldValues, 9 | FormProvider, 10 | useFormContext, 11 | useFormState, 12 | } from "react-hook-form"; 13 | 14 | import type * as LabelPrimitive from "@radix-ui/react-label"; 15 | import { Slot } from "@radix-ui/react-slot"; 16 | 17 | import { Label } from "@/components/ui/label"; 18 | 19 | import { cn } from "@/lib/utils"; 20 | 21 | const Form = FormProvider; 22 | 23 | type FormFieldContextValue< 24 | TFieldValues extends FieldValues = FieldValues, 25 | TName extends FieldPath = FieldPath, 26 | > = { 27 | name: TName; 28 | }; 29 | 30 | const FormFieldContext = React.createContext( 31 | {} as FormFieldContextValue, 32 | ); 33 | 34 | const FormField = < 35 | TFieldValues extends FieldValues = FieldValues, 36 | TName extends FieldPath = FieldPath, 37 | >({ 38 | ...props 39 | }: ControllerProps) => { 40 | return ( 41 | 42 | 43 | 44 | ); 45 | }; 46 | 47 | const useFormField = () => { 48 | const fieldContext = React.useContext(FormFieldContext); 49 | const itemContext = React.useContext(FormItemContext); 50 | const { getFieldState } = useFormContext(); 51 | const formState = useFormState({ name: fieldContext.name }); 52 | const fieldState = getFieldState(fieldContext.name, formState); 53 | 54 | if (!fieldContext) { 55 | throw new Error("useFormField should be used within "); 56 | } 57 | 58 | const { id } = itemContext; 59 | 60 | return { 61 | id, 62 | name: fieldContext.name, 63 | formItemId: `${id}-form-item`, 64 | formDescriptionId: `${id}-form-item-description`, 65 | formMessageId: `${id}-form-item-message`, 66 | ...fieldState, 67 | }; 68 | }; 69 | 70 | type FormItemContextValue = { 71 | id: string; 72 | }; 73 | 74 | const FormItemContext = React.createContext( 75 | {} as FormItemContextValue, 76 | ); 77 | 78 | function FormItem({ className, ...props }: React.ComponentProps<"div">) { 79 | const id = React.useId(); 80 | 81 | return ( 82 | 83 |
88 | 89 | ); 90 | } 91 | 92 | function FormLabel({ 93 | className, 94 | ...props 95 | }: React.ComponentProps) { 96 | const { error, formItemId } = useFormField(); 97 | 98 | return ( 99 |