├── .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 |
74 | )}
75 |
76 |
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 |
83 | )}
84 |
85 |
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 |
--------------------------------------------------------------------------------