Spaces:
Sleeping
Sleeping
| 'use client'; | |
| import { useState } from 'react'; | |
| import { useRouter } from 'next/navigation'; | |
| import Link from 'next/link'; | |
| import { motion, AnimatePresence } from 'framer-motion'; | |
| import { login as loginAPI } from '@/lib/api'; | |
| import { showToast } from '@/lib/toast'; | |
| import { Eye, EyeOff, Loader2, Mail, Lock, ChevronRight } from 'lucide-react'; | |
| export default function LoginForm() { | |
| const [email, setEmail] = useState(''); | |
| const [password, setPassword] = useState(''); | |
| const [rememberMe, setRememberMe] = useState(() => { | |
| if (typeof window !== 'undefined') { | |
| return localStorage.getItem('rememberMe') === 'true'; | |
| } | |
| return false; | |
| }); | |
| const [showPassword, setShowPassword] = useState(false); | |
| const [error, setError] = useState(''); | |
| const [loading, setLoading] = useState(false); | |
| const router = useRouter(); | |
| const handleSubmit = async (e: React.FormEvent) => { | |
| e.preventDefault(); | |
| setError(''); | |
| setLoading(true); | |
| try { | |
| const loginResponse = await loginAPI({ email, password }); | |
| if (loginResponse.user) { | |
| localStorage.setItem('user', JSON.stringify(loginResponse.user)); | |
| if (rememberMe) { | |
| localStorage.setItem('rememberMe', 'true'); | |
| } else { | |
| localStorage.removeItem('rememberMe'); | |
| } | |
| } | |
| if (loginResponse.access_token) { | |
| localStorage.setItem('auth_token', loginResponse.access_token); | |
| } | |
| showToast.success('Neural Link Established'); | |
| setTimeout(() => { | |
| router.push('/tasks'); | |
| }, 100); | |
| } catch (err: any) { | |
| setError(err.message || 'Identity verification failed. Access denied.'); | |
| } finally { | |
| setLoading(false); | |
| } | |
| }; | |
| return ( | |
| <form className="space-y-6" onSubmit={handleSubmit}> | |
| <AnimatePresence> | |
| {error && ( | |
| <motion.div | |
| initial={{ opacity: 0, y: -10 }} | |
| animate={{ opacity: 1, y: 0 }} | |
| exit={{ opacity: 0, y: -10 }} | |
| className="p-4 rounded-2xl bg-red-500/10 border border-red-500/20 flex items-center gap-3" | |
| > | |
| <div className="w-1.5 h-1.5 rounded-full bg-red-500 animate-pulse" /> | |
| <p className="text-xs font-bold text-red-400 uppercase tracking-widest">{error}</p> | |
| </motion.div> | |
| )} | |
| </AnimatePresence> | |
| <div className="space-y-4"> | |
| <div className="space-y-2"> | |
| <label className="text-[10px] font-black text-white/40 uppercase tracking-[0.2em] ml-1">Email</label> | |
| <div className="relative group/input"> | |
| <div className="absolute inset-y-0 left-4 flex items-center pointer-events-none text-slate-500 group-focus-within/input:text-indigo-400 transition-colors"> | |
| <Mail className="h-4 w-4" /> | |
| </div> | |
| <input | |
| type="email" | |
| required | |
| value={email} | |
| onChange={(e) => setEmail(e.target.value)} | |
| className="w-full bg-white/[0.03] border border-white/5 rounded-2xl py-4 pl-12 pr-4 text-white placeholder-white/10 focus:outline-none focus:border-indigo-500/50 focus:bg-white/[0.05] transition-all font-bold" | |
| placeholder="operator@matrix.net" | |
| /> | |
| </div> | |
| </div> | |
| <div className="space-y-2"> | |
| <div className="flex justify-between items-center px-1"> | |
| <label className="text-[10px] font-black text-white/40 uppercase tracking-[0.2em]">Password</label> | |
| <Link href="/forgot-password" title="Recover Access Cipher" className="text-[10px] font-black text-indigo-400 uppercase tracking-widest hover:text-indigo-300 transition-colors">Forgot Password?</Link> | |
| </div> | |
| <div className="relative group/input"> | |
| <div className="absolute inset-y-0 left-4 flex items-center pointer-events-none text-slate-500 group-focus-within/input:text-indigo-400 transition-colors"> | |
| <Lock className="h-4 w-4" /> | |
| </div> | |
| <input | |
| type={showPassword ? "text" : "password"} | |
| required | |
| value={password} | |
| onChange={(e) => setPassword(e.target.value)} | |
| className="w-full bg-white/[0.03] border border-white/5 rounded-2xl py-4 pl-12 pr-12 text-white placeholder-white/10 focus:outline-none focus:border-indigo-500/50 focus:bg-white/[0.05] transition-all font-bold" | |
| placeholder="••••••••" | |
| /> | |
| <button | |
| type="button" | |
| onClick={() => setShowPassword(!showPassword)} | |
| className="absolute inset-y-0 right-4 flex items-center text-slate-500 hover:text-white transition-colors" | |
| > | |
| {showPassword ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />} | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| <div className="flex items-center justify-between px-1"> | |
| <label className="flex items-center gap-2 cursor-pointer group"> | |
| <div className="relative"> | |
| <input | |
| type="checkbox" | |
| className="sr-only" | |
| checked={rememberMe} | |
| onChange={(e) => setRememberMe(e.target.checked)} | |
| /> | |
| <div className={`w-4 h-4 rounded border transition-all ${rememberMe ? 'bg-indigo-500 border-indigo-500' : 'bg-white/5 border-white/10 group-hover:border-white/20'}`}> | |
| {rememberMe && ( | |
| <svg className="w-full h-full text-white p-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |
| <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={4} d="M5 13l4 4L19 7" /> | |
| </svg> | |
| )} | |
| </div> | |
| </div> | |
| <span className="text-[10px] font-black text-white/40 uppercase tracking-widest group-hover:text-white/60 transition-colors">Remember Me</span> | |
| </label> | |
| </div> | |
| <motion.button | |
| whileHover={{ scale: 1.02 }} | |
| whileTap={{ scale: 0.98 }} | |
| disabled={loading} | |
| className="w-full py-5 bg-white text-black font-black rounded-2xl shadow-[0_20px_40px_-15px_rgba(255,255,255,0.3)] hover:bg-indigo-50 transition-all flex items-center justify-center gap-3 disabled:opacity-50 group uppercase tracking-widest text-xs" | |
| > | |
| {loading ? ( | |
| <Loader2 className="h-5 w-5 animate-spin" /> | |
| ) : ( | |
| <> | |
| LOGIN | |
| <ChevronRight className="h-4 w-4 group-hover:translate-x-1 transition-transform" /> | |
| </> | |
| )} | |
| </motion.button> | |
| </form> | |
| ); | |
| } |