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..7476d17 --- /dev/null +++ b/app/api/auth/user/route.ts @@ -0,0 +1,31 @@ +import prisma from '@/lib/database/prismaClient'; +import { NextResponse } from 'next/server'; + +export async function POST(request: Request) { + try { + + const { supabaseId } = await request.json(); + 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 }); + } + + return NextResponse.json({ user: userData },{status: 200}); + } 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 === '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 ( -
- -

{error}

-
- ); - } + + + + + + + + + - 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..edac9c4 --- /dev/null +++ b/components/AuthComponent/SigninForm.tsx @@ -0,0 +1,104 @@ +"use client"; + +import React, { useState } from "react"; +import { useRouter } from "next/navigation"; +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 + + + +
+
+ + +
+ +
+ + +
+ + {signinError && ( + + {signinError} + + )} + + + +
+ + + +
+
+ ); +} 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 + + + +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ + {signupError && ( + + {signupError} + + )} + + + +
+ + + +
+
+ ); +} diff --git a/components/Buttons/LandingEmerlandButton.tsx b/components/Buttons/LandingEmerlandButton.tsx new file mode 100644 index 0000000..13ad338 --- /dev/null +++ b/components/Buttons/LandingEmerlandButton.tsx @@ -0,0 +1,13 @@ +import React from "react"; + +export default function LandingEmerlandButton({ + children, +}: { + children: React.ReactNode; +}) { + return ( + + ); +} \ No newline at end of file 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 ( + + ); +} 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 ( -
-
-
-

Log In

-
- {/* Email Field */} -
- - -
- - {/* Password Field */} -
- - -
- - {/* Error Message */} - {error &&

{error}

} - - {/* Submit Button */} - - -
- - Don't have an account? - Sign up - -
-
-
-
-
- ); -}; - -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 ( -
-
-
-

Sign Up

-
- {/* Name Field */} -
- - -
- - {/* Email Field */} -
- - -
- - {/* Password Field */} -
- - -
- - {/* LeetCode Username Field */} -
- - -
- - {/* Gender Field */} -
- - -
- - {/* Error Message */} - {error &&

{error}

} - - {/* Submit Button */} - -
- - Already have an account? - Log in - -
-
-
-
-
- ); -}; - -export default SignupForm; diff --git a/components/header.tsx b/components/header.tsx index 6d4dfff..8cd4550 100644 --- a/components/header.tsx +++ b/components/header.tsx @@ -11,7 +11,7 @@ const Navbar = ({ userId }: { userId?: string }) => { const router = useRouter(); return (
-