Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
108 changes: 107 additions & 1 deletion app/auth/signin/page.tsx
Original file line number Diff line number Diff line change
@@ -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<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
email: '',
password: '',
},
});

const onSubmit = async (values: z.infer<typeof formSchema>) => {
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 <Loading />;
}

return (
<AuthLayout>
<LoginForm />
<LoginForm
form={form}
onSubmit={onSubmit}
showPassword={showPassword}
setShowPassword={setShowPassword}
isLoading={isLoading}
/>
</AuthLayout>
);
}
164 changes: 72 additions & 92 deletions components/auth/AuthLayout.tsx
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -11,15 +10,13 @@ import {
CarouselPrevious,
type CarouselApi,
} from '../ui/carousel';
import Autoplay from 'embla-carousel-autoplay';

interface AuthLayoutProps {
children: React.ReactNode;
showCarousel?: boolean;
}

const AuthLayout = ({ children, showCarousel = true }: AuthLayoutProps) => {
// Slide data
const slides = [
{
id: 1,
Expand Down Expand Up @@ -67,136 +64,119 @@ const AuthLayout = ({ children, showCarousel = true }: AuthLayoutProps) => {
const [api, setApi] = useState<CarouselApi>();

useEffect(() => {
if (!api) {
return;
}

api.on('select', () => {
setCurrentSlide(api.selectedScrollSnap());
});
if (!api) return;
api.on('select', () => setCurrentSlide(api.selectedScrollSnap()));
}, [api]);

return (
<div className='bg-background'>
<div className='flex flex-col lg:flex-row justify-between h-screen overflow-hidden'>
{/* Left Panel - Auth Form */}
<div className='w-full lg:basis-1/2 relative z-10 p-4 lg:p-0'>
<div className='bg-background min-h-screen w-full flex justify-center'>
<div className='flex flex-col lg:flex-row h-screen w-full max-w-[1920px] xl:max-w-[1600px] 2xl:max-w-[1800px] mx-auto'>
{/* Left Panel */}
<div className='w-full h-full lg:basis-1/2 relative flex items-center justify-center px-6 py-10 lg:px-16 xl:px-24 2xl:px-32 overflow-hidden'>
<Image
src='/auth/top-left.png'
alt='auth'
width={248}
height={231}
className='object-cover absolute top-0 left-0 pointer-events-none z-10'
className='absolute top-0 left-0 object-contain pointer-events-none z-0'
unoptimized
/>

<Image
src='/auth/grid.svg'
alt='grid'
width={248}
height={231}
className='absolute bottom-0 left-0 object-cover pointer-events-none w-full min-w-[500px] max-h-[254px] z-0'
unoptimized
/>
<div className='flex flex-col justify-center h-full ml-4 lg:ml-[130px] mt-8 lg:mt-[33px] max-w-[500px] space-y-6 lg:space-y-8'>

{/* Form Content */}
<div className='flex flex-col justify-center h-full max-w-[500px] w-full gap-6 lg:gap-10 relative z-10'>
<Image
src='/auth/logo.svg'
alt='auth'
width={123}
height={22}
className='object-cover'
alt='auth-logo'
width={160}
height={30}
className='object-contain mx-auto lg:mx-0'
unoptimized
/>
<div className='space-y-6 lg:space-y-8'>{children}</div>
</div>
<div className='w-full h-[254px]'>
<Image
src='/auth/grid.svg'
alt='auth'
width={248}
height={231}
className='object-cover absolute bottom-0 right-0 pointer-events-none 0 w-full h-[254px] -z-10'
/>
<div className='space-y-6 lg:space-y-10'>{children}</div>
</div>
</div>

{/* Right Panel - Carousel */}
{showCarousel && (
<div className='w-full lg:basis-1/2 hidden lg:flex items-center justify-center p-4 lg:p-8 min-h-[400px] lg:min-h-0'>
<div
className='w-[650px] max-w-[650px] h-[400px] lg:h-full rounded-[12px] border border-[#2B2B2B] relative overflow-hidden bg-black'
style={{
background: `
url('/auth/bg.png')
conic-gradient(from 180deg at 50% 50%,rgba(0,0,0,0.60) 44.56deg,rgba(0,0,0,0.20) 90.14deg,rgba(0,0,0,0.06) 131deg,#000 222.03deg,rgba(0,0,0,0.24) 286.68deg,rgba(0,0,0,0.00) 317.11deg,#000 360deg)
`,
}}
>
<div className='flex flex-col items-center justify-center h-full gap-4 lg:gap-8 relative z-10 px-2 lg:px-8 py-4 lg:py-0'>
<div className='hidden lg:flex w-full lg:basis-1/2 items-center justify-center p-6 lg:p-8 xl:p-10'>
<div className="relative w-full max-w-[500px] md:max-w-[600px] xl:max-w-[700px] 2xl:max-w-[800px] aspect-[4/3] lg:aspect-[3/4] xl:aspect-[4/3] h-full rounded-2xl border border-[#2B2B2B] overflow-hidden bg-[url('/auth/bg.png')] bg-cover bg-center shadow-xl">
<div className='flex flex-col items-center justify-center h-full p-6 lg:p-8 relative z-10'>
<Carousel
opts={{
align: 'start',
loop: true,
}}
className='w-full'
opts={{ align: 'start', loop: true }}
setApi={setApi}
plugins={[
Autoplay({
delay: 2000,
}),
]}
className='w-full h-full flex'
>
<CarouselContent>
<CarouselContent className=' h-full'>
{slides.map(slide => (
<CarouselItem key={slide.id}>
<div className='flex flex-col items-center gap-4 lg:gap-8'>
{/* Main card with glassmorphism effects */}
<div className='w-full max-w-[300px] lg:max-w-[536px] relative flex items-center justify-center'>
<CarouselItem key={slide.id} className='h-full'>
<div className='flex flex-col items-center justify-between h-full gap-6 lg:gap-8'>
{/* Image Card Container */}
<div className='relative w-full max-w-[400px] lg:max-w-[500px] aspect-[5/3]'>
<Image
src='/auth/image-card.png'
alt='Glassmorphism card'
width={536}
height={320}
className='object-cover rounded-[12px] w-full h-auto'
alt='Glass card'
fill
className='object-cover rounded-2xl'
unoptimized
/>
<div className='z-10 absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2'>
<Image
src={slide.logo}
alt='logo'
width={537}
height={22}
className='object-cover w-[150px] lg:w-[250px] h-auto'
/>
<div className='absolute inset-0 flex items-center justify-center'>
<div className='relative w-[100px] h-[40px] sm:w-[120px] sm:h-[48px] lg:w-[160px] lg:h-[64px] xl:w-[200px] xl:h-[80px]'>
<Image
src={slide.logo}
alt={`${slide.title} logo`}
fill
className='object-contain'
/>
</div>
</div>
</div>

{/* Project information section */}
<div className='w-full max-w-[300px] lg:max-w-[400px]'>
<Badge className='w-fit text-xs font-medium backdrop-blur-md bg-white/10 border border-white/20 rounded-full px-3 py-1 text-white mb-4'>
{/* Content */}
<div className='text-center w-full max-w-[300px] lg:max-w-[400px] px-2 mb-8'>
<Badge className='inline-flex items-center text-xs font-medium backdrop-blur-md bg-white/10 border border-white/20 rounded-full px-3 py-1 text-white mb-4 hover:bg-white/15 transition-colors'>
{slide.badge}
</Badge>

<div className='space-y-3'>
<h3 className='text-white text-xs lg:text-sm font-semibold'>
{slide.title}
</h3>
<p className='text-xs lg:text-sm text-[#D9D9D9] leading-relaxed mx-auto'>
{slide.description}
</p>
</div>
<h3 className='text-white text-lg lg:text-xl font-semibold mb-3'>
{slide.title}
</h3>
<p className='text-sm lg:text-base text-[#D9D9D9] leading-relaxed'>
{slide.description}
</p>
</div>
</div>
</CarouselItem>
))}
</CarouselContent>
<div className='flex items-center justify-between gap-2 relative z-10 max-w-[300px] lg:max-w-[400px] mx-auto'>
<CarouselPrevious className='static bg-transparent border-none text-white translate-0' />
<div className='flex items-center justify-center gap-2'>
{slides.map((_, dotIndex) => (

{/* Controls */}
<div className='flex items-center justify-between w-full mt-6 absolute bottom-0'>
<CarouselPrevious className='relative left-0 translate-y-0 bg-transparent border-none text-white hover:text-gray-300 hover:scale-110 transition-all duration-200 h-8 w-8' />

<div className='flex items-center gap-2'>
{slides.map((_, i) => (
<button
key={dotIndex}
onClick={() => api?.scrollTo(dotIndex)}
className={`w-8 h-1.5 rounded-full transition-all duration-300 ${
dotIndex === currentSlide
? 'bg-white'
: 'bg-gray-600'
key={i}
onClick={() => api?.scrollTo(i)}
className={`h-2 rounded-full transition-all duration-300 ${
i === currentSlide
? 'bg-white w-8'
: 'bg-gray-600 hover:bg-gray-500 w-2'
}`}
aria-label={`Go to slide ${i + 1}`}
/>
))}
</div>
<CarouselNext className='static bg-transparent border-none text-white translate-0' />

<CarouselNext className='relative right-0 translate-y-0 bg-transparent border-none text-white hover:text-gray-300 hover:scale-110 transition-all duration-200 h-8 w-8' />
</div>
</Carousel>
</div>
Expand Down
Loading
Loading