diff --git a/.env.example b/.env.example
new file mode 100644
index 0000000..c87597a
--- /dev/null
+++ b/.env.example
@@ -0,0 +1,3 @@
+NEXT_PUBLIC_SUPABASE_ANON_KEY = ""
+NEXT_PUBLIC_SUPABASE_URL = ""
+DATABASE_URL = "postgresql://johndoe:randompassword@localhost:5432/mydb?schema=public"
\ No newline at end of file
diff --git a/app/api/auth/register/route.ts b/app/api/auth/register/route.ts
new file mode 100644
index 0000000..b440cdb
--- /dev/null
+++ b/app/api/auth/register/route.ts
@@ -0,0 +1,80 @@
+import prisma from "@/lib/database/prismaClient";
+import { supabase } from "@/lib/supabaseClient";
+import { hashPassword } from "@/utils/hashing";
+import { signupSchema } from "@/validations/validation";
+import { NextRequest, NextResponse } from "next/server";
+
+export async function POST(request: NextRequest) {
+ const body = await request.json();
+ const { email, password, fullName, leetcodeUsername, gender } = body;
+ try {
+ const { success, error } = signupSchema.safeParse({ email, password, fullName, leetcodeUsername, gender });
+
+ if (!success) {
+ return NextResponse.json({
+ success: false,
+ message: error.flatten().fieldErrors,
+ }, { status: 400 });
+ }
+
+
+ const userExists = await prisma.user.findFirst({
+ where: {
+ email: email,
+ },
+ });
+
+ if (userExists) {
+ return NextResponse.json({
+ success: false,
+ message: "User with this email already exists.",
+ }, { status: 404 });
+ }
+
+ const { data: signUpData, error: signUpError } = await supabase.auth.signUp({
+ email,
+ password: password,
+ });
+
+ if (signUpError) {
+ return NextResponse.json({
+ success: false,
+ message: signUpError.message,
+ }, { status: 401 });
+ }
+
+ const supabaseId = signUpData.user?.id;
+
+ if (!supabaseId) {
+ return NextResponse.json({
+ success: false,
+ message: "Failed to retrieve Supabase user ID.",
+ }, { status: 400 });
+ }
+
+ const hashedPassword = await hashPassword(password);
+ const user = await prisma.user.create({
+ data: {
+ email,
+ password: hashedPassword,
+ fullName,
+ leetcodeUsername,
+ gender,
+ supabaseId: supabaseId,
+ isVerified: false,
+ }
+ });
+
+ return NextResponse.json({
+ success: true,
+ message: "User created successfully",
+ user,
+ }, { status: 201 });
+
+ } catch (error: any) {
+ return NextResponse.json({
+ success: false,
+ message: error.message
+ }, { status: 500 });
+ }
+}
\ No newline at end of file
diff --git a/app/api/auth/user/route.ts b/app/api/auth/user/route.ts
new file mode 100644
index 0000000..f4c1bc3
--- /dev/null
+++ b/app/api/auth/user/route.ts
@@ -0,0 +1,37 @@
+import prisma from '@/lib/database/prismaClient';
+import { NextResponse } from 'next/server';
+
+export async function POST(request: Request) {
+ try {
+
+ const { supabaseId } = await request.json();
+ console.log("supabaseId", supabaseId);
+
+ const userData = await prisma.user.findUnique({
+ where: { supabaseId },
+ select: {
+ email: true,
+ fullName: true,
+ leetcodeUsername: true,
+ id: true,
+ isVerified: true,
+ gender: true,
+ createdAt: true,
+ supabaseId: true,
+ }
+ });
+
+ if (!userData) {
+ return NextResponse.json({ message: 'User not found' }, { status: 404 });
+ }
+
+ if (!userData.isVerified) {
+ return NextResponse.json({ message: 'User is not verified but registered', user: userData });
+ }
+
+ return NextResponse.json({ user: userData });
+ } catch (err) {
+ console.error('Error fetching user data:', err);
+ return NextResponse.json({ message: 'Error fetching user data' }, { status: 500 });
+ }
+}
diff --git a/app/signup/page.tsx b/app/auth/register/page.tsx
similarity index 67%
rename from app/signup/page.tsx
rename to app/auth/register/page.tsx
index 22d0a70..4b38104 100644
--- a/app/signup/page.tsx
+++ b/app/auth/register/page.tsx
@@ -1,4 +1,4 @@
-import SignupForm from "@/components/SignupForm";
+import SignupForm from "@/components/AuthComponent/SignupForm";
export default function SignupPage() {
return (
diff --git a/app/login/page.tsx b/app/auth/signin/page.tsx
similarity index 53%
rename from app/login/page.tsx
rename to app/auth/signin/page.tsx
index 14af8d5..eb020ee 100644
--- a/app/login/page.tsx
+++ b/app/auth/signin/page.tsx
@@ -1,9 +1,9 @@
-import LoginForm from "@/components/LoginForm";
+import SigninForm from "@/components/AuthComponent/SigninForm";
export default function SignupPage() {
return (
-
+
);
}
diff --git a/app/auth/verify/page.tsx b/app/auth/verify/page.tsx
new file mode 100644
index 0000000..418f9d9
--- /dev/null
+++ b/app/auth/verify/page.tsx
@@ -0,0 +1,58 @@
+'use client'
+
+import { useState } from 'react'
+import { Button } from '@/components/ui/button'
+import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card'
+import { Alert, AlertDescription } from '@/components/ui/alert'
+import { Mail } from 'lucide-react'
+
+export default function VerifyPage() {
+ const [resendStatus, setResendStatus] = useState<'idle' | 'sending' | 'sent' | 'error'>('idle')
+
+ const handleResendEmail = async () => {
+ setResendStatus('sending')
+ await new Promise(resolve => setTimeout(resolve, 2000))
+ setResendStatus('sent')
+ }
+
+ return (
+
+
+
+
+
+
+ Verify Your Email
+
+ We've sent a verification link to your registered email address.
+
+
+
+
+ Please check your inbox and click on the verification link to complete your registration.
+ If you don't see the email, please check your spam folder.
+
+
+ {/*
+
+ {resendStatus === 'sending' ? 'Resending...' : 'Resend Verification Email'}
+
+ {resendStatus === 'sent' && (
+
+ Verification email has been resent successfully.
+
+ )}
+ {resendStatus === 'error' && (
+
+ Failed to resend verification email. Please try again later.
+
+ )}
+ */}
+
+
+ )
+}
+
diff --git a/app/dashboard/page.tsx b/app/dashboard/page.tsx
index 2964eed..5a5cbfa 100644
--- a/app/dashboard/page.tsx
+++ b/app/dashboard/page.tsx
@@ -1,78 +1,97 @@
-"use client"
-import { useEffect, useState } from 'react';
-import { supabase } from '@/lib/supabaseClient';
-import { useRouter } from 'next/navigation';
-import Navbar from '@/components/header';
-import StatsCard from '@/components/Stats';
-import { fetchLeetCodeStats } from '@/lib/utils';
+"use client";
+
+import { useEffect } from "react";
+import { useRouter } from "next/navigation";
+import { useAuthStore } from "@/store/AuthStore/useAuthStore";
+import Navbar from "@/components/header";
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+import { Skeleton } from "@/components/ui/skeleton";
export default function Dashboard() {
- const [userData, setUserData] = useState(null);
- const [loading, setLoading] = useState(true);
- const [error, setError] = useState(null);
- const router = useRouter();
+ const router = useRouter();
+ const { authUser, fetchAuthUser, authUserLoading } = useAuthStore();
- useEffect(() => {
- const fetchData = async () => {
- try {
- const { data, error } = await supabase.auth.getSession();
+ useEffect(() => {
+ fetchAuthUser();
+ }, [fetchAuthUser]);
- if (error) throw new Error("Error fetching session.");
+ useEffect(() => {
+ if (!authUserLoading && !authUser) {
+ router.push("/auth/signin");
+ }
+ }, [authUserLoading, authUser, router]);
- const session = data.session;
- if (!session) {
- router.push('/login');
- return;
- }
- // Fetch user-specific data in a single call
- const { data: userInfo, error: userInfoError } = await supabase
- .from('user_info')
- .select('*')
- .eq('user_id', session.user.id)
- .single();
+ if (authUserLoading) {
+ return ;
+ }
- if (userInfoError) throw userInfoError;
+ if (!authUser) {
+ return null;
+ }
- setUserData(userInfo);
+ return (
+
+
+
+ Welcome, {authUser.fullName}
- } catch (err: any) {
- console.error(err);
- setError(err.message || 'An error occurred.');
- router.push('/login');
- } finally {
- setLoading(false);
- }
- };
+
+
+ Your Profile
+
+
+
+ LeetCode Username: {" "}
+ {authUser.leetcodeUsername}
+
+
+ Gender: {authUser.gender}
+
+
+
- fetchData();
- }, [router]);
+
+
+ LeetCode Stats
+
+
+ {/* LeetCode stats component will go here */}
+
+ LeetCode stats are coming soon!
+
+
+
+
+
+ );
+}
- if (loading) return Loading...
;
+function DashboardSkeleton() {
+ return (
+
+ {/*
*/}
+
+
- if (error) {
- return (
-
- );
- }
+
+
+
+
+
+
+
+
+
- return (
-
-
-
-
Welcome, {userData.name}
-
-
LeetCode Username: {userData.leetcode_username}
-
Gender: {userData.gender}
-
-
-
-
LeetCode Stats
-
-
-
-
- );
+
+
+
+
+
+
+
+
+
+
+ );
}
diff --git a/components/AuthComponent/AuthBottom.tsx b/components/AuthComponent/AuthBottom.tsx
new file mode 100644
index 0000000..b4117ea
--- /dev/null
+++ b/components/AuthComponent/AuthBottom.tsx
@@ -0,0 +1,16 @@
+import Link from "next/link";
+import React from "react";
+
+interface AuthBottomProps {
+ href: string;
+ title: string;
+ toTitle: string;
+}
+
+export default function AuthBottom({ href, title, toTitle }: AuthBottomProps) {
+ return (
+
+ {title} {toTitle}
+
+ );
+}
diff --git a/components/AuthComponent/SigninForm.tsx b/components/AuthComponent/SigninForm.tsx
new file mode 100644
index 0000000..325897b
--- /dev/null
+++ b/components/AuthComponent/SigninForm.tsx
@@ -0,0 +1,107 @@
+"use client";
+
+import React, { useState } from "react";
+import { useRouter } from "next/navigation";
+import Link from "next/link";
+import { supabase } from "@/lib/supabaseClient";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+import { Alert, AlertDescription } from "@/components/ui/alert";
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardFooter,
+ CardHeader,
+ CardTitle,
+} from "@/components/ui/card";
+import { useAuthStore } from "@/store/AuthStore/useAuthStore";
+import LoadingButton from "../Buttons/LoadingButton";
+import AuthBottom from "./AuthBottom";
+
+interface FormData {
+ email: string;
+ password: string;
+}
+
+export default function SigninForm() {
+ const { isSigningIn, signin, signinError } = useAuthStore();
+ const router = useRouter();
+ const [formData, setFormData] = useState({
+ email: "",
+ password: "",
+ });
+
+ const handleChange = (e: React.ChangeEvent) => {
+ const { name, value } = e.target;
+ setFormData((prev) => ({ ...prev, [name]: value }));
+ };
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault();
+ signin(formData, router);
+ };
+
+ return (
+
+
+
+ Sign in
+
+ Enter your credentials to access your account
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/components/AuthComponent/SignupForm.tsx b/components/AuthComponent/SignupForm.tsx
new file mode 100644
index 0000000..e3e8963
--- /dev/null
+++ b/components/AuthComponent/SignupForm.tsx
@@ -0,0 +1,166 @@
+"use client";
+
+import React, { useState } from "react";
+import { useRouter } from "next/navigation";
+import Link from "next/link";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+import {
+ Select,
+ SelectContent,
+ SelectGroup,
+ SelectItem,
+ SelectLabel,
+ SelectTrigger,
+ SelectValue,
+} from "@/components/ui/select";
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardFooter,
+ CardHeader,
+ CardTitle,
+} from "@/components/ui/card";
+import { Alert, AlertDescription } from "@/components/ui/alert";
+import { UserType } from "@/types/typeInterfaces";
+import { useAuthStore } from "@/store/AuthStore/useAuthStore";
+import AuthBottom from "./AuthBottom";
+import LoadingButton from "../Buttons/LoadingButton";
+
+export default function SignupForm() {
+ const { isSigningUp, signup, signupError } = useAuthStore();
+ const router = useRouter();
+ const [formData, setFormData] = useState({
+ fullName: "",
+ email: "",
+ password: "",
+ leetcodeUsername: "",
+ gender: "",
+ });
+
+ const handleChange = (e: React.ChangeEvent) => {
+ setFormData({ ...formData, [e.target.name]: e.target.value });
+ };
+
+ const handleSelectChange = (value: string) => {
+ setFormData({ ...formData, gender: value });
+ };
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault();
+ signup(formData, router);
+ };
+
+ return (
+
+
+
+
+ Register your Account
+
+
+ Sign up to start your coding journey
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/components/LandingComponents/LandingEmerlandButton.tsx b/components/Buttons/LandingEmerlandButton.tsx
similarity index 100%
rename from components/LandingComponents/LandingEmerlandButton.tsx
rename to components/Buttons/LandingEmerlandButton.tsx
diff --git a/components/Buttons/LoadingButton.tsx b/components/Buttons/LoadingButton.tsx
new file mode 100644
index 0000000..988398d
--- /dev/null
+++ b/components/Buttons/LoadingButton.tsx
@@ -0,0 +1,29 @@
+import React, { ButtonHTMLAttributes } from "react";
+import { Button } from "../ui/button";
+import { Loader2 } from "lucide-react";
+
+interface LoadingButtonProps {
+ loading: boolean;
+ loadingTitle: string;
+ title: string;
+ type: React.ButtonHTMLAttributes["type"];
+}
+
+export default function LoadingButton({
+ loading,
+ loadingTitle,
+ title,
+ type,
+}: LoadingButtonProps) {
+ return (
+
+ {loading ? (
+
+ {loadingTitle}
+
+ ) : (
+ title
+ )}
+
+ );
+}
diff --git a/components/LandingComponents/PriceCard.tsx b/components/LandingComponents/PriceCard.tsx
index 1c468f6..8411650 100644
--- a/components/LandingComponents/PriceCard.tsx
+++ b/components/LandingComponents/PriceCard.tsx
@@ -3,7 +3,7 @@
import React from "react";
import { motion } from "framer-motion";
import { Card, CardContent, CardFooter, CardHeader } from "../ui/card";
-import LandingEmerlandButton from "./LandingEmerlandButton";
+import LandingEmerlandButton from "../Buttons/LandingEmerlandButton";
export interface PricingCardProps {
title: string;
diff --git a/components/LoginForm.tsx b/components/LoginForm.tsx
deleted file mode 100644
index c95b83f..0000000
--- a/components/LoginForm.tsx
+++ /dev/null
@@ -1,110 +0,0 @@
-'use client';
-
-import React, { useState } from 'react';
-import { Button } from '@/components/ui/button';
-import { Input } from '@/components/ui/input';
-import { Label } from '@/components/ui/label';
-import { useRouter } from 'next/navigation';
-import { supabase } from '@/lib/supabaseClient';
-import Link from 'next/link';
-
-const LoginForm: React.FC = () => {
- const router = useRouter();
- const [formData, setFormData] = useState<{ email: string; password: string }>({
- email: '',
- password: '',
- });
-
- const [loading, setLoading] = useState(false);
- const [error, setError] = useState(null);
-
- const handleChange = (e: React.ChangeEvent) => {
- const { name, value } = e.target;
- setFormData((prev) => ({ ...prev, [name]: value }));
- };
-
- const handleSubmit = async (e: React.FormEvent) => {
- e.preventDefault();
- setLoading(true);
- setError(null);
-
- const { email, password } = formData;
-
- try {
- // Attempt user login
- const { data, error: loginError } = await supabase.auth.signInWithPassword({ email, password });
-
- if (loginError) {
- throw new Error(loginError.message);
- }
-
- // Redirect to dashboard if login succeeds
- if (data.session) {
- router.push('/dashboard');
- } else {
- throw new Error('Unable to retrieve session after login.');
- }
- } catch (err: any) {
- console.error('Login Error:', err);
- setError(err.message || 'Something went wrong.');
- } finally {
- setLoading(false);
- }
- };
-
- return (
-
-
-
- );
-};
-
-export default LoginForm;
diff --git a/components/SignupForm.tsx b/components/SignupForm.tsx
deleted file mode 100644
index 3a16265..0000000
--- a/components/SignupForm.tsx
+++ /dev/null
@@ -1,195 +0,0 @@
-'use client';
-
-import React, { useState } from 'react';
-import { Button } from '@/components/ui/button';
-import { Input } from '@/components/ui/input';
-import { Label } from '@/components/ui/label';
-import { useRouter } from 'next/navigation';
-import {
- Select,
- SelectContent,
- SelectGroup,
- SelectItem,
- SelectLabel,
- SelectTrigger,
- SelectValue,
-} from '@/components/ui/select';
-import { supabase } from '@/lib/supabaseClient';
-import Link from 'next/link';
-
-const SignupForm: React.FC = () => {
- const router = useRouter();
- const [formData, setFormData] = useState<{
- name: string;
- email: string;
- password: string;
- leetcodeUsername: string;
- gender: string;
- }>({
- name: '',
- email: '',
- password: '',
- leetcodeUsername: '',
- gender: '',
- });
-
- const [loading, setLoading] = useState(false);
- const [error, setError] = useState(null);
-
- const handleChange = (e: React.ChangeEvent) => {
- setFormData({ ...formData, [e.target.name]: e.target.value });
- };
-
- const handleSelectChange = (value: string) => {
- setFormData({ ...formData, gender: value });
- };
-
- const handleSubmit = async (e: React.FormEvent) => {
- e.preventDefault();
- setLoading(true);
- setError(null);
-
- try {
- const { email, password, name, leetcodeUsername, gender } = formData;
-
- // Sign up the user
- const { data: signUpData, error: signUpError } = await supabase.auth.signUp({
- email,
- password,
- });
-
- if (signUpError) {
- throw new Error(signUpError.message);
- }
-
- const userId = signUpData?.user?.id; // Ensure correct path to user ID
-
- // Insert additional user information
- if (userId) {
- const { error: insertError } = await supabase.from('user_info').insert([
- {
- user_id: userId,
- name,
- leetcode_username: leetcodeUsername,
- gender,
- },
- ]);
-
- if (insertError) {
- throw new Error(insertError.message);
- }
- } else {
- throw new Error('User ID not found. Please try again.');
- }
-
- // Redirect to the dashboard
- router.push('/dashboard');
- } catch (err: any) {
- console.error('Error:', err);
- setError(err.message || 'Something went wrong. Please try again.');
- } finally {
- setLoading(false);
- }
- };
-
- return (
-
-
-
- );
-};
-
-export default SignupForm;
diff --git a/components/header.tsx b/components/header.tsx
index 6d4dfff..65bd67d 100644
--- a/components/header.tsx
+++ b/components/header.tsx
@@ -1,4 +1,5 @@
"use client"
+
import { useState } from "react";
import Link from "next/link";
import { Button } from "@/components/ui/button";
@@ -42,19 +43,19 @@ const Navbar = ({ userId }: { userId?: string }) => {
onClick={() =>
supabase.auth
.signOut()
- .then(() => router.push("/login"))
+ .then(() => router.push("/auth/signin"))
}
>
Sign Out
) : (
<>
-
- Log in
+
+ Sign In
-
- Sign up
+
+ Register
>
diff --git a/components/ui/alert.tsx b/components/ui/alert.tsx
new file mode 100644
index 0000000..41fa7e0
--- /dev/null
+++ b/components/ui/alert.tsx
@@ -0,0 +1,59 @@
+import * as React from "react"
+import { cva, type VariantProps } from "class-variance-authority"
+
+import { cn } from "@/lib/utils"
+
+const alertVariants = cva(
+ "relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground",
+ {
+ variants: {
+ variant: {
+ default: "bg-background text-foreground",
+ destructive:
+ "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ },
+ }
+)
+
+const Alert = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes & VariantProps
+>(({ className, variant, ...props }, ref) => (
+
+))
+Alert.displayName = "Alert"
+
+const AlertTitle = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+AlertTitle.displayName = "AlertTitle"
+
+const AlertDescription = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+AlertDescription.displayName = "AlertDescription"
+
+export { Alert, AlertTitle, AlertDescription }
diff --git a/components/ui/skeleton.tsx b/components/ui/skeleton.tsx
new file mode 100644
index 0000000..01b8b6d
--- /dev/null
+++ b/components/ui/skeleton.tsx
@@ -0,0 +1,15 @@
+import { cn } from "@/lib/utils"
+
+function Skeleton({
+ className,
+ ...props
+}: React.HTMLAttributes) {
+ return (
+
+ )
+}
+
+export { Skeleton }
diff --git a/lib/database/prismaClient.ts b/lib/database/prismaClient.ts
new file mode 100644
index 0000000..d33ddfb
--- /dev/null
+++ b/lib/database/prismaClient.ts
@@ -0,0 +1,5 @@
+import { PrismaClient } from '@prisma/client'
+
+const prisma = new PrismaClient();
+
+export default prisma;
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
index 087b4ab..bb24e74 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -8,6 +8,7 @@
"name": "leetcode-journal",
"version": "0.1.0",
"dependencies": {
+ "@prisma/client": "^6.2.0",
"@radix-ui/react-accordion": "^1.2.2",
"@radix-ui/react-dialog": "^1.1.4",
"@radix-ui/react-dropdown-menu": "^2.1.4",
@@ -16,17 +17,22 @@
"@radix-ui/react-separator": "^1.1.1",
"@radix-ui/react-slot": "^1.1.1",
"@radix-ui/react-tabs": "^1.1.2",
+ "@types/bcryptjs": "^2.4.6",
"axios": "^1.7.9",
+ "bcryptjs": "^2.4.3",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"embla-carousel-react": "^8.5.1",
"framer-motion": "^11.15.0",
"lucide-react": "^0.469.0",
"next": "15.1.2",
+ "prisma": "^6.2.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"tailwind-merge": "^2.6.0",
- "tailwindcss-animate": "^1.0.7"
+ "tailwindcss-animate": "^1.0.7",
+ "zod": "^3.24.1",
+ "zustand": "^5.0.3"
},
"devDependencies": {
"@eslint/eslintrc": "^3",
@@ -921,6 +927,63 @@
"node": ">=14"
}
},
+ "node_modules/@prisma/client": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.2.0.tgz",
+ "integrity": "sha512-tmEgej4OR+Wqk8MwZQcu58JzA1iFPmi/z7VPEmjTuTIQDLqHQZ6+MDRLL4wgNJXJkMHUKD9yMD5AkwYH0/0hKA==",
+ "hasInstallScript": true,
+ "engines": {
+ "node": ">=18.18"
+ },
+ "peerDependencies": {
+ "prisma": "*"
+ },
+ "peerDependenciesMeta": {
+ "prisma": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@prisma/debug": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.2.0.tgz",
+ "integrity": "sha512-Q96rqZVivmEtt29h1hhALceJTqggHDsr3RAWpeSJZOppQu6vcv5PyiY4XxyTf04gZw4Ue+kkqtaRcRms1zC8aQ=="
+ },
+ "node_modules/@prisma/engines": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.2.0.tgz",
+ "integrity": "sha512-Od7fH2gH+4n0E/XIhhAfO3OaKKNRzD0s1LY8umyvDQXlFmiDYF8kNJydcfWLvU3XNNV40wM2T0jOU+4ua1Zp3A==",
+ "hasInstallScript": true,
+ "dependencies": {
+ "@prisma/debug": "6.2.0",
+ "@prisma/engines-version": "6.2.0-14.4123509d24aa4dede1e864b46351bf2790323b69",
+ "@prisma/fetch-engine": "6.2.0",
+ "@prisma/get-platform": "6.2.0"
+ }
+ },
+ "node_modules/@prisma/engines-version": {
+ "version": "6.2.0-14.4123509d24aa4dede1e864b46351bf2790323b69",
+ "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-6.2.0-14.4123509d24aa4dede1e864b46351bf2790323b69.tgz",
+ "integrity": "sha512-7tw1qs/9GWSX6qbZs4He09TOTg1ff3gYsB3ubaVNN0Pp1zLm9NC5C5MZShtkz7TyQjx7blhpknB7HwEhlG+PrQ=="
+ },
+ "node_modules/@prisma/fetch-engine": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.2.0.tgz",
+ "integrity": "sha512-zz0HmZ2Npsthnh+1cj7aFPRWs57GS4CNlM9uXpVeQm2/YN0LMRNeuI5/zpqRhHrZUXdKde0jltJnvIM1Xz/mPQ==",
+ "dependencies": {
+ "@prisma/debug": "6.2.0",
+ "@prisma/engines-version": "6.2.0-14.4123509d24aa4dede1e864b46351bf2790323b69",
+ "@prisma/get-platform": "6.2.0"
+ }
+ },
+ "node_modules/@prisma/get-platform": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.2.0.tgz",
+ "integrity": "sha512-Nnk2fcjiRB9E0uRKCMl+EmBC1Vs6kXqaHa2E108pDrEXAgxj0Ns/YQSeZE0o4QJiK5m1PGmImA9/FFUAgCUTHA==",
+ "dependencies": {
+ "@prisma/debug": "6.2.0"
+ }
+ },
"node_modules/@radix-ui/number": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.0.tgz",
@@ -1834,6 +1897,11 @@
"tslib": "^2.8.0"
}
},
+ "node_modules/@types/bcryptjs": {
+ "version": "2.4.6",
+ "resolved": "https://registry.npmjs.org/@types/bcryptjs/-/bcryptjs-2.4.6.tgz",
+ "integrity": "sha512-9xlo6R2qDs5uixm0bcIqCeMCE6HiQsIyel9KQySStiyqNl2tnj2mP3DX1Nf56MD6KMenNNlBBsy3LJ7gUEQPXQ=="
+ },
"node_modules/@types/estree": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz",
@@ -2482,6 +2550,11 @@
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"license": "MIT"
},
+ "node_modules/bcryptjs": {
+ "version": "2.4.3",
+ "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz",
+ "integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ=="
+ },
"node_modules/binary-extensions": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
@@ -5598,6 +5671,24 @@
"node": ">= 0.8.0"
}
},
+ "node_modules/prisma": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/prisma/-/prisma-6.2.0.tgz",
+ "integrity": "sha512-3bnAPqtWXbyA9QEKYEstPcsQMxoQ97rjC0E1OZ+QVKuNNpzRDdIgdCpTVpHvqj/9UaWpqaEiENYqS2At8DtESA==",
+ "hasInstallScript": true,
+ "dependencies": {
+ "@prisma/engines": "6.2.0"
+ },
+ "bin": {
+ "prisma": "build/index.js"
+ },
+ "engines": {
+ "node": ">=18.18"
+ },
+ "optionalDependencies": {
+ "fsevents": "2.3.3"
+ }
+ },
"node_modules/prop-types": {
"version": "15.8.1",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
@@ -7089,6 +7180,42 @@
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
+ },
+ "node_modules/zod": {
+ "version": "3.24.1",
+ "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.1.tgz",
+ "integrity": "sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==",
+ "funding": {
+ "url": "https://github.com/sponsors/colinhacks"
+ }
+ },
+ "node_modules/zustand": {
+ "version": "5.0.3",
+ "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.3.tgz",
+ "integrity": "sha512-14fwWQtU3pH4dE0dOpdMiWjddcH+QzKIgk1cl8epwSE7yag43k/AD/m4L6+K7DytAOr9gGBe3/EXj9g7cdostg==",
+ "engines": {
+ "node": ">=12.20.0"
+ },
+ "peerDependencies": {
+ "@types/react": ">=18.0.0",
+ "immer": ">=9.0.6",
+ "react": ">=18.0.0",
+ "use-sync-external-store": ">=1.2.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "immer": {
+ "optional": true
+ },
+ "react": {
+ "optional": true
+ },
+ "use-sync-external-store": {
+ "optional": true
+ }
+ }
}
}
}
diff --git a/package.json b/package.json
index 3c64197..a5446d1 100644
--- a/package.json
+++ b/package.json
@@ -9,6 +9,7 @@
"lint": "next lint"
},
"dependencies": {
+ "@prisma/client": "^6.2.0",
"@radix-ui/react-accordion": "^1.2.2",
"@radix-ui/react-dialog": "^1.1.4",
"@radix-ui/react-dropdown-menu": "^2.1.4",
@@ -17,17 +18,22 @@
"@radix-ui/react-separator": "^1.1.1",
"@radix-ui/react-slot": "^1.1.1",
"@radix-ui/react-tabs": "^1.1.2",
+ "@types/bcryptjs": "^2.4.6",
"axios": "^1.7.9",
+ "bcryptjs": "^2.4.3",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"embla-carousel-react": "^8.5.1",
"framer-motion": "^11.15.0",
"lucide-react": "^0.469.0",
"next": "15.1.2",
+ "prisma": "^6.2.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"tailwind-merge": "^2.6.0",
- "tailwindcss-animate": "^1.0.7"
+ "tailwindcss-animate": "^1.0.7",
+ "zod": "^3.24.1",
+ "zustand": "^5.0.3"
},
"devDependencies": {
"@eslint/eslintrc": "^3",
diff --git a/prisma/migrations/20250108093314_init/migration.sql b/prisma/migrations/20250108093314_init/migration.sql
new file mode 100644
index 0000000..95ab0cd
--- /dev/null
+++ b/prisma/migrations/20250108093314_init/migration.sql
@@ -0,0 +1,25 @@
+-- CreateEnum
+CREATE TYPE "GENDER" AS ENUM ('NONBINARY', 'OTHER', 'PREFERNOTTOSAY', 'MALE', 'FEMALE');
+
+-- CreateTable
+CREATE TABLE "User" (
+ "id" TEXT NOT NULL,
+ "email" TEXT NOT NULL,
+ "fullName" TEXT NOT NULL,
+ "Gender" TEXT NOT NULL,
+ "leetcodeUsername" TEXT NOT NULL,
+ "password" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+
+ CONSTRAINT "User_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateIndex
+CREATE UNIQUE INDEX "User_id_key" ON "User"("id");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "User_leetcodeUsername_key" ON "User"("leetcodeUsername");
diff --git a/prisma/migrations/20250108095109_init/migration.sql b/prisma/migrations/20250108095109_init/migration.sql
new file mode 100644
index 0000000..6f7966a
--- /dev/null
+++ b/prisma/migrations/20250108095109_init/migration.sql
@@ -0,0 +1,16 @@
+/*
+ Warnings:
+
+ - You are about to drop the column `Gender` on the `User` table. All the data in the column will be lost.
+ - A unique constraint covering the columns `[supabaseId]` on the table `User` will be added. If there are existing duplicate values, this will fail.
+ - Added the required column `gender` to the `User` table without a default value. This is not possible if the table is not empty.
+ - Added the required column `supabaseId` to the `User` table without a default value. This is not possible if the table is not empty.
+
+*/
+-- AlterTable
+ALTER TABLE "User" DROP COLUMN "Gender",
+ADD COLUMN "gender" TEXT NOT NULL,
+ADD COLUMN "supabaseId" TEXT NOT NULL;
+
+-- CreateIndex
+CREATE UNIQUE INDEX "User_supabaseId_key" ON "User"("supabaseId");
diff --git a/prisma/migrations/20250108130332_init2/migration.sql b/prisma/migrations/20250108130332_init2/migration.sql
new file mode 100644
index 0000000..a075e95
--- /dev/null
+++ b/prisma/migrations/20250108130332_init2/migration.sql
@@ -0,0 +1,2 @@
+-- AlterTable
+ALTER TABLE "User" ADD COLUMN "isVerified" BOOLEAN NOT NULL DEFAULT false;
diff --git a/prisma/migrations/migration_lock.toml b/prisma/migrations/migration_lock.toml
new file mode 100644
index 0000000..648c57f
--- /dev/null
+++ b/prisma/migrations/migration_lock.toml
@@ -0,0 +1,3 @@
+# Please do not edit this file manually
+# It should be added in your version-control system (e.g., Git)
+provider = "postgresql"
\ No newline at end of file
diff --git a/prisma/schema.prisma b/prisma/schema.prisma
new file mode 100644
index 0000000..45888f9
--- /dev/null
+++ b/prisma/schema.prisma
@@ -0,0 +1,35 @@
+// This is your Prisma schema file,
+// learn more about it in the docs: https://pris.ly/d/prisma-schema
+
+// Looking for ways to speed up your queries, or scale easily with your serverless or edge functions?
+// Try Prisma Accelerate: https://pris.ly/cli/accelerate-init
+
+generator client {
+ provider = "prisma-client-js"
+}
+
+datasource db {
+ provider = "postgresql"
+ url = env("DATABASE_URL")
+}
+
+enum GENDER {
+ NONBINARY
+ OTHER
+ PREFERNOTTOSAY
+ MALE
+ FEMALE
+}
+
+model User {
+ id String @id @unique @default(uuid())
+ supabaseId String @unique
+ email String @unique
+ fullName String
+ gender String
+ leetcodeUsername String @unique
+ password String
+ isVerified Boolean @default(false)
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+}
diff --git a/store/AuthStore/useAuthStore.ts b/store/AuthStore/useAuthStore.ts
new file mode 100644
index 0000000..667712f
--- /dev/null
+++ b/store/AuthStore/useAuthStore.ts
@@ -0,0 +1,104 @@
+import prisma from '@/lib/database/prismaClient';
+import { supabase } from '@/lib/supabaseClient';
+import axios from 'axios';
+import { create } from 'zustand'
+
+interface User {
+ id?: string;
+ email: string;
+ password: string;
+ gender: string;
+ fullName: string;
+ leetcodeUsername: string;
+}
+
+interface authStore {
+ isSigningIn: boolean;
+ signinError: string | null;
+ signin: (signinMetaData: { email: string, password: string }, router: any) => void;
+ logout: () => void;
+
+ signupError: string | null;
+ isSigningUp: boolean;
+ signup: (signupMetaData: User, router: any) => void;
+ user: User | null;
+
+ authUserLoading: boolean;
+ fetchAuthUser: () => void;
+ authUser: User | null;
+}
+
+export const useAuthStore = create((set) => ({
+ signinError: null,
+ isSigningIn: false,
+ signin: async (signinMetaData, router) => {
+ set({ isSigningIn: true })
+ try {
+ const { data, error: loginError } =
+ await supabase.auth.signInWithPassword(signinMetaData);
+
+ if (loginError) {
+ set({ signinError: loginError.message })
+ console.log(loginError.message);
+ return
+ }
+
+ if (data.session) {
+ router.push("/dashboard");
+ } else {
+ throw new Error("Unable to retrieve session after login.");
+ }
+ } catch (err: any) {
+ console.error("Login Error:", err);
+ set({ signinError: err.message || "Something went wrong. Please try again." });
+ } finally {
+ set({ isSigningIn: false })
+ }
+ },
+ logout: () => {
+ console.log('logout');
+ },
+
+ signupError: null,
+ isSigningUp: false,
+ signup: async (signupMetaData, router) => {
+ set({ isSigningUp: true });
+ try {
+ const response = await axios.post('/api/auth/register', signupMetaData);
+ if (response.status === 201) {
+ set({ user: signupMetaData });
+ router.push('/auth/verify');
+ set({ signupError: null });
+ }
+ } catch (err: any) {
+ console.error('Error:', err);
+ set({ signupError: err.message || 'Something went wrong. Please try again.' });
+ } finally {
+ set({ isSigningUp: false });
+ }
+ },
+ user: null,
+
+ authUserLoading: false,
+ authUser: null,
+ fetchAuthUser: async () => {
+ try {
+ set({ authUserLoading: true });
+ const { data: sessionData, error: sessionError } =
+ await supabase.auth.getSession();
+ const supabaseId = sessionData.session?.user.id;
+ console.log("supabaseId", supabaseId);
+
+ const response = await axios.post('/api/auth/user', {
+ supabaseId,
+ });
+ if (response.status === 200) {
+ set({ authUser: response.data.user });
+ }
+ } catch (err: any) {
+ console.error('Error fetching user data:', err);
+ } finally {
+ set({ authUserLoading: false });
+ }
+ },
+}));
\ No newline at end of file
diff --git a/types/typeInterfaces.ts b/types/typeInterfaces.ts
new file mode 100644
index 0000000..5271217
--- /dev/null
+++ b/types/typeInterfaces.ts
@@ -0,0 +1,8 @@
+export interface UserType {
+ id?: string;
+ fullName: string;
+ gender: string;
+ email: string;
+ password: string;
+ leetcodeUsername: string
+}
\ No newline at end of file
diff --git a/utils/hashing.ts b/utils/hashing.ts
new file mode 100644
index 0000000..4c2d14f
--- /dev/null
+++ b/utils/hashing.ts
@@ -0,0 +1,10 @@
+import bcrypt from 'bcryptjs';
+
+export const hashPassword = async (password: string) => {
+ const salt = await bcrypt.genSalt(10);
+ return await bcrypt.hash(password, salt);
+}
+
+export const comparePassword = async (password: string, hashedPassword: string) => {
+ return await bcrypt.compare(password, hashedPassword);
+}
\ No newline at end of file
diff --git a/validations/validation.ts b/validations/validation.ts
new file mode 100644
index 0000000..4685424
--- /dev/null
+++ b/validations/validation.ts
@@ -0,0 +1,9 @@
+import z from 'zod';
+
+export const signupSchema = z.object({
+ email: z.string().email({ message: "Invalid email address" }),
+ password: z.string().min(6, { message: "Password must be at least 6 characters long" }),
+ fullName: z.string().nonempty({ message: "Full name is required" }),
+ leetcodeUsername: z.string().nonempty({ message: "Leetcode username is required" }),
+ gender: z.string().nonempty({ message: "Gender is required" })
+});
\ No newline at end of file