ca-grow-ops-manager/frontend/src/pages/LoginPage.tsx
fullsizemalt 4fd7aed250
Some checks are pending
Deploy to Production / deploy (push) Waiting to run
Test / backend-test (push) Waiting to run
Test / frontend-test (push) Waiting to run
feat(theme): Universal Ersen OS Refactor
- Implemented global page transitions (Framer Motion)
- Unified Data Views with new high-density DataTable primitive
- Refactored Navbar and Layout for 'Ersen OS' branding
- Modernized Login Page with premium split-screen design
- Upgraded System Primitives for consistent operational aesthetics
2025-12-19 19:01:09 -08:00

183 lines
11 KiB
TypeScript

import React, { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { motion, AnimatePresence } from 'framer-motion';
import { Shield, ArrowRight, Loader2, Key, ChevronRight, Lock } from 'lucide-react';
import api from '../lib/api';
import { useAuth } from '../context/AuthContext';
import { DevTools } from '../components/dev/DevTools';
import { pageVariants, itemVariants } from '../lib/animations';
export default function LoginPage() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState('');
const [isLoading, setIsLoading] = useState(false);
const { login } = useAuth();
const navigate = useNavigate();
useEffect(() => {
document.title = 'Ersen OS | Authentication';
}, []);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError('');
setIsLoading(true);
try {
const { data } = await api.post('/auth/login', { email, password });
login(data.accessToken, data.refreshToken, data.user);
navigate('/dashboard');
} catch (err: any) {
setError(err.response?.data?.message || 'Authentication failed. Please check your credentials.');
} finally {
setIsLoading(false);
}
};
return (
<div className="min-h-screen bg-[#050505] text-slate-100 flex overflow-hidden font-sans selection:bg-indigo-500/30">
{/* Left Side: Brand/Visual */}
<div className="hidden lg:flex flex-1 relative items-center justify-center border-r border-slate-800/50 bg-[radial-gradient(circle_at_center,_var(--tw-gradient-stops))] from-slate-900 via-[#050505] to-[#050505]">
<div className="absolute inset-0 opacity-20" style={{ backgroundImage: 'radial-gradient(#1e293b 1px, transparent 1px)', backgroundSize: '32px 32px' }} />
<motion.div
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 1, ease: "easeOut" }}
className="relative z-10 flex flex-col items-center text-center p-12 space-y-8"
>
<div className="w-24 h-24 rounded-3xl bg-indigo-500 flex items-center justify-center shadow-2xl shadow-indigo-500/20 rotate-3">
<Lock className="text-white -rotate-3" size={48} strokeWidth={2.5} />
</div>
<div className="space-y-2">
<h2 className="text-5xl font-bold tracking-tighter bg-gradient-to-b from-white to-slate-500 bg-clip-text text-transparent">
ERSEN OS
</h2>
<p className="text-slate-500 font-mono text-sm tracking-[0.3em] uppercase">
Operational Infrastructure
</p>
</div>
<div className="flex items-center gap-6 mt-12 bg-slate-900/50 backdrop-blur-md px-6 py-3 rounded-full border border-slate-800/50 shadow-xl">
<div className="flex -space-x-3">
{[1, 2, 3].map(i => (
<div key={i} className="w-8 h-8 rounded-full border-2 border-[#050505] bg-slate-800" />
))}
</div>
<p className="text-xs text-slate-400 font-medium">Security Verified Nodes Active</p>
<div className="w-2 h-2 rounded-full bg-emerald-500 animate-pulse" />
</div>
</motion.div>
{/* Decorative elements */}
<div className="absolute bottom-12 left-12 text-[10px] font-mono text-slate-700 tracking-tighter uppercase leading-none">
Distributed ledger validated<br />
Secure tunnel engaged<br />
v4.2.0-stable
</div>
</div>
{/* Right Side: Form */}
<div className="flex-1 flex flex-col items-center justify-center p-6 lg:p-24 relative">
<motion.div
variants={pageVariants}
initial="initial"
animate="animate"
className="w-full max-w-sm space-y-12"
>
<div className="space-y-3">
<motion.div variants={itemVariants} className="lg:hidden w-12 h-12 rounded-xl bg-indigo-500 flex items-center justify-center mb-8 shadow-lg shadow-indigo-500/20">
<Lock className="text-white" size={24} />
</motion.div>
<motion.h1 variants={itemVariants} className="text-3xl font-bold tracking-tight text-white lg:text-4xl">
Authenticate
</motion.h1>
<motion.p variants={itemVariants} className="text-slate-400 text-sm">
Access your mission-critical operations portal.
</motion.p>
</div>
<motion.form variants={itemVariants} onSubmit={handleSubmit} className="space-y-6">
<AnimatePresence mode="wait">
{error && (
<motion.div
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: 'auto' }}
exit={{ opacity: 0, height: 0 }}
className="p-4 bg-rose-500/10 border border-rose-500/20 rounded-xl flex items-start gap-3"
>
<Shield className="text-rose-500 flex-shrink-0 mt-0.5" size={16} />
<p className="text-sm text-rose-500 leading-snug">{error}</p>
</motion.div>
)}
</AnimatePresence>
<div className="space-y-4">
<div className="space-y-2">
<label className="text-xs font-bold text-slate-500 uppercase tracking-widest ml-1">Secure Email</label>
<div className="relative group">
<input
type="email"
required
className="w-full bg-slate-900/50 border border-slate-800 rounded-xl px-4 py-3.5 text-sm focus:outline-none focus:ring-2 focus:ring-indigo-500/40 focus:border-indigo-500 transition-all placeholder:text-slate-600"
placeholder="terminal@ersen.xyz"
value={email}
onChange={(e) => setEmail(e.target.value)}
disabled={isLoading}
/>
<div className="absolute inset-0 rounded-xl bg-indigo-500/5 opacity-0 group-hover:opacity-100 pointer-events-none transition-opacity" />
</div>
</div>
<div className="space-y-2">
<label className="text-xs font-bold text-slate-500 uppercase tracking-widest ml-1">Access Key</label>
<div className="relative group">
<input
type="password"
required
className="w-full bg-slate-900/50 border border-slate-800 rounded-xl px-4 py-3.5 text-sm focus:outline-none focus:ring-2 focus:ring-indigo-500/40 focus:border-indigo-500 transition-all placeholder:text-slate-600"
placeholder="••••••••••••"
value={password}
onChange={(e) => setPassword(e.target.value)}
disabled={isLoading}
/>
<div className="absolute inset-0 rounded-xl bg-indigo-500/5 opacity-0 group-hover:opacity-100 pointer-events-none transition-opacity" />
</div>
</div>
</div>
<button
type="submit"
disabled={isLoading}
className="group relative w-full bg-indigo-600 hover:bg-indigo-500 text-white rounded-xl h-14 font-bold tracking-tight shadow-xl shadow-indigo-600/10 transition-all active:scale-[0.98] disabled:opacity-50 flex items-center justify-center overflow-hidden"
>
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-white/10 to-transparent -translate-x-[100%] group-hover:translate-x-[100%] transition-transform duration-1000" />
{isLoading ? (
<Loader2 className="animate-spin" size={20} />
) : (
<div className="flex items-center gap-2">
<span>Initiate Protocol</span>
<ChevronRight size={18} className="group-hover:translate-x-1 transition-transform" />
</div>
)}
</button>
</motion.form>
<motion.div variants={itemVariants} className="pt-24 border-t border-slate-800 space-y-4">
<div className="flex items-center justify-between">
<p className="text-[10px] font-bold text-slate-600 uppercase tracking-widest">Global Security</p>
<div className="h-px flex-1 bg-slate-800 mx-4" />
<Shield className="text-slate-700" size={14} />
</div>
<p className="text-[11px] text-slate-500 leading-relaxed text-center">
This system is for the exclusive use of authorized Ersen personnel. All activity is logged and monitored for compliance integrity.
</p>
</motion.div>
</motion.div>
</div>
<DevTools />
</div>
);
}