Tech4biz-channel/Channel-Frontend/src/features/auth/components/LoginForm.tsx

593 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 bg-white border border-ink-200 shadow-sm p-2"
style={{
transform: 'perspective(300px) rotateY(-8deg) rotateX(4deg)',
}}
>
<img src="/logo.png" alt="Tech4Biz" className="w-full h-full object-contain" />
</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 login-form-title font-extrabold tracking-tight" style={{ color: 'var(--color-ink-800)' }}>
{mfaPendingEmail ? 'Verify Identity' : 'Tech4Biz Portal'}
</h1>
<p className="text-sm login-form-subtitle 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>
);
};