595 lines
24 KiB
TypeScript
595 lines
24 KiB
TypeScript
import React, { useState, useEffect, useRef } from 'react';
|
|
import { useAuth } from '../store/AuthContext';
|
|
import { useNavigate } from 'react-router-dom';
|
|
import { ShieldCheck, Mail, Lock, Sparkles, ArrowRight, Eye, EyeOff, RefreshCw } from 'lucide-react';
|
|
|
|
/* ── Floating Particle ─────────────────────────────────────── */
|
|
interface Particle {
|
|
id: number;
|
|
x: number;
|
|
y: number;
|
|
size: number;
|
|
opacity: number;
|
|
delay: number;
|
|
duration: number;
|
|
}
|
|
|
|
const generateParticles = (count: number): Particle[] =>
|
|
Array.from({ length: count }, (_, i) => ({
|
|
id: i,
|
|
x: Math.random() * 100,
|
|
y: Math.random() * 100,
|
|
size: Math.random() * 4 + 2,
|
|
opacity: Math.random() * 0.4 + 0.1,
|
|
delay: Math.random() * 6,
|
|
duration: Math.random() * 4 + 5,
|
|
}));
|
|
|
|
/* ── Animated Background ───────────────────────────────────── */
|
|
const AnimatedBackground: React.FC = () => {
|
|
const [particles] = useState(() => generateParticles(25));
|
|
|
|
return (
|
|
<div className="absolute inset-0 overflow-hidden pointer-events-none">
|
|
{/* Mesh gradient base */}
|
|
<div className="absolute inset-0 gradient-mesh" />
|
|
|
|
{/* Large glowing orbs */}
|
|
<div
|
|
className="orb orb-primary absolute w-[500px] h-[500px] -top-32 -right-32 opacity-60"
|
|
style={{ animationDuration: '9s' }}
|
|
/>
|
|
<div
|
|
className="orb orb-secondary absolute w-[400px] h-[400px] -bottom-20 -left-20 opacity-50"
|
|
style={{ animationDuration: '7s', animationDelay: '-4s' }}
|
|
/>
|
|
<div
|
|
className="orb orb-accent absolute w-[300px] h-[300px] top-1/2 left-1/3 opacity-40"
|
|
style={{ animationDuration: '11s', animationDelay: '-2s' }}
|
|
/>
|
|
|
|
{/* Morphing blob */}
|
|
<div
|
|
className="absolute top-1/4 right-1/4 w-64 h-64 opacity-20 animate-morph"
|
|
style={{
|
|
background: 'linear-gradient(135deg, rgba(162,231,113,0.5), rgba(117,191,70,0.3))',
|
|
filter: 'blur(40px)',
|
|
}}
|
|
/>
|
|
|
|
{/* Grid overlay */}
|
|
<div
|
|
className="absolute inset-0 opacity-[0.025]"
|
|
style={{
|
|
backgroundImage: `linear-gradient(var(--color-ink-800) 1px, transparent 1px),
|
|
linear-gradient(90deg, var(--color-ink-800) 1px, transparent 1px)`,
|
|
backgroundSize: '60px 60px',
|
|
}}
|
|
/>
|
|
|
|
{/* Floating particles */}
|
|
{particles.map(p => (
|
|
<div
|
|
key={p.id}
|
|
className="absolute rounded-full animate-float"
|
|
style={{
|
|
left: `${p.x}%`,
|
|
top: `${p.y}%`,
|
|
width: `${p.size}px`,
|
|
height: `${p.size}px`,
|
|
opacity: p.opacity,
|
|
background: `radial-gradient(circle, var(--color-primary-400), var(--color-primary-600))`,
|
|
animationDelay: `${-p.delay}s`,
|
|
animationDuration: `${p.duration}s`,
|
|
filter: 'blur(0.5px)',
|
|
}}
|
|
/>
|
|
))}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
/* ── 3D Orbiting Ring ──────────────────────────────────────── */
|
|
const OrbitingRing: React.FC = () => (
|
|
<div className="relative w-28 h-28 mx-auto">
|
|
{/* Center icon */}
|
|
<div className="absolute inset-0 flex items-center justify-center z-10">
|
|
<div
|
|
className="w-16 h-16 rounded-2xl flex items-center justify-center animate-pulse-glow"
|
|
style={{
|
|
background: 'linear-gradient(135deg, var(--color-primary-400), var(--color-primary-600))',
|
|
boxShadow: '0 0 30px rgba(162,231,113,0.5)',
|
|
transform: 'perspective(300px) rotateY(-8deg) rotateX(4deg)',
|
|
}}
|
|
>
|
|
<ShieldCheck className="w-8 h-8" style={{ color: 'var(--color-ink-800)' }} />
|
|
</div>
|
|
</div>
|
|
|
|
{/* Orbiting ring 1 */}
|
|
<div
|
|
className="absolute inset-0 rounded-full"
|
|
style={{
|
|
border: '1.5px solid rgba(162,231,113,0.3)',
|
|
animation: 'spin 6s linear infinite',
|
|
}}
|
|
>
|
|
<div
|
|
className="absolute -top-1.5 left-1/2 -translate-x-1/2 w-3 h-3 rounded-full"
|
|
style={{ background: 'var(--color-primary-400)', boxShadow: '0 0 8px rgba(162,231,113,0.8)' }}
|
|
/>
|
|
</div>
|
|
|
|
{/* Orbiting ring 2 */}
|
|
<div
|
|
className="absolute inset-3 rounded-full"
|
|
style={{
|
|
border: '1px solid rgba(162,231,113,0.2)',
|
|
animation: 'spin 4s linear infinite reverse',
|
|
}}
|
|
>
|
|
<div
|
|
className="absolute -top-1 left-1/2 -translate-x-1/2 w-2 h-2 rounded-full"
|
|
style={{ background: 'var(--color-primary-600)' }}
|
|
/>
|
|
</div>
|
|
</div>
|
|
);
|
|
|
|
/* ── Main LoginForm ────────────────────────────────────────── */
|
|
export const LoginForm: React.FC = () => {
|
|
const { login, register, verifyMfa, mfaPendingEmail } = useAuth();
|
|
const navigate = useNavigate();
|
|
|
|
const [isRegister, setIsRegister] = useState(false);
|
|
const [email, setEmail] = useState('');
|
|
const [showPass, setShowPass] = useState(false);
|
|
const [otp, setOtp] = useState('');
|
|
const [error, setError] = useState('');
|
|
const [loading, setLoading] = useState(false);
|
|
const [mousePos, setMousePos] = useState({ x: 0, y: 0 });
|
|
const cardRef = useRef<HTMLDivElement>(null);
|
|
|
|
// Client Profile Registration States
|
|
const [companyName, setCompanyName] = useState('');
|
|
const [website, setWebsite] = useState('');
|
|
const [sector, setSector] = useState('Technology');
|
|
const [companySize, setCompanySize] = useState('1-10');
|
|
|
|
/* 3D tilt effect on card */
|
|
const handleMouseMove = (e: React.MouseEvent<HTMLDivElement>) => {
|
|
if (!cardRef.current) return;
|
|
const rect = cardRef.current.getBoundingClientRect();
|
|
const x = (e.clientX - rect.left) / rect.width - 0.5;
|
|
const y = (e.clientY - rect.top) / rect.height - 0.5;
|
|
setMousePos({ x, y });
|
|
};
|
|
const handleMouseLeave = () => setMousePos({ x: 0, y: 0 });
|
|
|
|
const cardTransform = `perspective(1200px) rotateY(${mousePos.x * 8}deg) rotateX(${-mousePos.y * 6}deg) translateZ(0)`;
|
|
|
|
const handleSubmit = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
setError('');
|
|
setLoading(true);
|
|
if (!email) { setError('Email address is required.'); setLoading(false); return; }
|
|
try {
|
|
if (isRegister) {
|
|
if (!companyName.trim() || !website.trim()) {
|
|
setError('Please fill in all company information.');
|
|
setLoading(false);
|
|
return;
|
|
}
|
|
await register(email, { companyName, website, sector, companySize });
|
|
} else {
|
|
await login(email);
|
|
}
|
|
} catch (err: unknown) {
|
|
setError(err instanceof Error ? err.message : 'Authentication failed.');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleMfaSubmit = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
setError('');
|
|
setLoading(true);
|
|
if (otp.length !== 6) { setError('Enter the 6-digit verification code.'); setLoading(false); return; }
|
|
try {
|
|
const loggedUser = await verifyMfa(otp);
|
|
navigate(loggedUser.role === 'ADMIN' ? '/admin' : '/client');
|
|
} catch (err: unknown) {
|
|
setError(err instanceof Error ? err.message : 'MFA validation failed.');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const [mounted, setMounted] = useState(false);
|
|
useEffect(() => { setTimeout(() => setMounted(true), 50); }, []);
|
|
|
|
return (
|
|
<div className="relative min-h-screen flex items-center justify-center overflow-hidden p-4">
|
|
<AnimatedBackground />
|
|
|
|
{/* Card wrapper with 3D tilt */}
|
|
<div
|
|
ref={cardRef}
|
|
onMouseMove={handleMouseMove}
|
|
onMouseLeave={handleMouseLeave}
|
|
className="relative w-full max-w-md z-10"
|
|
style={{
|
|
transform: cardTransform,
|
|
transition: 'transform 0.15s cubic-bezier(0.16,1,0.3,1)',
|
|
transformStyle: 'preserve-3d',
|
|
opacity: mounted ? 1 : 0,
|
|
animation: mounted ? 'scale-up 0.6s cubic-bezier(0.16,1,0.3,1) forwards' : 'none',
|
|
}}
|
|
>
|
|
{/* Glow behind card */}
|
|
<div
|
|
className="absolute -inset-4 rounded-3xl opacity-40 blur-3xl"
|
|
style={{ background: 'linear-gradient(135deg, rgba(162,231,113,0.3), rgba(117,191,70,0.2))' }}
|
|
/>
|
|
|
|
{/* Main glass card */}
|
|
<div
|
|
className="relative rounded-3xl overflow-hidden"
|
|
style={{
|
|
background: 'rgba(255,255,255,0.85)',
|
|
backdropFilter: 'blur(40px) saturate(200%)',
|
|
WebkitBackdropFilter: 'blur(40px) saturate(200%)',
|
|
border: '1px solid rgba(255,255,255,0.9)',
|
|
boxShadow: `
|
|
0 30px 80px -10px rgba(35,43,33,0.15),
|
|
0 0 0 1px rgba(162,231,113,0.15),
|
|
inset 0 1px 0 rgba(255,255,255,0.9)
|
|
`,
|
|
}}
|
|
>
|
|
{/* Top accent bar */}
|
|
<div
|
|
className="h-1 w-full"
|
|
style={{ background: 'linear-gradient(90deg, var(--color-primary-400), var(--color-primary-600), var(--color-primary-400))' }}
|
|
/>
|
|
|
|
<div className="p-8 space-y-7">
|
|
{/* Brand Header */}
|
|
<div className="text-center space-y-4 animate-fade-in">
|
|
<OrbitingRing />
|
|
|
|
<div>
|
|
<h1 className="text-2xl font-extrabold tracking-tight" style={{ color: 'var(--color-ink-800)' }}>
|
|
{mfaPendingEmail ? 'Verify Identity' : 'Tech4Biz Portal'}
|
|
</h1>
|
|
<p className="text-sm mt-1.5" style={{ color: 'var(--color-ink-600)' }}>
|
|
{mfaPendingEmail
|
|
? `Code sent to ${mfaPendingEmail}`
|
|
: 'Enterprise hardware & software asset distribution'}
|
|
</p>
|
|
</div>
|
|
|
|
{/* Mode toggle */}
|
|
{!mfaPendingEmail && (
|
|
<div
|
|
className="inline-flex rounded-xl p-1 gap-1"
|
|
style={{ background: 'var(--color-ink-100)' }}
|
|
>
|
|
{(['Login', 'Sign Up'] as const).map(label => {
|
|
const active = label === 'Login' ? !isRegister : isRegister;
|
|
return (
|
|
<button
|
|
key={label}
|
|
type="button"
|
|
onClick={() => { setIsRegister(label === 'Sign Up'); setError(''); }}
|
|
className="px-5 py-1.5 rounded-lg text-xs font-bold transition-all duration-300"
|
|
style={{
|
|
background: active ? 'white' : 'transparent',
|
|
color: active ? 'var(--color-ink-800)' : 'var(--color-ink-500)',
|
|
boxShadow: active ? '0 1px 4px rgba(35,43,33,0.08)' : 'none',
|
|
}}
|
|
>
|
|
{label}
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Error message */}
|
|
{error && (
|
|
<div
|
|
className="flex items-center gap-2.5 rounded-xl px-4 py-3 text-sm font-medium animate-slide-up"
|
|
style={{
|
|
background: 'rgba(229,72,77,0.07)',
|
|
border: '1px solid rgba(229,72,77,0.2)',
|
|
color: 'var(--color-danger)',
|
|
}}
|
|
>
|
|
<div className="w-1.5 h-1.5 rounded-full bg-current shrink-0" />
|
|
{error}
|
|
</div>
|
|
)}
|
|
|
|
{/* ── Credentials Form ── */}
|
|
{!mfaPendingEmail ? (
|
|
<form onSubmit={handleSubmit} className="space-y-4">
|
|
{/* Email field */}
|
|
<div className="animate-slide-up" style={{ animationDelay: '0.05s' }}>
|
|
<label
|
|
className="block text-xs font-bold mb-2 uppercase tracking-widest"
|
|
style={{ color: 'var(--color-ink-600)' }}
|
|
>
|
|
Email Address
|
|
</label>
|
|
<div className="relative">
|
|
<div
|
|
className="absolute left-3.5 top-1/2 -translate-y-1/2 pointer-events-none"
|
|
style={{ color: 'var(--color-ink-400)' }}
|
|
>
|
|
<Mail className="w-4 h-4" />
|
|
</div>
|
|
<input
|
|
type="email"
|
|
value={email}
|
|
onChange={e => setEmail(e.target.value)}
|
|
placeholder="client@tech4biz.com"
|
|
className="input-field has-left-icon"
|
|
required
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Password field (mock) */}
|
|
<div className="animate-slide-up" style={{ animationDelay: '0.1s' }}>
|
|
<label
|
|
className="block text-xs font-bold mb-2 uppercase tracking-widest"
|
|
style={{ color: 'var(--color-ink-600)' }}
|
|
>
|
|
Password
|
|
</label>
|
|
<div className="relative">
|
|
<div
|
|
className="absolute left-3.5 top-1/2 -translate-y-1/2 pointer-events-none"
|
|
style={{ color: 'var(--color-ink-400)' }}
|
|
>
|
|
<Lock className="w-4 h-4" />
|
|
</div>
|
|
<input
|
|
type={showPass ? 'text' : 'password'}
|
|
defaultValue="password"
|
|
readOnly
|
|
className="input-field has-left-icon has-right-icon"
|
|
style={{ background: 'var(--color-ink-50)', cursor: 'default' }}
|
|
/>
|
|
<button
|
|
type="button"
|
|
onClick={() => setShowPass(v => !v)}
|
|
className="absolute right-3 top-1/2 -translate-y-1/2 transition-premium"
|
|
style={{ color: 'var(--color-ink-400)' }}
|
|
>
|
|
{showPass ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
|
|
</button>
|
|
</div>
|
|
<p className="mt-1.5 text-xs" style={{ color: 'var(--color-ink-500)' }}>
|
|
Mock password preset:{' '}
|
|
<code
|
|
className="px-1.5 py-0.5 rounded font-mono text-xs font-bold"
|
|
style={{ background: 'var(--color-primary-50)', color: 'var(--color-primary-800)' }}
|
|
>
|
|
password
|
|
</code>
|
|
</p>
|
|
</div>
|
|
|
|
{/* Register Profile Fields */}
|
|
{isRegister && (
|
|
<>
|
|
<div className="animate-slide-up" style={{ animationDelay: '0.12s' }}>
|
|
<label
|
|
className="block text-xs font-bold mb-2 uppercase tracking-widest"
|
|
style={{ color: 'var(--color-ink-600)' }}
|
|
>
|
|
Company Name
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={companyName}
|
|
onChange={e => setCompanyName(e.target.value)}
|
|
placeholder="e.g. Acme Corporation"
|
|
className="input-field"
|
|
required
|
|
/>
|
|
</div>
|
|
|
|
<div className="animate-slide-up" style={{ animationDelay: '0.14s' }}>
|
|
<label
|
|
className="block text-xs font-bold mb-2 uppercase tracking-widest"
|
|
style={{ color: 'var(--color-ink-600)' }}
|
|
>
|
|
Corporate Website
|
|
</label>
|
|
<input
|
|
type="url"
|
|
value={website}
|
|
onChange={e => setWebsite(e.target.value)}
|
|
placeholder="https://example.com"
|
|
className="input-field"
|
|
required
|
|
/>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div className="animate-slide-up" style={{ animationDelay: '0.16s' }}>
|
|
<label
|
|
className="block text-xs font-bold mb-2 uppercase tracking-widest"
|
|
style={{ color: 'var(--color-ink-600)' }}
|
|
>
|
|
Sector
|
|
</label>
|
|
<select
|
|
value={sector}
|
|
onChange={e => setSector(e.target.value)}
|
|
className="input-field bg-white"
|
|
>
|
|
<option value="Technology">Technology</option>
|
|
<option value="Automotive">Automotive</option>
|
|
<option value="Telecommunications">Telecommunications</option>
|
|
<option value="Defense">Defense</option>
|
|
<option value="Semiconductors">Semiconductors</option>
|
|
<option value="Other">Other</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div className="animate-slide-up" style={{ animationDelay: '0.16s' }}>
|
|
<label
|
|
className="block text-xs font-bold mb-2 uppercase tracking-widest"
|
|
style={{ color: 'var(--color-ink-600)' }}
|
|
>
|
|
Company Size
|
|
</label>
|
|
<select
|
|
value={companySize}
|
|
onChange={e => setCompanySize(e.target.value)}
|
|
className="input-field bg-white"
|
|
>
|
|
<option value="1-10">1-10</option>
|
|
<option value="10-50">10-50</option>
|
|
<option value="50-250">50-250</option>
|
|
<option value="250-1000">250-1000</option>
|
|
<option value="1000+">1000+</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
</>
|
|
)}
|
|
|
|
{/* Quick fill hints */}
|
|
{!isRegister && (
|
|
<div
|
|
className="rounded-xl p-3 animate-slide-up"
|
|
style={{ background: 'var(--color-ink-50)', border: '1px solid var(--color-ink-100)', animationDelay: '0.15s' }}
|
|
>
|
|
<p className="text-xs font-bold mb-2" style={{ color: 'var(--color-ink-600)' }}>
|
|
Quick Access Accounts
|
|
</p>
|
|
<div className="flex flex-wrap gap-2">
|
|
{[
|
|
{ label: 'Admin', email: 'admin@tech4biz.com' },
|
|
{ label: 'Client', email: 'client@tech4biz.com' },
|
|
].map(acc => (
|
|
<button
|
|
key={acc.label}
|
|
type="button"
|
|
onClick={() => setEmail(acc.email)}
|
|
className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-semibold transition-premium"
|
|
style={{
|
|
background: email === acc.email ? 'var(--color-primary-100)' : 'white',
|
|
color: email === acc.email ? 'var(--color-primary-800)' : 'var(--color-ink-600)',
|
|
border: `1px solid ${email === acc.email ? 'var(--color-primary-200)' : 'var(--color-ink-200)'}`,
|
|
}}
|
|
>
|
|
<Sparkles className="w-3 h-3" />
|
|
{acc.label}
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Submit */}
|
|
<button
|
|
type="submit"
|
|
disabled={loading}
|
|
className="btn-primary w-full py-3 text-sm flex items-center justify-center gap-2 animate-slide-up"
|
|
style={{ animationDelay: '0.2s' }}
|
|
>
|
|
{loading ? (
|
|
<>
|
|
<RefreshCw className="w-4 h-4 animate-spin-slow" />
|
|
Processing…
|
|
</>
|
|
) : (
|
|
<>
|
|
{isRegister ? 'Create Account' : 'Authenticate'} <ArrowRight className="w-4 h-4" />
|
|
</>
|
|
)}
|
|
</button>
|
|
</form>
|
|
) : (
|
|
/* ── MFA Form ── */
|
|
<form onSubmit={handleMfaSubmit} className="space-y-5">
|
|
<div
|
|
className="text-center p-4 rounded-2xl animate-fade-in"
|
|
style={{ background: 'var(--color-primary-50)', border: '1px solid var(--color-primary-100)' }}
|
|
>
|
|
<p className="text-xs font-bold mb-1" style={{ color: 'var(--color-primary-700)' }}>
|
|
Development Mode
|
|
</p>
|
|
<p className="text-sm" style={{ color: 'var(--color-ink-700)' }}>
|
|
Use code{' '}
|
|
<code
|
|
className="px-2 py-0.5 rounded font-mono font-bold text-base"
|
|
style={{ background: 'var(--color-primary-100)', color: 'var(--color-primary-800)' }}
|
|
>
|
|
123456
|
|
</code>
|
|
</p>
|
|
</div>
|
|
|
|
{/* OTP input */}
|
|
<div>
|
|
<label className="block text-xs font-bold mb-3 text-center uppercase tracking-widest" style={{ color: 'var(--color-ink-600)' }}>
|
|
Verification Code
|
|
</label>
|
|
<input
|
|
type="text"
|
|
inputMode="numeric"
|
|
maxLength={6}
|
|
value={otp}
|
|
onChange={e => setOtp(e.target.value.replace(/\D/g, ''))}
|
|
placeholder="000000"
|
|
className="input-field text-center font-mono text-2xl tracking-[0.5em] py-4"
|
|
required
|
|
autoFocus
|
|
/>
|
|
</div>
|
|
|
|
<button
|
|
type="submit"
|
|
disabled={loading || otp.length !== 6}
|
|
className="btn-primary w-full py-3 text-sm flex items-center justify-center gap-2"
|
|
>
|
|
{loading ? (
|
|
<>
|
|
<RefreshCw className="w-4 h-4 animate-spin-slow" />
|
|
Verifying…
|
|
</>
|
|
) : (
|
|
<>
|
|
<ShieldCheck className="w-4 h-4" />
|
|
Verify Code
|
|
</>
|
|
)}
|
|
</button>
|
|
</form>
|
|
)}
|
|
</div>
|
|
|
|
{/* Bottom accent */}
|
|
<div
|
|
className="px-8 py-4 text-center"
|
|
style={{ borderTop: '1px solid var(--color-ink-50)', background: 'rgba(247,249,246,0.5)' }}
|
|
>
|
|
<p className="text-xs" style={{ color: 'var(--color-ink-400)' }}>
|
|
© 2026 Tech4Biz Solutions Inc. · Enterprise Security Portal
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|