├── .eslintrc.json
├── .gitignore
├── README.md
├── app
├── (auth)
│ ├── change-password
│ │ └── page.tsx
│ ├── layout.tsx
│ ├── profile
│ │ └── page.tsx
│ ├── signin
│ │ └── page.tsx
│ └── signup
│ │ └── page.tsx
├── (root)
│ └── page.tsx
├── api
│ └── auth
│ │ └── [...nextauth]
│ │ └── route.ts
├── dashboard
│ └── page.tsx
├── error
│ └── page.tsx
├── favicon.ico
├── globals.css
├── layout.tsx
└── unauthorized
│ └── page.tsx
├── components.json
├── components
├── button
│ ├── google-signin-button.tsx
│ └── signout-button.tsx
├── form
│ ├── change-password-form.tsx
│ ├── signin-form.tsx
│ ├── signup-form.tsx
│ └── update-form.tsx
├── shared
│ ├── footer.tsx
│ ├── main-nav.tsx
│ ├── mode-toggle.tsx
│ ├── navbar.tsx
│ ├── user-avatar.tsx
│ └── user-nav.tsx
└── ui
│ ├── avatar.tsx
│ ├── button.tsx
│ ├── dropdown-menu.tsx
│ ├── form.tsx
│ ├── input.tsx
│ ├── label.tsx
│ ├── toast.tsx
│ ├── toaster.tsx
│ └── use-toast.ts
├── constants
└── index.ts
├── lib
├── actions
│ └── auth.actions.ts
├── models
│ └── user.model.ts
├── mongodb.ts
├── nextauth-options.ts
├── utils.ts
├── utils
│ └── token.ts
└── validations
│ └── auth.ts
├── middleware.ts
├── next.config.js
├── package-lock.json
├── package.json
├── postcss.config.js
├── providers
├── auth-provider.tsx
└── theme-provider.tsx
├── public
├── next.svg
└── vercel.svg
├── tailwind.config.js
├── tailwind.config.ts
├── tsconfig.json
└── types
└── next-auth.d.ts
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "next/core-web-vitals",
3 | "rules": {
4 | "indent": ["warn", 2]
5 | }
6 | }
--------------------------------------------------------------------------------
/.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*.local
29 |
30 | # vercel
31 | .vercel
32 |
33 | # typescript
34 | *.tsbuildinfo
35 | next-env.d.ts
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## Next.js 13, NextAuth.js, MongoDB, Typescript Example
2 | A demo project that uses NextAuth.js for authentication, connects to MongoDB with Mongoose, and supports Google OAuth and email/password login.
3 |
4 | ## 🚀🌍 New Version Available (Next.js 14 + NextAuth.js v5 + i18n)
5 | Check out the new version of this project [here](https://github.com/wei30172/nextauth-v5-mongodb-typescript-example).
6 |
7 | ## Features
8 | - OAuth: Log in with Google.
9 |
10 | - Credential Login: Log in with email and password.
11 |
12 | - Profile Edit: Change user details.
13 |
14 | - Password Change: Safely update passwords.
15 |
16 | - Secure Routes: Access only for logged-in users / admins.
17 |
18 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
19 |
20 | ## Environment Setup
21 | Create a .env file in the root directory and add the following variables:
22 |
23 | ```env
24 | NEXTAUTH_URL="http://localhost:3000"
25 | NEXTAUTH_SECRET="YOUR_NEXTAUTH_SECRET"
26 |
27 | GOOGLE_CLIENT_ID="YOUR_GOOGLE_CLIENT_ID"
28 | GOOGLE_CLIENT_SECRET="YOUR_GOOGLE_CLIENT_SECRET"
29 |
30 | MONGODB_URI="YOUR_MONGODB_URI"
31 | ```
32 |
33 | GOOGLE_CLIENT_ID & GOOGLE_CLIENT_SECRET
34 |
35 | - Navigate to [https://console.cloud.google.com](https://console.cloud.google.com/) .
36 |
37 | - Create a new project.
38 |
39 | - Head over to APIs & Services => Credentials.
40 |
41 | - Click on CREATE CREDENTIALS => OAuth client ID.
42 |
43 | - Choose the Web application.
44 |
45 | - Add to Authorized JavaScript origins: http://localhost:3000 .
46 |
47 | - Add to Authorized redirect URIs: http://localhost:3000/api/auth/callback/google.
48 |
49 | - Finish by going to APIs & Services => OAuth consent screen and publishing the app.
50 |
51 | ## Getting Started
52 |
53 | First, run the development server:
54 |
55 | ```bash
56 | npm run dev
57 | # or
58 | yarn dev
59 | # or
60 | pnpm dev
61 | ```
62 |
63 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
64 |
65 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
66 |
67 | This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
68 |
69 | ## Learn More
70 |
71 | To learn more about Next.js, take a look at the following resources:
72 |
73 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
74 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
75 |
76 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
77 |
78 | ## Deploy on Vercel
79 |
80 | 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.
81 |
82 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
83 |
--------------------------------------------------------------------------------
/app/(auth)/change-password/page.tsx:
--------------------------------------------------------------------------------
1 | import ChangePasswordForm from "@/components/form/change-password-form"
2 | import { changeUserPassword } from "@/lib/actions/auth.actions"
3 |
4 | const ChangePasswordPage = async () => {
5 | return (
6 |
7 |
8 |
9 | )
10 | }
11 |
12 |
13 | export default ChangePasswordPage
--------------------------------------------------------------------------------
/app/(auth)/layout.tsx:
--------------------------------------------------------------------------------
1 | interface AuthLayoutProps {
2 | children: React.ReactNode
3 | }
4 |
5 | export default function AuthLayout({
6 | children
7 | }: AuthLayoutProps) {
8 | return (
9 |
12 | )
13 | }
--------------------------------------------------------------------------------
/app/(auth)/profile/page.tsx:
--------------------------------------------------------------------------------
1 | import UpdateForm from "@/components/form/update-form"
2 | import { updateUserProfile } from "@/lib/actions/auth.actions"
3 |
4 | const ProfilePage = async () => {
5 | return (
6 |
7 |
8 |
9 | )
10 | }
11 |
12 |
13 | export default ProfilePage
14 |
--------------------------------------------------------------------------------
/app/(auth)/signin/page.tsx:
--------------------------------------------------------------------------------
1 | import SignInForm from '@/components/form/signin-form'
2 |
3 | interface SignInPageProps {
4 | searchParams: {
5 | callbackUrl: string
6 | }
7 | }
8 | const SignInPage = ({
9 | searchParams: { callbackUrl }
10 | }: SignInPageProps) => {
11 | // console.log(callbackUrl)
12 | return (
13 |
14 |
15 |
16 | )
17 | }
18 |
19 | export default SignInPage
--------------------------------------------------------------------------------
/app/(auth)/signup/page.tsx:
--------------------------------------------------------------------------------
1 | import { signUpWithCredentials } from "@/lib/actions/auth.actions"
2 | import SignUpForm from "@/components/form/signup-form"
3 |
4 | interface SignUpPageProps {
5 | searchParams: {
6 | callbackUrl: string
7 | }
8 | }
9 |
10 | const SignUpPage = ({
11 | searchParams: { callbackUrl }
12 | }: SignUpPageProps) => {
13 | return (
14 |
15 |
19 |
20 | )
21 | }
22 |
23 | export default SignUpPage
--------------------------------------------------------------------------------
/app/(root)/page.tsx:
--------------------------------------------------------------------------------
1 | export default function Home() {
2 | return (
3 |
4 |
Welcome to the Home Page
5 |
6 | )
7 | }
8 |
--------------------------------------------------------------------------------
/app/api/auth/[...nextauth]/route.ts:
--------------------------------------------------------------------------------
1 | import NextAuth from "next-auth"
2 | import { nextauthOptions } from "@/lib/nextauth-options"
3 |
4 | const handler = NextAuth(nextauthOptions)
5 |
6 | export { handler as GET, handler as POST }
--------------------------------------------------------------------------------
/app/dashboard/page.tsx:
--------------------------------------------------------------------------------
1 | const Dashboard = () => {
2 | return (
3 | Hi, admin. Welcome to the Dashboard
4 | )
5 | }
6 |
7 | export default Dashboard
--------------------------------------------------------------------------------
/app/error/page.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { useRouter, useSearchParams } from "next/navigation"
4 | import { Button } from "@/components/ui/button"
5 |
6 | const Error = () => {
7 | const router = useRouter()
8 | const searchParams = useSearchParams()
9 | const errMsg = searchParams.get("error")
10 |
11 | return (
12 |
13 | Errors: {errMsg}
14 |
21 |
22 | )
23 | }
24 |
25 | export default Error
--------------------------------------------------------------------------------
/app/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wei30172/nextauth-mongodb-typescript-example/813a1884670d5fbd914161f811c0c2855d0abaea/app/favicon.ico
--------------------------------------------------------------------------------
/app/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | html,
6 | body,
7 | :root {
8 | height: 100%;
9 | }
10 |
11 | @layer base {
12 | :root {
13 | --background: 0 0% 100%;
14 | --foreground: 222.2 84% 4.9%;
15 |
16 | --card: 0 0% 100%;
17 | --card-foreground: 222.2 84% 4.9%;
18 |
19 | --popover: 0 0% 100%;
20 | --popover-foreground: 222.2 84% 4.9%;
21 |
22 | --primary: 222.2 47.4% 11.2%;
23 | --primary-foreground: 210 40% 98%;
24 |
25 | --secondary: 210 40% 96.1%;
26 | --secondary-foreground: 222.2 47.4% 11.2%;
27 |
28 | --muted: 210 40% 96.1%;
29 | --muted-foreground: 215.4 16.3% 46.9%;
30 |
31 | --accent: 210 40% 96.1%;
32 | --accent-foreground: 222.2 47.4% 11.2%;
33 |
34 | --destructive: 0 84.2% 60.2%;
35 | --destructive-foreground: 210 40% 98%;
36 |
37 | --border: 214.3 31.8% 91.4%;
38 | --input: 214.3 31.8% 91.4%;
39 | --ring: 222.2 84% 4.9%;
40 |
41 | --radius: 0.5rem;
42 | }
43 |
44 | .dark {
45 | --background: 222.2 84% 4.9%;
46 | --foreground: 210 40% 98%;
47 |
48 | --card: 222.2 84% 4.9%;
49 | --card-foreground: 210 40% 98%;
50 |
51 | --popover: 222.2 84% 4.9%;
52 | --popover-foreground: 210 40% 98%;
53 |
54 | --primary: 210 40% 98%;
55 | --primary-foreground: 222.2 47.4% 11.2%;
56 |
57 | --secondary: 217.2 32.6% 17.5%;
58 | --secondary-foreground: 210 40% 98%;
59 |
60 | --muted: 217.2 32.6% 17.5%;
61 | --muted-foreground: 215 20.2% 65.1%;
62 |
63 | --accent: 217.2 32.6% 17.5%;
64 | --accent-foreground: 210 40% 98%;
65 |
66 | --destructive: 0 62.8% 30.6%;
67 | --destructive-foreground: 210 40% 98%;
68 |
69 | --border: 217.2 32.6% 17.5%;
70 | --input: 217.2 32.6% 17.5%;
71 | --ring: 212.7 26.8% 83.9%;
72 | }
73 | }
74 |
75 | @layer base {
76 | * {
77 | @apply border-border;
78 | }
79 | body {
80 | @apply bg-background text-foreground;
81 | }
82 | }
--------------------------------------------------------------------------------
/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import './globals.css'
2 | import type { Metadata } from 'next'
3 | import { Inter } from 'next/font/google'
4 |
5 | import AuthProvider from '@/providers/auth-provider'
6 | import ThemeProvider from '@/providers/theme-provider'
7 | import Navbar from '@/components/shared/navbar'
8 | import Footer from '@/components/shared/footer'
9 | import { Toaster } from '@/components/ui/toaster'
10 |
11 | const inter = Inter({ subsets: ['latin'] })
12 |
13 | export const metadata: Metadata = {
14 | title: 'Nextjs fullstack Authentication',
15 | description: 'Sign-Up and Sign-In with Nextjs',
16 | }
17 |
18 | export default function RootLayout({
19 | children,
20 | }: {
21 | children: React.ReactNode
22 | }) {
23 | return (
24 |
25 |
26 |
27 |
33 |
34 |
35 | {children}
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 | )
44 | }
45 |
--------------------------------------------------------------------------------
/app/unauthorized/page.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { useRouter } from 'next/navigation'
4 | import { Button } from '@/components/ui/button'
5 |
6 | const Unauthorized = () => {
7 | const router = useRouter()
8 |
9 | return (
10 |
11 | You Are Not Authorized!
12 |
18 |
19 | )
20 | }
21 |
22 | export default Unauthorized
--------------------------------------------------------------------------------
/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": "app/globals.css",
9 | "baseColor": "slate",
10 | "cssVariables": true
11 | },
12 | "aliases": {
13 | "components": "@/components",
14 | "utils": "@/lib/utils"
15 | }
16 | }
--------------------------------------------------------------------------------
/components/button/google-signin-button.tsx:
--------------------------------------------------------------------------------
1 | import { signIn } from 'next-auth/react'
2 | import { Button } from '@/components/ui/button'
3 |
4 | interface GoogleSignInButtonProps {
5 | children: React.ReactNode
6 | callbackUrl: string
7 | }
8 | const GoogleSignInButton = ({
9 | children,
10 | callbackUrl
11 | }: GoogleSignInButtonProps) => {
12 |
13 | const loginWithGoogle = async () => {
14 | await signIn("google", { callbackUrl })
15 | }
16 |
17 | return (
18 |
21 | )
22 | }
23 |
24 | export default GoogleSignInButton
--------------------------------------------------------------------------------
/components/button/signout-button.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { Button } from "@/components/ui/button"
4 | import { signOut } from "next-auth/react"
5 |
6 | const SignOutButton = () => {
7 | const signout = () => {
8 | signOut({
9 | redirect: true,
10 | callbackUrl: `${window.location.origin}/signin`
11 | })
12 | }
13 |
14 | return (
15 |
18 | )
19 | }
20 |
21 | export default SignOutButton
--------------------------------------------------------------------------------
/components/form/change-password-form.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { useState } from "react"
4 | import { useForm } from "react-hook-form"
5 | import { experimental_useFormStatus as useFormStatus } from 'react-dom'
6 | // 在最新版本的 react-dom 中,使用 useFormStatus 來管理表單狀態。
7 | // import { useFormStatus } from 'react-dom'
8 | import { zodResolver } from "@hookform/resolvers/zod"
9 | import * as z from "zod"
10 | import { useRouter } from "next/navigation"
11 | import { signOut } from "next-auth/react"
12 | import { changePasswordValidation } from "@/lib/validations/auth"
13 | import { ChangeUserPasswordParams } from "@/lib/actions/auth.actions"
14 |
15 | import { Button } from "@/components/ui/button"
16 | import {
17 | Form,
18 | FormControl,
19 | FormField,
20 | FormItem,
21 | FormLabel,
22 | FormMessage,
23 | } from "@/components/ui/form"
24 | import { Input } from "@/components/ui/input"
25 | import { useToast } from "@/components/ui/use-toast"
26 |
27 | interface ChangePasswordProps {
28 | changeUserPassword: (values: ChangeUserPasswordParams) => Promise<{success?: boolean}>
29 | }
30 |
31 | const ChangePasswordForm = ({
32 | changeUserPassword
33 | }: ChangePasswordProps) => {
34 | const router = useRouter()
35 | const { pending } = useFormStatus()
36 | const { toast } = useToast()
37 | const [isLoggingOut, setIsLoggingOut] = useState(false)
38 |
39 | const form = useForm>({
40 | resolver: zodResolver(changePasswordValidation),
41 | defaultValues: {
42 | oldPassword: "",
43 | newPassword: "",
44 | confirmPassword: ""
45 | }
46 | })
47 |
48 | async function onSubmit(values: z.infer) {
49 | // console.log(values)
50 | const res = await changeUserPassword({
51 | oldPassword: values.oldPassword,
52 | newPassword: values.newPassword
53 | })
54 |
55 | if (res?.success) {
56 | toast({
57 | title: "Change password successfully.",
58 | description: "You are being signed out..."
59 | })
60 | setIsLoggingOut(true)
61 | setTimeout(() => {
62 | signOut({
63 | redirect: true,
64 | callbackUrl: `${window.location.origin}/signin`
65 | })
66 | }, 5000)
67 | }
68 | }
69 |
70 | return (
71 |
142 |
143 | )
144 | }
145 |
146 | export default ChangePasswordForm
--------------------------------------------------------------------------------
/components/form/signin-form.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { useForm } from "react-hook-form"
4 | import { experimental_useFormStatus as useFormStatus } from 'react-dom'
5 | // 在最新版本的 react-dom 中,使用 useFormStatus 來管理表單狀態。
6 | // import { useFormStatus } from 'react-dom'
7 | import { zodResolver } from "@hookform/resolvers/zod"
8 | import * as z from "zod"
9 | import { signIn } from "next-auth/react"
10 | import { userSignInValidation } from "@/lib/validations/auth"
11 | import Link from "next/link"
12 |
13 | import { Button } from "@/components/ui/button"
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 | import GoogleSignInButton from "@/components/button/google-signin-button"
24 |
25 | interface SignInFormProps {
26 | callbackUrl: string
27 | }
28 |
29 | const SignInForm = ({
30 | callbackUrl
31 | }: SignInFormProps) => {
32 | const { pending } = useFormStatus()
33 |
34 | const form = useForm>({
35 | resolver: zodResolver(userSignInValidation),
36 | defaultValues: {
37 | email: "",
38 | password: ""
39 | }
40 | })
41 |
42 | async function onSubmit(values: z.infer) {
43 | // console.log(values)
44 | await signIn("credentials", {
45 | email: values.email,
46 | password: values.password,
47 | callbackUrl
48 | })
49 | }
50 |
51 | return (
52 |
94 |
99 |
100 | Sign in with Google
101 |
102 |
103 | Don't have an account?
104 |
105 | Sign Up
106 |
107 |
108 |
109 | )
110 | }
111 |
112 | export default SignInForm
--------------------------------------------------------------------------------
/components/form/signup-form.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { useForm } from "react-hook-form"
4 | import { experimental_useFormStatus as useFormStatus } from 'react-dom'
5 | // 在最新版本的 react-dom 中,使用 useFormStatus 來管理表單狀態。
6 | // import { useFormStatus } from 'react-dom'
7 | import { zodResolver } from "@hookform/resolvers/zod"
8 | import * as z from "zod"
9 | import Link from "next/link"
10 | import { useRouter } from "next/navigation"
11 | import { userSignUpValidation } from "@/lib/validations/auth"
12 | import { SignUpWithCredentialsParams } from "@/lib/actions/auth.actions"
13 |
14 | import { Button } from "@/components/ui/button"
15 | import {
16 | Form,
17 | FormControl,
18 | FormField,
19 | FormItem,
20 | FormLabel,
21 | FormMessage,
22 | } from "@/components/ui/form"
23 | import { Input } from "@/components/ui/input"
24 | import { useToast } from "@/components/ui/use-toast"
25 |
26 | interface SignUpFormProps {
27 | callbackUrl: string,
28 | signUpWithCredentials: (values: SignUpWithCredentialsParams) => Promise<{success?: boolean}>
29 | }
30 |
31 | const SignUpForm = ({
32 | signUpWithCredentials
33 | }: SignUpFormProps) => {
34 | const router = useRouter()
35 | const { pending } = useFormStatus()
36 | const { toast } = useToast()
37 |
38 | const form = useForm>({
39 | resolver: zodResolver(userSignUpValidation),
40 | defaultValues: {
41 | name: "",
42 | email: "",
43 | password: "",
44 | confirmPassword: "",
45 | }
46 | })
47 |
48 | async function onSubmit(values: z.infer) {
49 | // console.log(values)
50 | const res = await signUpWithCredentials(values)
51 |
52 | if (res?.success) {
53 | toast({
54 | description: "Sign up successfully."
55 | })
56 | router.push("/signin")
57 | }
58 | }
59 |
60 | return (
61 |
133 |
134 |
135 |
or
136 |
137 |
138 |
139 | Already have an account?
140 |
141 | Sign In
142 |
143 |
144 |
145 | )
146 | }
147 |
148 | export default SignUpForm
--------------------------------------------------------------------------------
/components/form/update-form.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { useForm } from "react-hook-form"
4 | import { experimental_useFormStatus as useFormStatus } from 'react-dom'
5 | // 在最新版本的 react-dom 中,使用 useFormStatus 來管理表單狀態。
6 | // import { useFormStatus } from 'react-dom'
7 | import { zodResolver } from "@hookform/resolvers/zod"
8 | import * as z from "zod"
9 | import { useSession } from "next-auth/react"
10 | import { userUpdateValidation } from "@/lib/validations/auth"
11 | import { UpdateUserProfileParams } from "@/lib/actions/auth.actions"
12 | import Link from "next/link"
13 |
14 | import { Button } from "@/components/ui/button"
15 | import {
16 | Form,
17 | FormControl,
18 | FormField,
19 | FormItem,
20 | FormLabel,
21 | FormMessage,
22 | } from "@/components/ui/form"
23 | import { Input } from "@/components/ui/input"
24 | import { useToast } from "@/components/ui/use-toast"
25 | import UserAvatar from "@/components/shared/user-avatar"
26 |
27 | interface UpdateFormProps {
28 | updateUserProfile: (values: UpdateUserProfileParams) => Promise<{success?: boolean}>
29 | }
30 |
31 | const UpdateForm = ({
32 | updateUserProfile
33 | }: UpdateFormProps) => {
34 | const { data: session, update } = useSession()
35 | const { pending } = useFormStatus()
36 | const { toast } = useToast()
37 |
38 | const form = useForm>({
39 | resolver: zodResolver(userUpdateValidation),
40 | defaultValues: {
41 | name: "",
42 | }
43 | })
44 |
45 | async function onSubmit(values: z.infer) {
46 | update({name: values.name})
47 | const res = await updateUserProfile(values)
48 |
49 | if (res?.success) {
50 | toast({
51 | description: "Update successfully."
52 | })
53 | }
54 | }
55 |
56 | return (
57 |
83 | {session?.user.provider === "credentials" && <>
84 |
87 |
88 |
89 | Change Password
90 |
91 |
92 | >}
93 |
94 | )
95 | }
96 |
97 | export default UpdateForm
--------------------------------------------------------------------------------
/components/shared/footer.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link"
2 | import { footerLinks } from "@/constants"
3 |
4 | import { Code } from "lucide-react"
5 |
6 | const Footer = () => {
7 | return (
8 |
47 | )
48 | }
49 |
50 | export default Footer
--------------------------------------------------------------------------------
/components/shared/main-nav.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { useState } from "react"
4 | import { usePathname } from "next/navigation"
5 | import Link from "next/link"
6 | import { cn } from "@/lib/utils"
7 | import { mainNavLinks } from "@/constants"
8 |
9 | import { Menu } from "lucide-react"
10 |
11 |
12 | const MainNav = () => {
13 | const [menuOpen, setMenuOpen] = useState(false)
14 | const pathName = usePathname()
15 |
16 | return (
17 |
18 |
21 |
26 | {
27 | mainNavLinks.map((link) => (
28 |
36 | {link.title}
37 |
38 | ))
39 | }
40 |
41 |
42 | )
43 | }
44 |
45 | export default MainNav
--------------------------------------------------------------------------------
/components/shared/mode-toggle.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { useTheme } from "next-themes"
4 |
5 | import { Button } from "@/components/ui/button"
6 | import { Moon, Sun } from "lucide-react"
7 |
8 | export default function ModeToggle() {
9 | const { theme, setTheme } = useTheme()
10 |
11 | const toggleTheme = () => {
12 | if (theme === "dark") {
13 | setTheme("light")
14 | } else {
15 | setTheme("dark")
16 | }
17 | }
18 |
19 | return (
20 |
25 | )
26 | }
--------------------------------------------------------------------------------
/components/shared/navbar.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link"
2 |
3 | import MainNav from "@/components/shared/main-nav"
4 | import UserNav from "@/components/shared/user-nav"
5 | import ModeToggle from "@/components/shared/mode-toggle"
6 | import { Code } from "lucide-react"
7 |
8 | const Navbar = () => {
9 | return (
10 |
11 |
21 |
22 | )
23 | }
24 |
25 | export default Navbar
--------------------------------------------------------------------------------
/components/shared/user-avatar.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 | import { useSession } from "next-auth/react"
3 |
4 | import { UserCircle2 } from "lucide-react"
5 | import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
6 |
7 | const UserAvatar = () => {
8 | const { data: session } = useSession()
9 | // console.log(session)
10 |
11 | return (
12 |
13 | {session?.user?.image ? (
14 |
15 |
16 | CN
17 |
18 | ) : (
19 |
20 | )}
21 |
{session?.user?.name}
22 |
23 | )
24 | }
25 |
26 | export default UserAvatar
--------------------------------------------------------------------------------
/components/shared/user-nav.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link"
2 | import { getUserSession } from "@/lib/actions/auth.actions"
3 |
4 | import {
5 | DropdownMenu,
6 | DropdownMenuContent,
7 | DropdownMenuItem,
8 | DropdownMenuLabel,
9 | DropdownMenuSeparator,
10 | DropdownMenuTrigger,
11 | } from "@/components/ui/dropdown-menu"
12 | import { buttonVariants } from "@/components/ui/button"
13 | import UserAvatar from "@/components/shared/user-avatar"
14 | import SignOutButton from "@/components/button/signout-button"
15 |
16 | const UserNav = async () => {
17 | const { session } = await getUserSession()
18 | // console.log(session)
19 |
20 | return (
21 |
22 | {session ? (
23 |
24 |
25 |
26 | My Account
27 |
28 |
29 |
30 | Profile
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 | ) : (
39 |
40 | Sign In
41 |
42 | )}
43 |
44 | )
45 | }
46 |
47 | export default UserNav
--------------------------------------------------------------------------------
/components/ui/avatar.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as AvatarPrimitive from "@radix-ui/react-avatar"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Avatar = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, ...props }, ref) => (
12 |
20 | ))
21 | Avatar.displayName = AvatarPrimitive.Root.displayName
22 |
23 | const AvatarImage = React.forwardRef<
24 | React.ElementRef,
25 | React.ComponentPropsWithoutRef
26 | >(({ className, ...props }, ref) => (
27 |
32 | ))
33 | AvatarImage.displayName = AvatarPrimitive.Image.displayName
34 |
35 | const AvatarFallback = React.forwardRef<
36 | React.ElementRef,
37 | React.ComponentPropsWithoutRef
38 | >(({ className, ...props }, ref) => (
39 |
47 | ))
48 | AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
49 |
50 | export { Avatar, AvatarImage, AvatarFallback }
51 |
--------------------------------------------------------------------------------
/components/ui/button.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { Slot } from "@radix-ui/react-slot"
3 | import { cva, type VariantProps } from "class-variance-authority"
4 |
5 | import { cn } from "@/lib/utils"
6 |
7 | const buttonVariants = cva(
8 | "inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
9 | {
10 | variants: {
11 | variant: {
12 | default: "bg-primary text-primary-foreground hover:bg-primary/90",
13 | destructive:
14 | "bg-destructive text-destructive-foreground hover:bg-destructive/90",
15 | outline:
16 | "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
17 | secondary:
18 | "bg-secondary text-secondary-foreground hover:bg-secondary/80",
19 | ghost: "hover:bg-accent hover:text-accent-foreground",
20 | link: "text-primary underline-offset-4 hover:underline",
21 | },
22 | size: {
23 | default: "h-10 px-4 py-2",
24 | sm: "h-9 rounded-md px-3",
25 | lg: "h-11 rounded-md px-8",
26 | icon: "h-10 w-10",
27 | },
28 | },
29 | defaultVariants: {
30 | variant: "default",
31 | size: "default",
32 | },
33 | }
34 | )
35 |
36 | export interface ButtonProps
37 | extends React.ButtonHTMLAttributes,
38 | VariantProps {
39 | asChild?: boolean
40 | }
41 |
42 | const Button = React.forwardRef(
43 | ({ className, variant, size, asChild = false, ...props }, ref) => {
44 | const Comp = asChild ? Slot : "button"
45 | return (
46 |
51 | )
52 | }
53 | )
54 | Button.displayName = "Button"
55 |
56 | export { Button, buttonVariants }
57 |
--------------------------------------------------------------------------------
/components/ui/dropdown-menu.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
5 | import { Check, ChevronRight, Circle } from "lucide-react"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const DropdownMenu = DropdownMenuPrimitive.Root
10 |
11 | const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
12 |
13 | const DropdownMenuGroup = DropdownMenuPrimitive.Group
14 |
15 | const DropdownMenuPortal = DropdownMenuPrimitive.Portal
16 |
17 | const DropdownMenuSub = DropdownMenuPrimitive.Sub
18 |
19 | const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
20 |
21 | const DropdownMenuSubTrigger = React.forwardRef<
22 | React.ElementRef,
23 | React.ComponentPropsWithoutRef & {
24 | inset?: boolean
25 | }
26 | >(({ className, inset, children, ...props }, ref) => (
27 |
36 | {children}
37 |
38 |
39 | ))
40 | DropdownMenuSubTrigger.displayName =
41 | DropdownMenuPrimitive.SubTrigger.displayName
42 |
43 | const DropdownMenuSubContent = React.forwardRef<
44 | React.ElementRef,
45 | React.ComponentPropsWithoutRef
46 | >(({ className, ...props }, ref) => (
47 |
55 | ))
56 | DropdownMenuSubContent.displayName =
57 | DropdownMenuPrimitive.SubContent.displayName
58 |
59 | const DropdownMenuContent = React.forwardRef<
60 | React.ElementRef,
61 | React.ComponentPropsWithoutRef
62 | >(({ className, sideOffset = 4, ...props }, ref) => (
63 |
64 |
73 |
74 | ))
75 | DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
76 |
77 | const DropdownMenuItem = React.forwardRef<
78 | React.ElementRef,
79 | React.ComponentPropsWithoutRef & {
80 | inset?: boolean
81 | }
82 | >(({ className, inset, ...props }, ref) => (
83 |
92 | ))
93 | DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
94 |
95 | const DropdownMenuCheckboxItem = React.forwardRef<
96 | React.ElementRef,
97 | React.ComponentPropsWithoutRef
98 | >(({ className, children, checked, ...props }, ref) => (
99 |
108 |
109 |
110 |
111 |
112 |
113 | {children}
114 |
115 | ))
116 | DropdownMenuCheckboxItem.displayName =
117 | DropdownMenuPrimitive.CheckboxItem.displayName
118 |
119 | const DropdownMenuRadioItem = React.forwardRef<
120 | React.ElementRef,
121 | React.ComponentPropsWithoutRef
122 | >(({ className, children, ...props }, ref) => (
123 |
131 |
132 |
133 |
134 |
135 |
136 | {children}
137 |
138 | ))
139 | DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
140 |
141 | const DropdownMenuLabel = React.forwardRef<
142 | React.ElementRef,
143 | React.ComponentPropsWithoutRef & {
144 | inset?: boolean
145 | }
146 | >(({ className, inset, ...props }, ref) => (
147 |
156 | ))
157 | DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
158 |
159 | const DropdownMenuSeparator = React.forwardRef<
160 | React.ElementRef,
161 | React.ComponentPropsWithoutRef
162 | >(({ className, ...props }, ref) => (
163 |
168 | ))
169 | DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
170 |
171 | const DropdownMenuShortcut = ({
172 | className,
173 | ...props
174 | }: React.HTMLAttributes) => {
175 | return (
176 |
180 | )
181 | }
182 | DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
183 |
184 | export {
185 | DropdownMenu,
186 | DropdownMenuTrigger,
187 | DropdownMenuContent,
188 | DropdownMenuItem,
189 | DropdownMenuCheckboxItem,
190 | DropdownMenuRadioItem,
191 | DropdownMenuLabel,
192 | DropdownMenuSeparator,
193 | DropdownMenuShortcut,
194 | DropdownMenuGroup,
195 | DropdownMenuPortal,
196 | DropdownMenuSub,
197 | DropdownMenuSubContent,
198 | DropdownMenuSubTrigger,
199 | DropdownMenuRadioGroup,
200 | }
201 |
--------------------------------------------------------------------------------
/components/ui/form.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as LabelPrimitive from "@radix-ui/react-label"
3 | import { Slot } from "@radix-ui/react-slot"
4 | import {
5 | Controller,
6 | ControllerProps,
7 | FieldPath,
8 | FieldValues,
9 | FormProvider,
10 | useFormContext,
11 | } from "react-hook-form"
12 |
13 | import { cn } from "@/lib/utils"
14 | import { Label } from "@/components/ui/label"
15 |
16 | const Form = FormProvider
17 |
18 | type FormFieldContextValue<
19 | TFieldValues extends FieldValues = FieldValues,
20 | TName extends FieldPath = FieldPath
21 | > = {
22 | name: TName
23 | }
24 |
25 | const FormFieldContext = React.createContext(
26 | {} as FormFieldContextValue
27 | )
28 |
29 | const FormField = <
30 | TFieldValues extends FieldValues = FieldValues,
31 | TName extends FieldPath = FieldPath
32 | >({
33 | ...props
34 | }: ControllerProps) => {
35 | return (
36 |
37 |
38 |
39 | )
40 | }
41 |
42 | const useFormField = () => {
43 | const fieldContext = React.useContext(FormFieldContext)
44 | const itemContext = React.useContext(FormItemContext)
45 | const { getFieldState, formState } = useFormContext()
46 |
47 | const fieldState = getFieldState(fieldContext.name, formState)
48 |
49 | if (!fieldContext) {
50 | throw new Error("useFormField should be used within ")
51 | }
52 |
53 | const { id } = itemContext
54 |
55 | return {
56 | id,
57 | name: fieldContext.name,
58 | formItemId: `${id}-form-item`,
59 | formDescriptionId: `${id}-form-item-description`,
60 | formMessageId: `${id}-form-item-message`,
61 | ...fieldState,
62 | }
63 | }
64 |
65 | type FormItemContextValue = {
66 | id: string
67 | }
68 |
69 | const FormItemContext = React.createContext(
70 | {} as FormItemContextValue
71 | )
72 |
73 | const FormItem = React.forwardRef<
74 | HTMLDivElement,
75 | React.HTMLAttributes
76 | >(({ className, ...props }, ref) => {
77 | const id = React.useId()
78 |
79 | return (
80 |
81 |
82 |
83 | )
84 | })
85 | FormItem.displayName = "FormItem"
86 |
87 | const FormLabel = React.forwardRef<
88 | React.ElementRef,
89 | React.ComponentPropsWithoutRef
90 | >(({ className, ...props }, ref) => {
91 | const { error, formItemId } = useFormField()
92 |
93 | return (
94 |
100 | )
101 | })
102 | FormLabel.displayName = "FormLabel"
103 |
104 | const FormControl = React.forwardRef<
105 | React.ElementRef,
106 | React.ComponentPropsWithoutRef
107 | >(({ ...props }, ref) => {
108 | const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
109 |
110 | return (
111 |
122 | )
123 | })
124 | FormControl.displayName = "FormControl"
125 |
126 | const FormDescription = React.forwardRef<
127 | HTMLParagraphElement,
128 | React.HTMLAttributes
129 | >(({ className, ...props }, ref) => {
130 | const { formDescriptionId } = useFormField()
131 |
132 | return (
133 |
139 | )
140 | })
141 | FormDescription.displayName = "FormDescription"
142 |
143 | const FormMessage = React.forwardRef<
144 | HTMLParagraphElement,
145 | React.HTMLAttributes
146 | >(({ className, children, ...props }, ref) => {
147 | const { error, formMessageId } = useFormField()
148 | const body = error ? String(error?.message) : children
149 |
150 | if (!body) {
151 | return null
152 | }
153 |
154 | return (
155 |
161 | {body}
162 |
163 | )
164 | })
165 | FormMessage.displayName = "FormMessage"
166 |
167 | export {
168 | useFormField,
169 | Form,
170 | FormItem,
171 | FormLabel,
172 | FormControl,
173 | FormDescription,
174 | FormMessage,
175 | FormField,
176 | }
177 |
--------------------------------------------------------------------------------
/components/ui/input.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | export interface InputProps
6 | extends React.InputHTMLAttributes {}
7 |
8 | const Input = React.forwardRef(
9 | ({ className, type, ...props }, ref) => {
10 | return (
11 |
20 | )
21 | }
22 | )
23 | Input.displayName = "Input"
24 |
25 | export { Input }
26 |
--------------------------------------------------------------------------------
/components/ui/label.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as LabelPrimitive from "@radix-ui/react-label"
5 | import { cva, type VariantProps } from "class-variance-authority"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const labelVariants = cva(
10 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
11 | )
12 |
13 | const Label = React.forwardRef<
14 | React.ElementRef,
15 | React.ComponentPropsWithoutRef &
16 | VariantProps
17 | >(({ className, ...props }, ref) => (
18 |
23 | ))
24 | Label.displayName = LabelPrimitive.Root.displayName
25 |
26 | export { Label }
27 |
--------------------------------------------------------------------------------
/components/ui/toast.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as ToastPrimitives from "@radix-ui/react-toast"
3 | import { cva, type VariantProps } from "class-variance-authority"
4 | import { X } from "lucide-react"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const ToastProvider = ToastPrimitives.Provider
9 |
10 | const ToastViewport = React.forwardRef<
11 | React.ElementRef,
12 | React.ComponentPropsWithoutRef
13 | >(({ className, ...props }, ref) => (
14 |
22 | ))
23 | ToastViewport.displayName = ToastPrimitives.Viewport.displayName
24 |
25 | const toastVariants = cva(
26 | "group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
27 | {
28 | variants: {
29 | variant: {
30 | default: "border bg-background text-foreground",
31 | destructive:
32 | "destructive group border-destructive bg-destructive text-destructive-foreground",
33 | },
34 | },
35 | defaultVariants: {
36 | variant: "default",
37 | },
38 | }
39 | )
40 |
41 | const Toast = React.forwardRef<
42 | React.ElementRef,
43 | React.ComponentPropsWithoutRef &
44 | VariantProps
45 | >(({ className, variant, ...props }, ref) => {
46 | return (
47 |
52 | )
53 | })
54 | Toast.displayName = ToastPrimitives.Root.displayName
55 |
56 | const ToastAction = React.forwardRef<
57 | React.ElementRef,
58 | React.ComponentPropsWithoutRef
59 | >(({ className, ...props }, ref) => (
60 |
68 | ))
69 | ToastAction.displayName = ToastPrimitives.Action.displayName
70 |
71 | const ToastClose = React.forwardRef<
72 | React.ElementRef,
73 | React.ComponentPropsWithoutRef
74 | >(({ className, ...props }, ref) => (
75 |
84 |
85 |
86 | ))
87 | ToastClose.displayName = ToastPrimitives.Close.displayName
88 |
89 | const ToastTitle = React.forwardRef<
90 | React.ElementRef,
91 | React.ComponentPropsWithoutRef
92 | >(({ className, ...props }, ref) => (
93 |
98 | ))
99 | ToastTitle.displayName = ToastPrimitives.Title.displayName
100 |
101 | const ToastDescription = React.forwardRef<
102 | React.ElementRef,
103 | React.ComponentPropsWithoutRef
104 | >(({ className, ...props }, ref) => (
105 |
110 | ))
111 | ToastDescription.displayName = ToastPrimitives.Description.displayName
112 |
113 | type ToastProps = React.ComponentPropsWithoutRef
114 |
115 | type ToastActionElement = React.ReactElement
116 |
117 | export {
118 | type ToastProps,
119 | type ToastActionElement,
120 | ToastProvider,
121 | ToastViewport,
122 | Toast,
123 | ToastTitle,
124 | ToastDescription,
125 | ToastClose,
126 | ToastAction,
127 | }
128 |
--------------------------------------------------------------------------------
/components/ui/toaster.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import {
4 | Toast,
5 | ToastClose,
6 | ToastDescription,
7 | ToastProvider,
8 | ToastTitle,
9 | ToastViewport,
10 | } from "@/components/ui/toast"
11 | import { useToast } from "@/components/ui/use-toast"
12 |
13 | export function Toaster() {
14 | const { toasts } = useToast()
15 |
16 | return (
17 |
18 | {toasts.map(function ({ id, title, description, action, ...props }) {
19 | return (
20 |
21 |
22 | {title && {title}}
23 | {description && (
24 | {description}
25 | )}
26 |
27 | {action}
28 |
29 |
30 | )
31 | })}
32 |
33 |
34 | )
35 | }
36 |
--------------------------------------------------------------------------------
/components/ui/use-toast.ts:
--------------------------------------------------------------------------------
1 | // Inspired by react-hot-toast library
2 | import * as React from "react"
3 |
4 | import type {
5 | ToastActionElement,
6 | ToastProps,
7 | } from "@/components/ui/toast"
8 |
9 | const TOAST_LIMIT = 1
10 | const TOAST_REMOVE_DELAY = 1000000
11 |
12 | type ToasterToast = ToastProps & {
13 | id: string
14 | title?: React.ReactNode
15 | description?: React.ReactNode
16 | action?: ToastActionElement
17 | }
18 |
19 | const actionTypes = {
20 | ADD_TOAST: "ADD_TOAST",
21 | UPDATE_TOAST: "UPDATE_TOAST",
22 | DISMISS_TOAST: "DISMISS_TOAST",
23 | REMOVE_TOAST: "REMOVE_TOAST",
24 | } as const
25 |
26 | let count = 0
27 |
28 | function genId() {
29 | count = (count + 1) % Number.MAX_VALUE
30 | return count.toString()
31 | }
32 |
33 | type ActionType = typeof actionTypes
34 |
35 | type Action =
36 | | {
37 | type: ActionType["ADD_TOAST"]
38 | toast: ToasterToast
39 | }
40 | | {
41 | type: ActionType["UPDATE_TOAST"]
42 | toast: Partial
43 | }
44 | | {
45 | type: ActionType["DISMISS_TOAST"]
46 | toastId?: ToasterToast["id"]
47 | }
48 | | {
49 | type: ActionType["REMOVE_TOAST"]
50 | toastId?: ToasterToast["id"]
51 | }
52 |
53 | interface State {
54 | toasts: ToasterToast[]
55 | }
56 |
57 | const toastTimeouts = new Map>()
58 |
59 | const addToRemoveQueue = (toastId: string) => {
60 | if (toastTimeouts.has(toastId)) {
61 | return
62 | }
63 |
64 | const timeout = setTimeout(() => {
65 | toastTimeouts.delete(toastId)
66 | dispatch({
67 | type: "REMOVE_TOAST",
68 | toastId: toastId,
69 | })
70 | }, TOAST_REMOVE_DELAY)
71 |
72 | toastTimeouts.set(toastId, timeout)
73 | }
74 |
75 | export const reducer = (state: State, action: Action): State => {
76 | switch (action.type) {
77 | case "ADD_TOAST":
78 | return {
79 | ...state,
80 | toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
81 | }
82 |
83 | case "UPDATE_TOAST":
84 | return {
85 | ...state,
86 | toasts: state.toasts.map((t) =>
87 | t.id === action.toast.id ? { ...t, ...action.toast } : t
88 | ),
89 | }
90 |
91 | case "DISMISS_TOAST": {
92 | const { toastId } = action
93 |
94 | // ! Side effects ! - This could be extracted into a dismissToast() action,
95 | // but I'll keep it here for simplicity
96 | if (toastId) {
97 | addToRemoveQueue(toastId)
98 | } else {
99 | state.toasts.forEach((toast) => {
100 | addToRemoveQueue(toast.id)
101 | })
102 | }
103 |
104 | return {
105 | ...state,
106 | toasts: state.toasts.map((t) =>
107 | t.id === toastId || toastId === undefined
108 | ? {
109 | ...t,
110 | open: false,
111 | }
112 | : t
113 | ),
114 | }
115 | }
116 | case "REMOVE_TOAST":
117 | if (action.toastId === undefined) {
118 | return {
119 | ...state,
120 | toasts: [],
121 | }
122 | }
123 | return {
124 | ...state,
125 | toasts: state.toasts.filter((t) => t.id !== action.toastId),
126 | }
127 | }
128 | }
129 |
130 | const listeners: Array<(state: State) => void> = []
131 |
132 | let memoryState: State = { toasts: [] }
133 |
134 | function dispatch(action: Action) {
135 | memoryState = reducer(memoryState, action)
136 | listeners.forEach((listener) => {
137 | listener(memoryState)
138 | })
139 | }
140 |
141 | type Toast = Omit
142 |
143 | function toast({ ...props }: Toast) {
144 | const id = genId()
145 |
146 | const update = (props: ToasterToast) =>
147 | dispatch({
148 | type: "UPDATE_TOAST",
149 | toast: { ...props, id },
150 | })
151 | const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id })
152 |
153 | dispatch({
154 | type: "ADD_TOAST",
155 | toast: {
156 | ...props,
157 | id,
158 | open: true,
159 | onOpenChange: (open) => {
160 | if (!open) dismiss()
161 | },
162 | },
163 | })
164 |
165 | return {
166 | id: id,
167 | dismiss,
168 | update,
169 | }
170 | }
171 |
172 | function useToast() {
173 | const [state, setState] = React.useState(memoryState)
174 |
175 | React.useEffect(() => {
176 | listeners.push(setState)
177 | return () => {
178 | const index = listeners.indexOf(setState)
179 | if (index > -1) {
180 | listeners.splice(index, 1)
181 | }
182 | }
183 | }, [state])
184 |
185 | return {
186 | ...state,
187 | toast,
188 | dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
189 | }
190 | }
191 |
192 | export { useToast, toast }
193 |
--------------------------------------------------------------------------------
/constants/index.ts:
--------------------------------------------------------------------------------
1 | export const mainNavLinks = [
2 | { title: "New Arrivals", url: "/" },
3 | { title: "Best Sellers", url: "/" },
4 | { title: "Sales", url: "/" },
5 | { title: "Categories", url: "/" }
6 | ]
7 |
8 | export const footerLinks = [
9 | {
10 | title: "Shop",
11 | links: [
12 | { title: "New Arrivals", url: "/" },
13 | { title: "Best Sellers", url: "/" },
14 | { title: "Sales", url: "/" },
15 | { title: "Categories", url: "/" }
16 | ],
17 | },
18 | {
19 | title: "Support",
20 | links: [
21 | { title: "Customer Service", url: "/" },
22 | { title: "Returns & Exchanges", url: "/" },
23 | { title: "Shipping Information", url: "/" },
24 | { title: "Size Guide", url: "/" }
25 | ],
26 | },
27 | {
28 | title: "Company",
29 | links: [
30 | { title: "About Us", url: "/" },
31 | { title: "Careers", url: "/" },
32 | { title: "Blog", url: "/" },
33 | { title: "Affiliate Program", url: "/" }
34 | ],
35 | },
36 | {
37 | title: "Socials",
38 | links: [
39 | { title: "Instagram", url: "/" },
40 | { title: "Twitter", url: "/" },
41 | { title: "Facebook", url: "/" },
42 | { title: "Pinterest", url: "/" }
43 | ],
44 | }
45 | ]
--------------------------------------------------------------------------------
/lib/actions/auth.actions.ts:
--------------------------------------------------------------------------------
1 | "use server" // 開頭統一加上"use server"即可
2 |
3 | import { getServerSession } from "next-auth/next"
4 | import { Account, Profile } from "next-auth"
5 | import { redirect } from "next/navigation"
6 | import bcrypt from "bcrypt"
7 | import { nextauthOptions } from "@/lib/nextauth-options"
8 | import connectDB from "@/lib/mongodb"
9 | import User from "@/lib/models/user.model"
10 |
11 | export async function getUserSession() {
12 | const session = await getServerSession(nextauthOptions)
13 | return ({ session })
14 | }
15 |
16 | interface ExtendedProfile extends Profile {
17 | picture?: string
18 | }
19 |
20 | interface SignInWithOauthParams {
21 | account: Account,
22 | profile: ExtendedProfile
23 | }
24 |
25 | export async function signInWithOauth({
26 | account,
27 | profile
28 | }: SignInWithOauthParams) {
29 | // console.log({account, profile})
30 | connectDB()
31 |
32 | const user = await User.findOne({email: profile.email})
33 |
34 | if (user) return true
35 |
36 | const newUser = new User({
37 | name: profile.name,
38 | email: profile.email,
39 | image: profile.picture,
40 | provider: account.provider
41 | })
42 |
43 | // console.log(newUser)
44 | await newUser.save()
45 |
46 | return true
47 | }
48 |
49 | interface GetUserByEmailParams {
50 | email: string
51 | }
52 |
53 | export async function getUserByEmail({
54 | email
55 | }: GetUserByEmailParams) {
56 | connectDB()
57 |
58 | const user = await User.findOne({email}).select("-password")
59 |
60 | if (!user) {
61 | throw new Error ("User does not exist!")
62 | }
63 |
64 | // console.log({user})
65 | return {...user._doc, _id: user._id.toString()}
66 | }
67 |
68 | export interface UpdateUserProfileParams {
69 | name: string
70 | }
71 |
72 | export async function updateUserProfile({
73 | name
74 | }: UpdateUserProfileParams) {
75 | const session = await getServerSession(nextauthOptions)
76 | // console.log(session)
77 |
78 | connectDB()
79 |
80 | try {
81 | if (!session) {
82 | throw new Error("Unauthorization!")
83 | }
84 |
85 | const user = await User.findByIdAndUpdate(session?.user?._id, {
86 | name
87 | }, { new: true }).select("-password")
88 |
89 | if (!user) {
90 | throw new Error ("User does not exist!")
91 | }
92 |
93 | return { success: true }
94 | } catch (error) {
95 | redirect(`/error?error=${(error as Error).message}`)
96 | }
97 | }
98 |
99 | export interface SignUpWithCredentialsParams {
100 | name: string,
101 | email: string,
102 | password: string
103 | }
104 |
105 | export async function signUpWithCredentials ({
106 | name,
107 | email,
108 | password
109 | }: SignUpWithCredentialsParams) {
110 | connectDB()
111 |
112 | try {
113 | const user = await User.findOne({email})
114 |
115 | if (user) {
116 | throw new Error("User already exists.")
117 | }
118 |
119 | const salt = await bcrypt.genSalt(10)
120 | const hashedPassword = await bcrypt.hash(password, salt)
121 |
122 | const newUser = new User({
123 | name,
124 | email,
125 | password: hashedPassword
126 | })
127 |
128 | // console.log({newUser})
129 | await newUser.save()
130 |
131 | return { success: true }
132 | } catch (error) {
133 | redirect(`/error?error=${(error as Error).message}`)
134 | }
135 | }
136 |
137 | interface SignInWithCredentialsParams {
138 | email: string,
139 | password: string
140 | }
141 |
142 | export async function signInWithCredentials ({
143 | email,
144 | password
145 | }: SignInWithCredentialsParams) {
146 | connectDB()
147 |
148 | const user = await User.findOne({email})
149 |
150 | if (!user) {
151 | throw new Error("Invalid email or password!")
152 | }
153 |
154 | const passwordIsValid = await bcrypt.compare(
155 | password,
156 | user.password
157 | )
158 |
159 | if (!passwordIsValid) {
160 | throw new Error("Invalid email or password")
161 | }
162 |
163 | return {...user._doc, _id: user._id.toString()}
164 | }
165 |
166 | export interface ChangeUserPasswordParams {
167 | oldPassword: string,
168 | newPassword: string
169 | }
170 |
171 | export async function changeUserPassword ({
172 | oldPassword,
173 | newPassword
174 | }: ChangeUserPasswordParams) {
175 | const session = await getServerSession(nextauthOptions)
176 | // console.log(session)
177 |
178 | connectDB()
179 |
180 | try {
181 | if (!session) {
182 | throw new Error("Unauthorization!")
183 | }
184 |
185 | if (session?.user?.provider !== "credentials") {
186 | throw new Error(`Signed in via ${session?.user?.provider}. Changes not allowed with this method.`)
187 | }
188 |
189 | const user = await User.findById(session?.user?._id)
190 |
191 | if (!user) {
192 | throw new Error("User does not exist!")
193 | }
194 |
195 | const passwordIsValid = await bcrypt.compare(
196 | oldPassword,
197 | user.password
198 | )
199 |
200 | if (!passwordIsValid) {
201 | throw new Error("Incorrect old password.")
202 | }
203 |
204 | const salt = await bcrypt.genSalt(10)
205 | const hashedPassword = await bcrypt.hash(newPassword, salt)
206 |
207 | await User.findByIdAndUpdate(user._id, {
208 | password: hashedPassword
209 | })
210 |
211 | return { success: true }
212 | } catch (error) {
213 | redirect(`/error?error=${(error as Error).message}`)
214 | }
215 | }
--------------------------------------------------------------------------------
/lib/models/user.model.ts:
--------------------------------------------------------------------------------
1 | import mongoose from "mongoose"
2 |
3 | const userSchema = new mongoose.Schema({
4 | name: {
5 | type: String,
6 | required: true
7 | },
8 | email: {
9 | type: String,
10 | unique: true,
11 | required: true
12 | },
13 | password: {
14 | type: String
15 | },
16 | image: {
17 | type: String
18 | },
19 | role: {
20 | type: String,
21 | default: "user"
22 | },
23 | provider: {
24 | type: String,
25 | default: "credentials"
26 | }
27 | }, { timestamps: true })
28 |
29 | const User = mongoose.models.User || mongoose.model("User", userSchema)
30 |
31 | export default User
--------------------------------------------------------------------------------
/lib/mongodb.ts:
--------------------------------------------------------------------------------
1 | import mongoose from "mongoose"
2 |
3 | const connection: { isConnected?: number } ={}
4 |
5 | const connectDB = async () => {
6 | if (connection.isConnected) {
7 | return
8 | }
9 |
10 | if (!process.env.MONGODB_URI) {
11 | console.log("Error: Invalid/Missing environment variable MONGODB_URI")
12 | return
13 | }
14 |
15 | try {
16 | const db = await mongoose.connect(process.env.MONGODB_URI)
17 | // console.log(db)
18 | connection.isConnected = db.connections[0].readyState
19 |
20 | if (connection.isConnected === 1) {
21 | console.log("🚀 Successfully connected to database")
22 | } else {
23 | console.log("🔴 Failed to connect to database")
24 | }
25 | } catch (error) {
26 | console.log("🔴 Failed to connect to MongoDB:", (error as Error).message)
27 | }
28 | }
29 |
30 | export default connectDB
--------------------------------------------------------------------------------
/lib/nextauth-options.ts:
--------------------------------------------------------------------------------
1 | import { NextAuthOptions } from "next-auth"
2 | import GoogleProvider from "next-auth/providers/google"
3 | import CredentialsProvider from "next-auth/providers/credentials"
4 | import { signInWithOauth, getUserByEmail, signInWithCredentials } from "@/lib/actions/auth.actions"
5 |
6 | export const nextauthOptions: NextAuthOptions = {
7 | secret: process.env.NEXTAUTH_SECRET,
8 | pages: {
9 | signIn: "/signin", // app/signin
10 | error: "/error", // app/error
11 | },
12 | providers: [
13 | GoogleProvider({
14 | clientId: process.env.GOOGLE_CLIENT_ID!,
15 | clientSecret: process.env.GOOGLE_CLIENT_SECRET!
16 | }),
17 | CredentialsProvider({
18 | name: "Credentials",
19 | credentials: {
20 | email: { label: "Email", type: "email", required: true },
21 | password: { label: "Password", type: "password", required: true }
22 | },
23 | async authorize(credentials) {
24 | // console.log(credentials)
25 | if (!credentials?.email || !credentials?.password) {
26 | return null
27 | }
28 |
29 | const user = await signInWithCredentials({
30 | email: credentials?.email,
31 | password: credentials?.password
32 | })
33 |
34 | // console.log({user})
35 | return user
36 | }
37 | })
38 | ],
39 | callbacks: {
40 | async signIn({ account, profile }) {
41 | // console.log({account, profile})
42 | if (account?.type === "oauth" && profile) {
43 | return await signInWithOauth({account, profile})
44 | }
45 | return true
46 | },
47 | async jwt({ token, trigger, session }) {
48 | // console.log({token})
49 | // console.log({trigger, session})
50 | if (trigger === "update") {
51 | token.name = session.name
52 | } else {
53 | if (token.email) {
54 | const user = await getUserByEmail({email: token.email})
55 | // console.log({user})
56 | token.name = user.name
57 | token._id = user._id
58 | token.role = user.role
59 | token.provider = user.provider
60 | }
61 | }
62 | return token
63 | },
64 | async session({ session, token }) {
65 | // console.log({session, token})
66 | return {
67 | ...session,
68 | user: {
69 | ...session.user,
70 | name: token.name,
71 | _id: token._id,
72 | role: token.role,
73 | provider: token.provider
74 | }
75 | }
76 | }
77 | }
78 | }
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/lib/utils/token.ts:
--------------------------------------------------------------------------------
1 | import jwt from "jsonwebtoken"
2 |
3 | export const generateToken = (payload: any) => {
4 | return jwt.sign(
5 | payload,
6 | process.env.TOKEN_SECRET!,
7 | { expiresIn: "30d"}
8 | )
9 | }
10 |
11 | export const verifyToken = (token: string) => {
12 | return jwt.verify(
13 | token,
14 | process.env.TOKEN_SECRET!
15 | )
16 | }
--------------------------------------------------------------------------------
/lib/validations/auth.ts:
--------------------------------------------------------------------------------
1 | import * as z from "zod"
2 |
3 | export const userSignInValidation = z.object({
4 | email: z.string()
5 | .min(1, "Email is required")
6 | .email("Invalid email"),
7 | password: z.string()
8 | .min(1, "Password is required")
9 | .min(8, "Password must be 8+ characters"),
10 | })
11 |
12 | export const userSignUpValidation = z
13 | .object({
14 | name: z.string()
15 | .min(1, "Username is required")
16 | .max(50, "Username must be less than 50 characters"),
17 | email: z.string()
18 | .min(1, "Email is required")
19 | .email("Invalid email"),
20 | password: z.string()
21 | .min(1, "Password is required")
22 | .min(8, "Password must be 8+ characters"),
23 | confirmPassword: z.string()
24 | .min(1, "Password confirmation is required"),
25 | })
26 | .refine((data) => data.password === data.confirmPassword, {
27 | path: ["confirmPassword"],
28 | message: "Password do not match",
29 | })
30 |
31 | export const userUpdateValidation = z
32 | .object({
33 | name: z.string()
34 | .min(1, "Username is required")
35 | .max(50, "Username must be less than 50 characters"),
36 | })
37 |
38 | export const changePasswordValidation = z
39 | .object({
40 | oldPassword: z.string()
41 | .min(1, "Old password is required")
42 | .min(8, "Password must be 8+ characters"),
43 | newPassword: z.string()
44 | .min(1, "New password is required")
45 | .min(8, "Password must be 8+ characters"),
46 | confirmPassword: z.string()
47 | .min(1, "Password confirmation is required"),
48 | })
49 | .refine((data) => data.oldPassword !== data.newPassword, {
50 | path: ["newPassword"],
51 | message: "New password must differ from old",
52 | })
53 | .refine((data) => data.newPassword === data.confirmPassword, {
54 | path: ["confirmPassword"],
55 | message: "Password do not match",
56 | })
--------------------------------------------------------------------------------
/middleware.ts:
--------------------------------------------------------------------------------
1 | // export { default } from "next-auth/middleware"
2 |
3 | // export const config = { matcher: ["/profile"] }
4 |
5 | import { withAuth } from "next-auth/middleware"
6 | import { NextResponse } from "next/server"
7 |
8 | export default withAuth(
9 | // `withAuth` augments your `Request` with the user's token.
10 | function middleware(req) {
11 | // console.log(req.nextauth.token)
12 | // console.log(req.nextUrl)
13 | const { token } = req.nextauth
14 | const { pathname, origin } = req.nextUrl
15 |
16 | if (pathname.startsWith("/dashboard") && token?.role !== "admin") {
17 | return NextResponse.redirect(`${origin}/unauthorized`)
18 | }
19 | },
20 | {
21 | callbacks: {
22 | // If `authorized` returns `true`, the middleware function will execute.
23 | authorized: ({ token }) => !!token
24 | },
25 | }
26 | )
27 |
28 | export const config = { matcher: ["/profile", "/dashboard/:path*"] }
--------------------------------------------------------------------------------
/next.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import("next").NextConfig} */
2 | const nextConfig = {
3 | images: {
4 | domains: ['lh3.googleusercontent.com']
5 | },
6 | // Next.js 14 版本之後可省略
7 | experimental: {
8 | serverActions: true
9 | }
10 | }
11 |
12 | module.exports = nextConfig
13 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "nextauth-mongodb-typescript-example",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "next lint --fix"
10 | },
11 | "dependencies": {
12 | "@hookform/resolvers": "^3.3.1",
13 | "@radix-ui/react-avatar": "^1.0.3",
14 | "@radix-ui/react-dropdown-menu": "^2.0.5",
15 | "@radix-ui/react-label": "^2.0.2",
16 | "@radix-ui/react-slot": "^1.0.2",
17 | "@radix-ui/react-toast": "^1.1.4",
18 | "@types/bcrypt": "^5.0.0",
19 | "@types/jsonwebtoken": "^9.0.2",
20 | "@types/node": "20.5.9",
21 | "@types/react": "18.2.21",
22 | "@types/react-dom": "18.2.7",
23 | "autoprefixer": "10.4.15",
24 | "bcrypt": "^5.1.1",
25 | "class-variance-authority": "^0.7.0",
26 | "clsx": "^2.0.0",
27 | "eslint": "8.48.0",
28 | "eslint-config-next": "13.4.19",
29 | "jsonwebtoken": "^9.0.2",
30 | "lucide-react": "^0.274.0",
31 | "mongoose": "^7.5.0",
32 | "next": "13.4.19",
33 | "next-auth": "^4.23.1",
34 | "next-themes": "^0.2.1",
35 | "postcss": "8.4.29",
36 | "react": "18.2.0",
37 | "react-dom": "18.2.0",
38 | "react-hook-form": "^7.46.1",
39 | "tailwind-merge": "^1.14.0",
40 | "tailwindcss": "3.3.3",
41 | "tailwindcss-animate": "^1.0.7",
42 | "typescript": "5.2.2",
43 | "zod": "^3.22.2"
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/providers/auth-provider.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { SessionProvider } from "next-auth/react"
4 |
5 | interface ProviderProps {
6 | children: React.ReactNode
7 | }
8 | export default function AuthProvider({children}: ProviderProps) {
9 | return {children}
10 | }
--------------------------------------------------------------------------------
/providers/theme-provider.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import { ThemeProvider as NextThemesProvider } from "next-themes"
5 | import { type ThemeProviderProps } from "next-themes/dist/types"
6 |
7 | export default function ThemeProvider({ children, ...props }: ThemeProviderProps) {
8 | return {children}
9 | }
--------------------------------------------------------------------------------
/public/next.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | module.exports = {
3 | darkMode: ["class"],
4 | content: [
5 | './pages/**/*.{ts,tsx}',
6 | './components/**/*.{ts,tsx}',
7 | './app/**/*.{ts,tsx}',
8 | './src/**/*.{ts,tsx}',
9 | ],
10 | theme: {
11 | container: {
12 | center: true,
13 | padding: "2rem",
14 | screens: {
15 | "2xl": "1400px",
16 | },
17 | },
18 | extend: {
19 | colors: {
20 | border: "hsl(var(--border))",
21 | input: "hsl(var(--input))",
22 | ring: "hsl(var(--ring))",
23 | background: "hsl(var(--background))",
24 | foreground: "hsl(var(--foreground))",
25 | primary: {
26 | DEFAULT: "hsl(var(--primary))",
27 | foreground: "hsl(var(--primary-foreground))",
28 | },
29 | secondary: {
30 | DEFAULT: "hsl(var(--secondary))",
31 | foreground: "hsl(var(--secondary-foreground))",
32 | },
33 | destructive: {
34 | DEFAULT: "hsl(var(--destructive))",
35 | foreground: "hsl(var(--destructive-foreground))",
36 | },
37 | muted: {
38 | DEFAULT: "hsl(var(--muted))",
39 | foreground: "hsl(var(--muted-foreground))",
40 | },
41 | accent: {
42 | DEFAULT: "hsl(var(--accent))",
43 | foreground: "hsl(var(--accent-foreground))",
44 | },
45 | popover: {
46 | DEFAULT: "hsl(var(--popover))",
47 | foreground: "hsl(var(--popover-foreground))",
48 | },
49 | card: {
50 | DEFAULT: "hsl(var(--card))",
51 | foreground: "hsl(var(--card-foreground))",
52 | },
53 | },
54 | borderRadius: {
55 | lg: "var(--radius)",
56 | md: "calc(var(--radius) - 2px)",
57 | sm: "calc(var(--radius) - 4px)",
58 | },
59 | keyframes: {
60 | "accordion-down": {
61 | from: { height: 0 },
62 | to: { height: "var(--radix-accordion-content-height)" },
63 | },
64 | "accordion-up": {
65 | from: { height: "var(--radix-accordion-content-height)" },
66 | to: { height: 0 },
67 | },
68 | },
69 | animation: {
70 | "accordion-down": "accordion-down 0.2s ease-out",
71 | "accordion-up": "accordion-up 0.2s ease-out",
72 | },
73 | },
74 | },
75 | plugins: [require("tailwindcss-animate")],
76 | }
--------------------------------------------------------------------------------
/tailwind.config.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from 'tailwindcss'
2 |
3 | const config: Config = {
4 | content: [
5 | './pages/**/*.{js,ts,jsx,tsx,mdx}',
6 | './components/**/*.{js,ts,jsx,tsx,mdx}',
7 | './app/**/*.{js,ts,jsx,tsx,mdx}',
8 | ],
9 | theme: {
10 | extend: {
11 | backgroundImage: {
12 | 'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))',
13 | 'gradient-conic':
14 | 'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))',
15 | },
16 | },
17 | },
18 | plugins: [],
19 | }
20 | export default config
21 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "strict": true,
8 | "noEmit": true,
9 | "esModuleInterop": true,
10 | "module": "esnext",
11 | "moduleResolution": "bundler",
12 | "resolveJsonModule": true,
13 | "isolatedModules": true,
14 | "jsx": "preserve",
15 | "incremental": true,
16 | "plugins": [
17 | {
18 | "name": "next"
19 | }
20 | ],
21 | "paths": {
22 | "@/*": ["./*"]
23 | }
24 | },
25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
26 | "exclude": ["node_modules"]
27 | }
28 |
--------------------------------------------------------------------------------
/types/next-auth.d.ts:
--------------------------------------------------------------------------------
1 | import NextAuth from "next-auth"
2 |
3 | declare module "next-auth" {
4 | interface User {
5 | _id: string
6 | role: string
7 | provider: string
8 | }
9 | interface Session {
10 | user: User & {
11 | _id: string
12 | role: string
13 | provider: string
14 | }
15 | token: {
16 | _id: string
17 | role: string
18 | provider: string
19 | }
20 | }
21 | }
--------------------------------------------------------------------------------