├── .eslintrc.json ├── .gitignore ├── README.md ├── components.json ├── next.config.mjs ├── package-lock.json ├── package.json ├── postcss.config.mjs ├── src ├── app │ ├── api │ │ └── auth │ │ │ ├── [...nextauth] │ │ │ └── route.ts │ │ │ └── signup │ │ │ └── route.ts │ ├── favicon.ico │ ├── fonts │ │ ├── GeistMonoVF.woff │ │ └── GeistVF.woff │ ├── globals.css │ ├── layout.tsx │ ├── page.tsx │ ├── sign-in │ │ └── page.tsx │ └── sign-up │ │ └── page.tsx ├── components │ ├── ui │ │ ├── avatar.tsx │ │ ├── button.tsx │ │ ├── card.tsx │ │ ├── dropdown-menu.tsx │ │ ├── input.tsx │ │ ├── separator.tsx │ │ └── sonner.tsx │ └── user-button.tsx ├── lib │ ├── mongodb.ts │ └── utils.ts └── models │ └── user.ts ├── tailwind.config.ts └── tsconfig.json /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["next/core-web-vitals", "next/typescript"] 3 | } 4 | -------------------------------------------------------------------------------- /.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 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /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.ts", 8 | "css": "src/app/globals.css", 9 | "baseColor": "neutral", 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 | } -------------------------------------------------------------------------------- /next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = {}; 3 | 4 | export default nextConfig; 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "auth", 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" 10 | }, 11 | "dependencies": { 12 | "@radix-ui/react-avatar": "^1.1.1", 13 | "@radix-ui/react-dropdown-menu": "^2.1.2", 14 | "@radix-ui/react-separator": "^1.1.0", 15 | "@radix-ui/react-slot": "^1.1.0", 16 | "@types/bcryptjs": "^2.4.6", 17 | "bcrypt": "^5.1.1", 18 | "bcryptjs": "^2.4.3", 19 | "class-variance-authority": "^0.7.0", 20 | "clsx": "^2.1.1", 21 | "lucide-react": "^0.453.0", 22 | "mongoose": "^8.7.1", 23 | "next": "14.2.15", 24 | "next-auth": "^4.24.8", 25 | "next-themes": "^0.3.0", 26 | "react": "^18", 27 | "react-dom": "^18", 28 | "react-icons": "^5.3.0", 29 | "save-dev": "^0.0.1-security", 30 | "sonner": "^1.5.0", 31 | "tailwind-merge": "^2.5.4", 32 | "tailwindcss-animate": "^1.0.7" 33 | }, 34 | "devDependencies": { 35 | "@types/node": "^20", 36 | "@types/react": "^18", 37 | "@types/react-dom": "^18", 38 | "eslint": "^8", 39 | "eslint-config-next": "14.2.15", 40 | "postcss": "^8", 41 | "tailwindcss": "^3.4.1", 42 | "typescript": "^5" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /postcss.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('postcss-load-config').Config} */ 2 | const config = { 3 | plugins: { 4 | tailwindcss: {}, 5 | }, 6 | }; 7 | 8 | export default config; 9 | -------------------------------------------------------------------------------- /src/app/api/auth/[...nextauth]/route.ts: -------------------------------------------------------------------------------- 1 | import NextAuth from "next-auth"; 2 | import User from "@/models/user"; 3 | import connectToDatabase from "@/lib/mongodb"; 4 | import bcrypt from "bcryptjs"; 5 | import CredentialsProvider from "next-auth/providers/credentials"; 6 | import Github from "next-auth/providers/github"; 7 | 8 | const handler = NextAuth({ 9 | session: { 10 | strategy: "jwt", 11 | }, 12 | providers: [ 13 | 14 | Github({ 15 | clientId: process.env.GITHUB_ID as string, 16 | clientSecret: process.env.GITHUB_SECRET as string, 17 | }), 18 | CredentialsProvider({ 19 | name: "Credentials", 20 | credentials: { 21 | email: {}, 22 | password:{}, 23 | }, 24 | async authorize(credentials) { 25 | try { 26 | await connectToDatabase(); 27 | const user = await User.findOne({ email: credentials?.email }); 28 | if (!user) { 29 | throw new Error("") 30 | } 31 | const isValidPassword = await bcrypt.compare( 32 | credentials?.password ?? "", user.password as string 33 | ); 34 | if (!isValidPassword) { 35 | throw new Error ("") 36 | } 37 | return user; 38 | } 39 | catch { 40 | return null 41 | } 42 | } 43 | }) 44 | 45 | ], 46 | callbacks: { 47 | async signIn({ account, profile }) { 48 | if (account?.provider === "github") { 49 | await connectToDatabase(); 50 | const existingUser = await User.findOne({ email: profile?.email }); 51 | if (!existingUser) { 52 | await User.create({ 53 | name: profile?.name, 54 | email: profile?.email, 55 | }) 56 | } 57 | } 58 | return true; 59 | }, 60 | 61 | async jwt({ token, user }) { 62 | if (user) { 63 | token.id = user.id; 64 | token.email = user.email; 65 | } 66 | return token; 67 | }, 68 | async session({ session, token }) { 69 | if (token) { 70 | session.user = { 71 | email: token.email, 72 | name: token.name, 73 | image: token.picture, 74 | }; 75 | }; 76 | return session; 77 | } 78 | 79 | }, 80 | pages: { 81 | signIn: "/sign-in", 82 | }, 83 | secret: process.env.NEXTAUTH_SECRET 84 | 85 | 86 | 87 | }); 88 | export { handler as GET, handler as POST }; 89 | -------------------------------------------------------------------------------- /src/app/api/auth/signup/route.ts: -------------------------------------------------------------------------------- 1 | import bcrypt from 'bcryptjs'; 2 | import { NextResponse } from 'next/server' 3 | import User from '@/models/user' 4 | import connectToDatabase from '@/lib/mongodb'; 5 | 6 | 7 | export async function POST(request: Request) { 8 | const { name, email, password, confirmPassword } = await request.json(); 9 | 10 | const isValidEmail = (email: string) => { 11 | const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; 12 | return emailRegex.test(email); 13 | } 14 | if (!name || !email || !password || !confirmPassword) { 15 | return NextResponse.json({message: " All fields are required"}, {status:400}) 16 | } 17 | 18 | if (!isValidEmail(email)) { 19 | return NextResponse.json({ message: "Invalid email format" }, { status: 400 }); 20 | } 21 | if (confirmPassword !== password) { 22 | return NextResponse.json({message:"Password do not match"}, { status:400}) 23 | } 24 | if (password.length < 6) { 25 | return NextResponse.json({ message: "Password must be at least 6 character long" }, { status: 400 }); 26 | } 27 | 28 | try { 29 | await connectToDatabase(); 30 | const existingUser = await User.findOne({ email }); 31 | if (existingUser) { 32 | return NextResponse.json({ message: "User already exist" }, { status: 400 }); 33 | } 34 | 35 | const hashedPassword = await bcrypt.hash(password, 10); 36 | 37 | const newUser = new User({ 38 | email, 39 | name, 40 | password: hashedPassword, 41 | }); 42 | await newUser.save(); 43 | return NextResponse.json({ message: "User created" }, { status: 201 }); 44 | 45 | } catch (error) { 46 | console.log(error) 47 | return NextResponse.json({ message: "Something went wrong" }, { status: 500 }); 48 | } 49 | } -------------------------------------------------------------------------------- /src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/francis-njenga/Full-Stack-Next-js-Authentication-System-with-NextAuth-and-MongoDB/3c234422edf56e648155569ae266b34acee4b27e/src/app/favicon.ico -------------------------------------------------------------------------------- /src/app/fonts/GeistMonoVF.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/francis-njenga/Full-Stack-Next-js-Authentication-System-with-NextAuth-and-MongoDB/3c234422edf56e648155569ae266b34acee4b27e/src/app/fonts/GeistMonoVF.woff -------------------------------------------------------------------------------- /src/app/fonts/GeistVF.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/francis-njenga/Full-Stack-Next-js-Authentication-System-with-NextAuth-and-MongoDB/3c234422edf56e648155569ae266b34acee4b27e/src/app/fonts/GeistVF.woff -------------------------------------------------------------------------------- /src/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | html, body, :root{ 6 | height:100%; 7 | } 8 | 9 | 10 | body { 11 | font-family: Arial, Helvetica, sans-serif; 12 | } 13 | 14 | @layer utilities { 15 | .text-balance { 16 | text-wrap: balance; 17 | } 18 | } 19 | 20 | @layer base { 21 | :root { 22 | --background: 0 0% 100%; 23 | --foreground: 0 0% 3.9%; 24 | --card: 0 0% 100%; 25 | --card-foreground: 0 0% 3.9%; 26 | --popover: 0 0% 100%; 27 | --popover-foreground: 0 0% 3.9%; 28 | --primary: 0 0% 9%; 29 | --primary-foreground: 0 0% 98%; 30 | --secondary: 0 0% 96.1%; 31 | --secondary-foreground: 0 0% 9%; 32 | --muted: 0 0% 96.1%; 33 | --muted-foreground: 0 0% 45.1%; 34 | --accent: 0 0% 96.1%; 35 | --accent-foreground: 0 0% 9%; 36 | --destructive: 0 84.2% 60.2%; 37 | --destructive-foreground: 0 0% 98%; 38 | --border: 0 0% 89.8%; 39 | --input: 0 0% 89.8%; 40 | --ring: 0 0% 3.9%; 41 | --chart-1: 12 76% 61%; 42 | --chart-2: 173 58% 39%; 43 | --chart-3: 197 37% 24%; 44 | --chart-4: 43 74% 66%; 45 | --chart-5: 27 87% 67%; 46 | --radius: 0.5rem; 47 | } 48 | .dark { 49 | --background: 0 0% 3.9%; 50 | --foreground: 0 0% 98%; 51 | --card: 0 0% 3.9%; 52 | --card-foreground: 0 0% 98%; 53 | --popover: 0 0% 3.9%; 54 | --popover-foreground: 0 0% 98%; 55 | --primary: 0 0% 98%; 56 | --primary-foreground: 0 0% 9%; 57 | --secondary: 0 0% 14.9%; 58 | --secondary-foreground: 0 0% 98%; 59 | --muted: 0 0% 14.9%; 60 | --muted-foreground: 0 0% 63.9%; 61 | --accent: 0 0% 14.9%; 62 | --accent-foreground: 0 0% 98%; 63 | --destructive: 0 62.8% 30.6%; 64 | --destructive-foreground: 0 0% 98%; 65 | --border: 0 0% 14.9%; 66 | --input: 0 0% 14.9%; 67 | --ring: 0 0% 83.1%; 68 | --chart-1: 220 70% 50%; 69 | --chart-2: 160 60% 45%; 70 | --chart-3: 30 80% 55%; 71 | --chart-4: 280 65% 60%; 72 | --chart-5: 340 75% 55%; 73 | } 74 | } 75 | 76 | @layer base { 77 | * { 78 | @apply border-border; 79 | } 80 | body { 81 | @apply bg-background text-foreground; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import localFont from "next/font/local"; 3 | import "./globals.css"; 4 | import { Toaster } from "@/components/ui/sonner"; 5 | 6 | const geistSans = localFont({ 7 | src: "./fonts/GeistVF.woff", 8 | variable: "--font-geist-sans", 9 | weight: "100 900", 10 | }); 11 | const geistMono = localFont({ 12 | src: "./fonts/GeistMonoVF.woff", 13 | variable: "--font-geist-mono", 14 | weight: "100 900", 15 | }); 16 | 17 | export const metadata: Metadata = { 18 | title: "Create Next App", 19 | description: "Generated by create next app", 20 | }; 21 | 22 | export default function RootLayout({ 23 | children, 24 | }: Readonly<{ 25 | children: React.ReactNode; 26 | }>) { 27 | return ( 28 | 29 | 32 | 33 | {children} 34 | 35 | 36 | ); 37 | } 38 | -------------------------------------------------------------------------------- /src/app/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import {SessionProvider} from "next-auth/react" 3 | import UserButton from "@/components/user-button"; 4 | 5 | const Home = () => { 6 | return ( 7 |
8 | 9 | 10 | 11 |
12 | ); 13 | }; 14 | 15 | export default Home; 16 | -------------------------------------------------------------------------------- /src/app/sign-in/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | //shadcn ui 4 | 5 | import { Button } from "@/components/ui/button"; 6 | import { 7 | Card, 8 | CardHeader, 9 | CardDescription, 10 | CardContent, 11 | CardTitle, 12 | } from "@/components/ui/card"; 13 | import { Input } from "@/components/ui/input"; 14 | import { Separator } from "@/components/ui/separator"; 15 | 16 | import Link from "next/link"; 17 | 18 | //react icons 19 | import { FaGithub } from "react-icons/fa"; 20 | import { FcGoogle } from "react-icons/fc"; 21 | import { useState } from "react"; 22 | import { signIn } from "next-auth/react"; 23 | import { useRouter } from "next/navigation"; 24 | import { toast } from "sonner"; 25 | import { TriangleAlert } from "lucide-react"; 26 | 27 | const SignIn = () => { 28 | const [email, setEmail] = useState(""); 29 | const [password, setPassword] = useState(""); 30 | const [pending, setPending] = useState(false); 31 | const router = useRouter(); 32 | const [error, setError] = useState(""); 33 | 34 | const handleSubmit = async (e: React.FormEvent) => { 35 | e.preventDefault(); 36 | setPending(true); 37 | const res = await signIn("credentials", { 38 | redirect: false, 39 | email, 40 | password, 41 | }); 42 | if (res?.ok) { 43 | router.push("/"); 44 | toast.success("login successful"); 45 | } else if (res?.status === 401) { 46 | setError("Invalid Credentials"); 47 | setPending(false); 48 | } else { 49 | setError("Something went wrong"); 50 | } 51 | }; 52 | 53 | const handleProvider = ( 54 | event: React.MouseEvent, 55 | value: "github" | "google" 56 | ) => { 57 | event.preventDefault(); 58 | signIn(value, { callbackUrl: "/" }); 59 | }; 60 | return ( 61 |
62 | 63 | 64 | Sign in 65 | 66 | Use email or service, to sign in 67 | 68 | 69 | {!!error && ( 70 |
71 | 72 |

{error}

73 |
74 | )} 75 | 76 |
77 | setEmail(e.target.value)} 83 | required 84 | /> 85 | setPassword(e.target.value)} 91 | required 92 | /> 93 | 94 | 97 |
98 | 99 | 100 |
101 | 110 | 119 |
120 |

121 | Create new account 122 | 126 | Sing up{" "} 127 | 128 |

129 |
130 |
131 |
132 | ); 133 | }; 134 | 135 | export default SignIn; 136 | -------------------------------------------------------------------------------- /src/app/sign-up/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | //shadcn ui 4 | 5 | import { Button } from "@/components/ui/button"; 6 | import { 7 | Card, 8 | CardHeader, 9 | CardDescription, 10 | CardContent, 11 | CardTitle, 12 | } from "@/components/ui/card"; 13 | import { Input } from "@/components/ui/input"; 14 | import { Separator } from "@/components/ui/separator"; 15 | 16 | import Link from "next/link"; 17 | 18 | //react icons 19 | import { FaGithub } from "react-icons/fa"; 20 | import { FcGoogle } from "react-icons/fc"; 21 | import { useState } from "react"; 22 | import { toast } from "sonner"; 23 | import { useRouter } from "next/navigation"; 24 | import { TriangleAlert } from "lucide-react"; 25 | import { signIn } from "next-auth/react"; 26 | 27 | const SignUp = () => { 28 | const [form, setForm] = useState({ 29 | name: "", 30 | email: "", 31 | password: "", 32 | confirmPassword: "", 33 | }); 34 | const [pending, setPending] = useState(false); 35 | const [error, setError] = useState(null); 36 | const router = useRouter(); 37 | 38 | const handleSubmit = async (e: React.FormEvent) => { 39 | e.preventDefault(); 40 | setPending(true); 41 | 42 | const res = await fetch("/api/auth/signup", { 43 | method: "POST", 44 | headers: { "Content-Type": "application/json" }, 45 | body: JSON.stringify(form), 46 | }); 47 | const data = await res.json(); 48 | 49 | if (res.ok) { 50 | setPending(false); 51 | toast.success(data.message); 52 | router.push("/sign-in"); 53 | } else if (res.status === 400) { 54 | setError(data.message); 55 | setPending(false); 56 | } else if (res.status === 500) { 57 | setError(data.message); 58 | setPending(false); 59 | } 60 | }; 61 | 62 | const handleProvider = ( 63 | event: React.MouseEvent, 64 | value: "github" | "google" 65 | ) => { 66 | event.preventDefault(); 67 | signIn(value, { callbackUrl: "/" }); 68 | }; 69 | return ( 70 |
71 | 72 | 73 | Sign up 74 | 75 | Use email or service, to create account 76 | 77 | 78 | {!!error && ( 79 |
80 | 81 |

{error}

82 |
83 | )} 84 | 85 |
86 | setForm({ ...form, name: e.target.value })} 92 | required 93 | /> 94 | setForm({ ...form, email: e.target.value })} 100 | required 101 | /> 102 | setForm({ ...form, password: e.target.value })} 108 | required 109 | /> 110 | 116 | setForm({ ...form, confirmPassword: e.target.value }) 117 | } 118 | required 119 | /> 120 | 123 |
124 | 125 | 126 |
127 | 136 | 145 |
146 |

147 | Already have an account? 148 | 152 | Sing in{" "} 153 | 154 |

155 |
156 |
157 |
158 | ); 159 | }; 160 | 161 | export default SignUp; 162 | -------------------------------------------------------------------------------- /src/components/ui/avatar.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as AvatarPrimitive from "@radix-ui/react-avatar" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | const Avatar = React.forwardRef< 9 | React.ElementRef, 10 | React.ComponentPropsWithoutRef 11 | >(({ className, ...props }, ref) => ( 12 | 20 | )) 21 | Avatar.displayName = AvatarPrimitive.Root.displayName 22 | 23 | const AvatarImage = React.forwardRef< 24 | React.ElementRef, 25 | React.ComponentPropsWithoutRef 26 | >(({ className, ...props }, ref) => ( 27 | 32 | )) 33 | AvatarImage.displayName = AvatarPrimitive.Image.displayName 34 | 35 | const AvatarFallback = React.forwardRef< 36 | React.ElementRef, 37 | React.ComponentPropsWithoutRef 38 | >(({ className, ...props }, ref) => ( 39 | 47 | )) 48 | AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName 49 | 50 | export { Avatar, AvatarImage, AvatarFallback } 51 | -------------------------------------------------------------------------------- /src/components/ui/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 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50", 9 | { 10 | variants: { 11 | variant: { 12 | default: "bg-primary text-primary-foreground hover:bg-primary/90", 13 | destructive: 14 | "bg-destructive text-destructive-foreground hover:bg-destructive/90", 15 | outline: 16 | "border border-input bg-background hover:bg-accent hover:text-accent-foreground", 17 | secondary: 18 | "bg-secondary text-secondary-foreground hover:bg-secondary/80", 19 | ghost: "hover:bg-accent hover:text-accent-foreground", 20 | link: "text-primary underline-offset-4 hover:underline", 21 | }, 22 | size: { 23 | default: "h-10 px-4 py-2", 24 | sm: "h-9 rounded-md px-3", 25 | lg: "h-11 rounded-md px-8", 26 | icon: "h-10 w-10", 27 | }, 28 | }, 29 | defaultVariants: { 30 | variant: "default", 31 | size: "default", 32 | }, 33 | } 34 | ) 35 | 36 | export interface ButtonProps 37 | extends React.ButtonHTMLAttributes, 38 | VariantProps { 39 | asChild?: boolean 40 | } 41 | 42 | const Button = React.forwardRef( 43 | ({ className, variant, size, asChild = false, ...props }, ref) => { 44 | const Comp = asChild ? Slot : "button" 45 | return ( 46 | 51 | ) 52 | } 53 | ) 54 | Button.displayName = "Button" 55 | 56 | export { Button, buttonVariants } 57 | -------------------------------------------------------------------------------- /src/components/ui/card.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | const Card = React.forwardRef< 6 | HTMLDivElement, 7 | React.HTMLAttributes 8 | >(({ className, ...props }, ref) => ( 9 |
17 | )) 18 | Card.displayName = "Card" 19 | 20 | const CardHeader = React.forwardRef< 21 | HTMLDivElement, 22 | React.HTMLAttributes 23 | >(({ className, ...props }, ref) => ( 24 |
29 | )) 30 | CardHeader.displayName = "CardHeader" 31 | 32 | const CardTitle = React.forwardRef< 33 | HTMLParagraphElement, 34 | React.HTMLAttributes 35 | >(({ className, ...props }, ref) => ( 36 |

44 | )) 45 | CardTitle.displayName = "CardTitle" 46 | 47 | const CardDescription = React.forwardRef< 48 | HTMLParagraphElement, 49 | React.HTMLAttributes 50 | >(({ className, ...props }, ref) => ( 51 |

56 | )) 57 | CardDescription.displayName = "CardDescription" 58 | 59 | const CardContent = React.forwardRef< 60 | HTMLDivElement, 61 | React.HTMLAttributes 62 | >(({ className, ...props }, ref) => ( 63 |

64 | )) 65 | CardContent.displayName = "CardContent" 66 | 67 | const CardFooter = React.forwardRef< 68 | HTMLDivElement, 69 | React.HTMLAttributes 70 | >(({ className, ...props }, ref) => ( 71 |
76 | )) 77 | CardFooter.displayName = "CardFooter" 78 | 79 | export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } 80 | -------------------------------------------------------------------------------- /src/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 | -------------------------------------------------------------------------------- /src/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 | -------------------------------------------------------------------------------- /src/components/ui/separator.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as SeparatorPrimitive from "@radix-ui/react-separator" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | const Separator = React.forwardRef< 9 | React.ElementRef, 10 | React.ComponentPropsWithoutRef 11 | >( 12 | ( 13 | { className, orientation = "horizontal", decorative = true, ...props }, 14 | ref 15 | ) => ( 16 | 27 | ) 28 | ) 29 | Separator.displayName = SeparatorPrimitive.Root.displayName 30 | 31 | export { Separator } 32 | -------------------------------------------------------------------------------- /src/components/ui/sonner.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { useTheme } from "next-themes" 4 | import { Toaster as Sonner } from "sonner" 5 | 6 | type ToasterProps = React.ComponentProps 7 | 8 | const Toaster = ({ ...props }: ToasterProps) => { 9 | const { theme = "system" } = useTheme() 10 | 11 | return ( 12 | 28 | ) 29 | } 30 | 31 | export { Toaster } 32 | -------------------------------------------------------------------------------- /src/components/user-button.tsx: -------------------------------------------------------------------------------- 1 | import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; 2 | import { 3 | DropdownMenu, 4 | DropdownMenuContent, 5 | DropdownMenuItem, 6 | DropdownMenuTrigger, 7 | } from "@/components/ui/dropdown-menu"; 8 | import { useSession, signOut } from "next-auth/react"; 9 | import { useRouter } from "next/navigation"; 10 | import { Loader } from "lucide-react"; 11 | import { Button } from "@/components/ui/button"; 12 | import Link from "next/link"; 13 | 14 | const UserButton = () => { 15 | const router = useRouter(); 16 | const { data: session, status } = useSession(); 17 | 18 | if (status === "loading") { 19 | return ; 20 | } 21 | 22 | const avatarFallback = session?.user?.name?.charAt(0).toUpperCase(); 23 | const handleSignOut = async () => { 24 | await signOut({ 25 | redirect: false, 26 | }); 27 | router.push("/") 28 | } 29 | return ( 30 | 62 | ); 63 | }; 64 | 65 | export default UserButton; 66 | -------------------------------------------------------------------------------- /src/lib/mongodb.ts: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose' 2 | 3 | const MONGODB_URI = process.env.MONGO; 4 | 5 | if (!MONGODB_URI) { 6 | throw new Error (" please define mongo environment variable") 7 | } 8 | 9 | async function connectToDatabase() { 10 | if (mongoose.connection.readyState === 1) { 11 | return mongoose; 12 | } 13 | const opts = { 14 | bufferCommands: false, 15 | } 16 | await mongoose.connect(MONGODB_URI!, opts); 17 | return mongoose; 18 | } 19 | 20 | export default connectToDatabase; -------------------------------------------------------------------------------- /src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { clsx, type ClassValue } from "clsx" 2 | import { twMerge } from "tailwind-merge" 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)) 6 | } 7 | -------------------------------------------------------------------------------- /src/models/user.ts: -------------------------------------------------------------------------------- 1 | import mongoose, { Document, Model, Schema } from "mongoose"; 2 | 3 | interface IUser extends Document { 4 | name: string; 5 | email: string; 6 | password?: string; 7 | id: string; 8 | } 9 | 10 | const UserSchema: Schema = new mongoose.Schema({ 11 | name: { 12 | type: String, 13 | required: true, 14 | }, 15 | email: { 16 | type: String, 17 | required: true, 18 | unique: true, 19 | }, 20 | password: { 21 | type: String, 22 | required: false, 23 | }, 24 | }); 25 | 26 | const User: Model = 27 | mongoose.models.User || mongoose.model("User", UserSchema); 28 | 29 | export default User; 30 | -------------------------------------------------------------------------------- /tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "tailwindcss"; 2 | 3 | const config: Config = { 4 | darkMode: ["class"], 5 | content: [ 6 | "./src/pages/**/*.{js,ts,jsx,tsx,mdx}", 7 | "./src/components/**/*.{js,ts,jsx,tsx,mdx}", 8 | "./src/app/**/*.{js,ts,jsx,tsx,mdx}", 9 | ], 10 | theme: { 11 | extend: { 12 | colors: { 13 | background: 'hsl(var(--background))', 14 | foreground: 'hsl(var(--foreground))', 15 | card: { 16 | DEFAULT: 'hsl(var(--card))', 17 | foreground: 'hsl(var(--card-foreground))' 18 | }, 19 | popover: { 20 | DEFAULT: 'hsl(var(--popover))', 21 | foreground: 'hsl(var(--popover-foreground))' 22 | }, 23 | primary: { 24 | DEFAULT: 'hsl(var(--primary))', 25 | foreground: 'hsl(var(--primary-foreground))' 26 | }, 27 | secondary: { 28 | DEFAULT: 'hsl(var(--secondary))', 29 | foreground: 'hsl(var(--secondary-foreground))' 30 | }, 31 | muted: { 32 | DEFAULT: 'hsl(var(--muted))', 33 | foreground: 'hsl(var(--muted-foreground))' 34 | }, 35 | accent: { 36 | DEFAULT: 'hsl(var(--accent))', 37 | foreground: 'hsl(var(--accent-foreground))' 38 | }, 39 | destructive: { 40 | DEFAULT: 'hsl(var(--destructive))', 41 | foreground: 'hsl(var(--destructive-foreground))' 42 | }, 43 | border: 'hsl(var(--border))', 44 | input: 'hsl(var(--input))', 45 | ring: 'hsl(var(--ring))', 46 | chart: { 47 | '1': 'hsl(var(--chart-1))', 48 | '2': 'hsl(var(--chart-2))', 49 | '3': 'hsl(var(--chart-3))', 50 | '4': 'hsl(var(--chart-4))', 51 | '5': 'hsl(var(--chart-5))' 52 | } 53 | }, 54 | borderRadius: { 55 | lg: 'var(--radius)', 56 | md: 'calc(var(--radius) - 2px)', 57 | sm: 'calc(var(--radius) - 4px)' 58 | } 59 | } 60 | }, 61 | plugins: [require("tailwindcss-animate")], 62 | }; 63 | export default config; 64 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["dom", "dom.iterable", "esnext"], 4 | "allowJs": true, 5 | "skipLibCheck": true, 6 | "strict": true, 7 | "noEmit": true, 8 | "esModuleInterop": true, 9 | "module": "esnext", 10 | "moduleResolution": "bundler", 11 | "resolveJsonModule": true, 12 | "isolatedModules": true, 13 | "jsx": "preserve", 14 | "incremental": true, 15 | "plugins": [ 16 | { 17 | "name": "next" 18 | } 19 | ], 20 | "paths": { 21 | "@/*": ["./src/*"] 22 | } 23 | }, 24 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 25 | "exclude": ["node_modules"] 26 | } 27 | --------------------------------------------------------------------------------