diff --git a/app/auth/signin/page.tsx b/app/auth/signin/page.tsx index 62d338d8a..ab1e5131a 100644 --- a/app/auth/signin/page.tsx +++ b/app/auth/signin/page.tsx @@ -1,11 +1,117 @@ 'use client'; import AuthLayout from '@/components/auth/AuthLayout'; import LoginForm from '@/components/auth/LoginForm'; +import { useState } from 'react'; +import { useRouter, useSearchParams } from 'next/navigation'; +import { getSession, signIn } from 'next-auth/react'; +import { toast } from 'sonner'; +import Cookies from 'js-cookie'; +import z from 'zod'; +import { useForm } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import Loading from '@/components/loading/Loading'; + +const formSchema = z.object({ + email: z.string().email({ + message: 'Invalid email address', + }), + password: z.string().min(8, { + message: 'Password must be at least 8 characters', + }), +}); export default function SignInPage() { + const router = useRouter(); + const searchParams = useSearchParams(); + const callbackUrl = searchParams.get('callbackUrl') || '/user'; + const [showPassword, setShowPassword] = useState(false); + const [isLoading, setIsLoading] = useState(false); + + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: { + email: '', + password: '', + }, + }); + + const onSubmit = async (values: z.infer) => { + setIsLoading(true); + try { + const result = await signIn('credentials', { + email: values.email, + password: values.password, + redirect: false, + }); + + if (result?.error) { + if (result.error === 'UNVERIFIED_EMAIL') { + toast.error( + 'Please verify your email before signing in. Check your inbox for a verification link.' + ); + setTimeout(() => { + router.push( + `/auth/verify-email?email=${encodeURIComponent(values.email)}` + ); + }, 3000); + } else { + form.setError('email', { + type: 'manual', + message: 'Invalid email or password', + }); + form.setError('password', { + type: 'manual', + message: 'Invalid email or password', + }); + } + } else if (result?.ok) { + const session = await getSession(); + + if (session) { + if (session.user.accessToken) { + Cookies.set('accessToken', session.user.accessToken); + } + if (session.user.refreshToken) { + Cookies.set('refreshToken', session.user.refreshToken); + } + router.push(callbackUrl); + } else { + form.setError('root', { + type: 'manual', + message: + 'Login successful but session not found. Please try again.', + }); + } + router.push(callbackUrl); + } else { + form.setError('root', { + type: 'manual', + message: 'An unexpected error occurred. Please try again.', + }); + } + } catch { + form.setError('root', { + type: 'manual', + message: 'An unexpected error occurred. Please try again.', + }); + } finally { + setIsLoading(false); + } + }; + + if (isLoading) { + return ; + } + return ( - + ); } diff --git a/components/auth/AuthLayout.tsx b/components/auth/AuthLayout.tsx index 8a3cd1e0b..4d9586074 100644 --- a/components/auth/AuthLayout.tsx +++ b/components/auth/AuthLayout.tsx @@ -1,8 +1,7 @@ 'use client'; import Image from 'next/image'; -import React from 'react'; +import React, { useState, useEffect } from 'react'; import { Badge } from '../ui/badge'; -import { useState, useEffect } from 'react'; import { Carousel, CarouselContent, @@ -11,7 +10,6 @@ import { CarouselPrevious, type CarouselApi, } from '../ui/carousel'; -import Autoplay from 'embla-carousel-autoplay'; interface AuthLayoutProps { children: React.ReactNode; @@ -19,7 +17,6 @@ interface AuthLayoutProps { } const AuthLayout = ({ children, showCarousel = true }: AuthLayoutProps) => { - // Slide data const slides = [ { id: 1, @@ -67,136 +64,119 @@ const AuthLayout = ({ children, showCarousel = true }: AuthLayoutProps) => { const [api, setApi] = useState(); useEffect(() => { - if (!api) { - return; - } - - api.on('select', () => { - setCurrentSlide(api.selectedScrollSnap()); - }); + if (!api) return; + api.on('select', () => setCurrentSlide(api.selectedScrollSnap())); }, [api]); return ( -
-
- {/* Left Panel - Auth Form */} -
+
+
+ {/* Left Panel */} +
auth + + grid -
+ + {/* Form Content */} +
auth -
{children}
-
-
- auth +
{children}
{/* Right Panel - Carousel */} {showCarousel && ( -
-
-
+
+
+
- + {slides.map(slide => ( - -
- {/* Main card with glassmorphism effects */} -
+ +
+ {/* Image Card Container */} +
Glassmorphism card -
- logo +
+
+ {`${slide.title} +
- {/* Project information section */} -
- + {/* Content */} +
+ {slide.badge} - -
-

- {slide.title} -

-

- {slide.description} -

-
+

+ {slide.title} +

+

+ {slide.description} +

))} -
- -
- {slides.map((_, dotIndex) => ( + + {/* Controls */} +
+ + +
+ {slides.map((_, i) => (
- + +
diff --git a/components/auth/LoginForm.tsx b/components/auth/LoginForm.tsx index ecf184f0d..95503a1db 100644 --- a/components/auth/LoginForm.tsx +++ b/components/auth/LoginForm.tsx @@ -1,5 +1,5 @@ 'use client'; -import React, { useState } from 'react'; +import React from 'react'; import { BoundlessButton } from '../buttons'; import { Form, @@ -9,19 +9,14 @@ import { FormMessage, } from '../ui/form'; import { FormLabel } from '../ui/form'; -import z from 'zod'; -import { useForm } from 'react-hook-form'; -import { zodResolver } from '@hookform/resolvers/zod'; import { Input } from '../ui/input'; import { Eye, EyeOff, LockIcon, MailIcon } from 'lucide-react'; import { Checkbox } from '../ui/checkbox'; import { Label } from '../ui/label'; import Link from 'next/link'; -import { getSession, signIn } from 'next-auth/react'; -import { toast } from 'sonner'; -import { useRouter, useSearchParams } from 'next/navigation'; -import Cookies from 'js-cookie'; import Image from 'next/image'; +import { UseFormReturn } from 'react-hook-form'; +import z from 'zod'; const formSchema = z.object({ email: z.string().email({ @@ -32,88 +27,28 @@ const formSchema = z.object({ }), }); -const LoginForm = () => { - const router = useRouter(); - const searchParams = useSearchParams(); - const callbackUrl = searchParams.get('callbackUrl') || '/user'; - const [showPassword, setShowPassword] = useState(false); - - const form = useForm>({ - resolver: zodResolver(formSchema), - defaultValues: { - email: '', - password: '', - }, - }); - - const onSubmit = async (values: z.infer) => { - try { - const result = await signIn('credentials', { - email: values.email, - password: values.password, - redirect: false, - }); - - if (result?.error) { - if (result.error === 'UNVERIFIED_EMAIL') { - toast.error( - 'Please verify your email before signing in. Check your inbox for a verification link.' - ); - setTimeout(() => { - router.push( - `/auth/verify-email?email=${encodeURIComponent(values.email)}` - ); - }, 3000); - } else { - form.setError('email', { - type: 'manual', - message: 'Invalid email or password', - }); - form.setError('password', { - type: 'manual', - message: 'Invalid email or password', - }); - } - } else if (result?.ok) { - const session = await getSession(); - - if (session) { - if (session.user.accessToken) { - Cookies.set('accessToken', session.user.accessToken); - } - if (session.user.refreshToken) { - Cookies.set('refreshToken', session.user.refreshToken); - } - router.push(callbackUrl); - } else { - form.setError('root', { - type: 'manual', - message: - 'Login successful but session not found. Please try again.', - }); - } - router.push(callbackUrl); - } else { - form.setError('root', { - type: 'manual', - message: 'An unexpected error occurred. Please try again.', - }); - } - } catch { - form.setError('root', { - type: 'manual', - message: 'An unexpected error occurred. Please try again.', - }); - } - }; +interface LoginFormProps { + form: UseFormReturn>; + onSubmit: (values: z.infer) => Promise; + showPassword: boolean; + setShowPassword: (show: boolean) => void; + isLoading: boolean; +} +const LoginForm = ({ + form, + onSubmit, + showPassword, + setShowPassword, + isLoading, +}: LoginFormProps) => { return ( <> -
-

+
+

Sign in

-

+

Sign in to manage campaigns, apply for grants, and track your funding progress — all in one dashboard.

@@ -224,9 +159,9 @@ const LoginForm = () => { Sign in diff --git a/components/auth/SignupForm.tsx b/components/auth/SignupForm.tsx index 5e42fe321..3cecb3ef2 100644 --- a/components/auth/SignupForm.tsx +++ b/components/auth/SignupForm.tsx @@ -139,11 +139,11 @@ const SignupForm = () => { return ( <> -
-

+
+

Create account

-

+

Create an account to manage campaigns, apply for grants, and track your funding progress — all in one dashboard.

diff --git a/components/loading/Loading.tsx b/components/loading/Loading.tsx new file mode 100644 index 000000000..7150387b0 --- /dev/null +++ b/components/loading/Loading.tsx @@ -0,0 +1,12 @@ +import React from 'react'; +import LoadingSpinner from '../LoadingSpinner'; + +const Loading = () => { + return ( +
+ +
+ ); +}; + +export default Loading; diff --git a/components/providers/auth-provider.tsx b/components/providers/auth-provider.tsx index cf45b47e9..2a977db95 100644 --- a/components/providers/auth-provider.tsx +++ b/components/providers/auth-provider.tsx @@ -2,6 +2,7 @@ import React, { useEffect, useState } from 'react'; import { useAuthStore } from '@/lib/stores/auth-store'; +import Loading from '../loading/Loading'; interface AuthProviderProps { children: React.ReactNode; @@ -63,11 +64,7 @@ export function AuthProvider({ children }: AuthProviderProps) { // Show loading state while hydrating if (!isHydrated) { - return ( -
-
-
- ); + return ; } return <>{children}; @@ -98,11 +95,7 @@ export function AuthLoadingProvider({ const { isLoading } = useAuthStore(); if (!isHydrated || isLoading) { - return ( -
-
-
- ); + return ; } return <>{children};