393 lines
19 KiB
TypeScript
393 lines
19 KiB
TypeScript
"use client";
|
|
|
|
import { useState, useRef } from "react";
|
|
import Image from "next/image";
|
|
import localFont from "next/font/local";
|
|
import { useGSAP } from "@gsap/react";
|
|
import gsap from "gsap";
|
|
import { Envelope, Lock, ShieldCheck, ArrowRight, CircleNotch, Eye, EyeSlash } from "@phosphor-icons/react";
|
|
import logo from '../../../public/logo.png';
|
|
|
|
const figtree = localFont({
|
|
src: [
|
|
{
|
|
path: '../../../public/Fonts/figtree/figtree.ttf',
|
|
},
|
|
],
|
|
});
|
|
|
|
// Reusing styles for consistency
|
|
const inputStyle = `${figtree.className} w-full p-4 border-2 border-black text-lg outline-none focus:bg-[#F7F7F7] transition-colors placeholder:text-gray-400 bg-white`;
|
|
const buttonStyle = `${figtree.className} w-full flex items-center justify-center gap-2 px-8 py-4 border-2 border-black bg-[#E7FE78] text-lg font-bold hover:bg-[#dcfc4e] transition-colors disabled:opacity-50 disabled:cursor-not-allowed shadow-[4px_4px_0px_0px_rgba(0,0,0,1)] hover:translate-y-[2px] hover:shadow-[2px_2px_0px_0px_rgba(0,0,0,1)] active:translate-y-[4px] active:shadow-none transition-all`;
|
|
const labelStyle = `${figtree.className} text-sm font-bold uppercase tracking-wider mb-2 block`;
|
|
|
|
function Login() {
|
|
const [step, setStep] = useState<'credentials' | '2fa' | 'forgot-password' | 'recovery-sent'>('credentials');
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
const [formData, setFormData] = useState({
|
|
email: '',
|
|
password: '',
|
|
otp: ''
|
|
});
|
|
const [error, setError] = useState('');
|
|
const [showPassword, setShowPassword] = useState(false);
|
|
|
|
const formRef = useRef<HTMLDivElement>(null);
|
|
const imageRef = useRef<HTMLDivElement>(null);
|
|
|
|
useGSAP(() => {
|
|
// Animate image side
|
|
gsap.fromTo(imageRef.current,
|
|
{ x: -50, opacity: 0 },
|
|
{ x: 0, opacity: 1, duration: 1, ease: "power3.out" }
|
|
);
|
|
|
|
// Animate form side
|
|
gsap.fromTo(formRef.current,
|
|
{ x: 50, opacity: 0 },
|
|
{ x: 0, opacity: 1, duration: 1, delay: 0.2, ease: "power3.out" }
|
|
);
|
|
}, []);
|
|
|
|
const handleLogin = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
setError('');
|
|
setIsLoading(true);
|
|
|
|
// Simulate API call
|
|
setTimeout(() => {
|
|
setIsLoading(false);
|
|
if (formData.email === 'asabizanjo@gmail.com' && formData.password === 'Balouch@12') {
|
|
// Transition to 2FA
|
|
gsap.to(formRef.current, {
|
|
x: -20,
|
|
opacity: 0,
|
|
duration: 0.3,
|
|
onComplete: () => {
|
|
setStep('2fa');
|
|
gsap.fromTo(formRef.current,
|
|
{ x: 20, opacity: 0 },
|
|
{ x: 0, opacity: 1, duration: 0.3 }
|
|
);
|
|
}
|
|
});
|
|
} else {
|
|
setError('Invalid email or password');
|
|
}
|
|
}, 1000);
|
|
};
|
|
|
|
const handleVerify = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
setError('');
|
|
setIsLoading(true);
|
|
|
|
// Simulate API call
|
|
setTimeout(() => {
|
|
setIsLoading(false);
|
|
if (formData.otp === '0000') {
|
|
alert('Login Successful! (JWT would be stored here)');
|
|
// Here you would store the JWT
|
|
} else {
|
|
setError('Invalid OTP. Please try again (Hint: 0000)');
|
|
}
|
|
}, 1000);
|
|
};
|
|
|
|
const handleForgotPasswordSubmit = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
setError('');
|
|
setIsLoading(true);
|
|
|
|
// Simulate API call
|
|
setTimeout(() => {
|
|
setIsLoading(false);
|
|
if (formData.email) {
|
|
gsap.to(formRef.current, {
|
|
x: -20,
|
|
opacity: 0,
|
|
duration: 0.3,
|
|
onComplete: () => {
|
|
setStep('recovery-sent');
|
|
gsap.fromTo(formRef.current,
|
|
{ x: 20, opacity: 0 },
|
|
{ x: 0, opacity: 1, duration: 0.3 }
|
|
);
|
|
}
|
|
});
|
|
} else {
|
|
setError('Please enter your email address');
|
|
}
|
|
}, 1000);
|
|
};
|
|
|
|
return (
|
|
<div className="flex min-h-screen w-full bg-[#FAFAFA] overflow-hidden">
|
|
{/* Left Side - Visual/Logo */}
|
|
<div className="hidden lg:flex w-1/2 bg-[#E7FE78] border-r-4 border-black flex-col items-center justify-center p-12 relative">
|
|
<div className="absolute inset-0 opacity-10"
|
|
style={{
|
|
backgroundImage: `radial-gradient(circle at 2px 2px, black 1px, transparent 0)`,
|
|
backgroundSize: '32px 32px'
|
|
}}
|
|
/>
|
|
|
|
<div ref={imageRef} className="relative z-10 flex flex-col items-center text-center">
|
|
<div className="w-64 h-64 bg-white border-4 border-black rounded-full flex items-center justify-center mb-12 shadow-[8px_8px_0px_0px_rgba(0,0,0,1)]">
|
|
<Image
|
|
src={logo}
|
|
alt="Logo"
|
|
width={180}
|
|
height={180}
|
|
className="object-contain"
|
|
/>
|
|
</div>
|
|
<h1 className={`${figtree.className} text-5xl font-black mb-6 tracking-tight`}>
|
|
Welcome Back
|
|
</h1>
|
|
<p className={`${figtree.className} text-xl font-medium max-w-md leading-relaxed`}>
|
|
Your Home Away From Home
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Right Side - Form */}
|
|
<div className="w-full lg:w-1/2 flex flex-col items-center justify-center p-8 lg:p-24 bg-white relative">
|
|
<div ref={formRef} className="w-full max-w-md">
|
|
{step === 'credentials' && (
|
|
<>
|
|
<div className="mb-10">
|
|
<h2 className={`${figtree.className} text-4xl font-bold mb-3`}>Sign In</h2>
|
|
<p className={`${figtree.className} text-gray-500 text-lg`}>Enter your details to access your account</p>
|
|
</div>
|
|
|
|
<form onSubmit={handleLogin} className="flex flex-col gap-6">
|
|
<div>
|
|
<label className={labelStyle}>Email Address</label>
|
|
<div className="relative">
|
|
<input
|
|
type="email"
|
|
placeholder="john@example.com"
|
|
className={inputStyle}
|
|
value={formData.email}
|
|
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
|
|
/>
|
|
<Envelope className="absolute right-4 top-1/2 -translate-y-1/2 text-gray-400" size={24} />
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<div className="flex justify-between items-center">
|
|
<label className={labelStyle}>Password</label>
|
|
<button
|
|
type="button"
|
|
className={`${figtree.className} text-sm font-bold text-gray-500 hover:text-black transition-colors mb-2`}
|
|
onClick={() => {
|
|
setError('');
|
|
gsap.to(formRef.current, {
|
|
x: -20,
|
|
opacity: 0,
|
|
duration: 0.3,
|
|
onComplete: () => {
|
|
setStep('forgot-password');
|
|
gsap.fromTo(formRef.current,
|
|
{ x: 20, opacity: 0 },
|
|
{ x: 0, opacity: 1, duration: 0.3 }
|
|
);
|
|
}
|
|
});
|
|
}}
|
|
>
|
|
Forgot Password?
|
|
</button>
|
|
</div>
|
|
<div className="relative">
|
|
<input
|
|
type={showPassword ? "text" : "password"}
|
|
placeholder="••••••••"
|
|
className={inputStyle}
|
|
value={formData.password}
|
|
onChange={(e) => setFormData({ ...formData, password: e.target.value })}
|
|
/>
|
|
<button
|
|
type="button"
|
|
onClick={() => setShowPassword(!showPassword)}
|
|
className="absolute right-4 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600 transition-colors"
|
|
>
|
|
{showPassword ? <EyeSlash size={24} /> : <Eye size={24} />}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{error && (
|
|
<div className={`${figtree.className} p-4 bg-red-50 border-2 border-red-500 text-red-500 font-bold flex items-center gap-2`}>
|
|
<ShieldCheck size={20} />
|
|
{error}
|
|
</div>
|
|
)}
|
|
|
|
<button
|
|
type="submit"
|
|
disabled={isLoading}
|
|
className={buttonStyle}
|
|
>
|
|
{isLoading ? (
|
|
<CircleNotch className="animate-spin" size={24} />
|
|
) : (
|
|
<>Sign In <ArrowRight size={20} weight="bold" /></>
|
|
)}
|
|
</button>
|
|
|
|
<div className="text-center mt-4">
|
|
<p className={`${figtree.className} text-gray-500`}>
|
|
Don't have an account?{' '}
|
|
<a href="/signup" className="text-black font-bold hover:underline">
|
|
Sign Up
|
|
</a>
|
|
</p>
|
|
</div>
|
|
</form>
|
|
</>
|
|
)}
|
|
{step === '2fa' && (
|
|
<>
|
|
<div className="mb-10">
|
|
<h2 className={`${figtree.className} text-4xl font-bold mb-3`}>Two-Factor Auth</h2>
|
|
<p className={`${figtree.className} text-gray-500 text-lg`}>Enter the code sent to your device</p>
|
|
</div>
|
|
|
|
<form onSubmit={handleVerify} className="flex flex-col gap-6">
|
|
<div>
|
|
<label className={labelStyle}>Authentication Code</label>
|
|
<div className="relative">
|
|
<input
|
|
type="text"
|
|
placeholder="0000"
|
|
maxLength={4}
|
|
className={`${inputStyle} text-center text-3xl tracking-[1em] font-bold`}
|
|
value={formData.otp}
|
|
onChange={(e) => setFormData({ ...formData, otp: e.target.value })}
|
|
/>
|
|
</div>
|
|
<p className={`${figtree.className} text-xs text-gray-400 mt-2 text-center`}>Use '0000' for testing</p>
|
|
</div>
|
|
|
|
{error && (
|
|
<div className={`${figtree.className} p-4 bg-red-50 border-2 border-red-500 text-red-500 font-bold flex items-center gap-2`}>
|
|
<ShieldCheck size={20} />
|
|
{error}
|
|
</div>
|
|
)}
|
|
|
|
<button
|
|
type="submit"
|
|
disabled={isLoading}
|
|
className={buttonStyle}
|
|
>
|
|
{isLoading ? (
|
|
<CircleNotch className="animate-spin" size={24} />
|
|
) : (
|
|
<>Verify & Login <ShieldCheck size={20} weight="bold" /></>
|
|
)}
|
|
</button>
|
|
|
|
<button
|
|
type="button"
|
|
onClick={() => {
|
|
setStep('credentials');
|
|
setError('');
|
|
}}
|
|
className={`${figtree.className} text-gray-500 hover:text-black hover:underline text-center mt-4`}
|
|
>
|
|
Back to Login
|
|
</button>
|
|
</form>
|
|
</>
|
|
)}
|
|
|
|
{step === 'forgot-password' && (
|
|
<>
|
|
<div className="mb-10">
|
|
<h2 className={`${figtree.className} text-4xl font-bold mb-3`}>Reset Password</h2>
|
|
<p className={`${figtree.className} text-gray-500 text-lg`}>Enter your email to receive a recovery link</p>
|
|
</div>
|
|
|
|
<form onSubmit={handleForgotPasswordSubmit} className="flex flex-col gap-6">
|
|
<div>
|
|
<label className={labelStyle}>Email Address</label>
|
|
<div className="relative">
|
|
<input
|
|
type="email"
|
|
placeholder="john@example.com"
|
|
className={inputStyle}
|
|
value={formData.email}
|
|
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
|
|
/>
|
|
<Envelope className="absolute right-4 top-1/2 -translate-y-1/2 text-gray-400" size={24} />
|
|
</div>
|
|
</div>
|
|
|
|
{error && (
|
|
<div className={`${figtree.className} p-4 bg-red-50 border-2 border-red-500 text-red-500 font-bold flex items-center gap-2`}>
|
|
<ShieldCheck size={20} />
|
|
{error}
|
|
</div>
|
|
)}
|
|
|
|
<button
|
|
type="submit"
|
|
disabled={isLoading}
|
|
className={buttonStyle}
|
|
>
|
|
{isLoading ? (
|
|
<CircleNotch className="animate-spin" size={24} />
|
|
) : (
|
|
<>Send Recovery Link <ArrowRight size={20} weight="bold" /></>
|
|
)}
|
|
</button>
|
|
|
|
<button
|
|
type="button"
|
|
onClick={() => {
|
|
setStep('credentials');
|
|
setError('');
|
|
}}
|
|
className={`${figtree.className} text-gray-500 hover:text-black hover:underline text-center mt-4`}
|
|
>
|
|
Back to Login
|
|
</button>
|
|
</form>
|
|
</>
|
|
)}
|
|
|
|
{step === 'recovery-sent' && (
|
|
<div className="text-center">
|
|
<div className="w-20 h-20 bg-[#E7FE78] rounded-full flex items-center justify-center mx-auto mb-6 border-2 border-black shadow-[4px_4px_0px_0px_rgba(0,0,0,1)]">
|
|
<Envelope size={40} weight="fill" />
|
|
</div>
|
|
<h2 className={`${figtree.className} text-3xl font-bold mb-4`}>Check Your Email</h2>
|
|
<p className={`${figtree.className} text-gray-500 text-lg mb-8`}>
|
|
We've sent a password recovery link to <br />
|
|
<span className="font-bold text-black">{formData.email}</span>
|
|
</p>
|
|
|
|
<button
|
|
type="button"
|
|
onClick={() => {
|
|
setStep('credentials');
|
|
setError('');
|
|
}}
|
|
className={buttonStyle}
|
|
>
|
|
Back to Login
|
|
</button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default Login;
|