Spaces:
Sleeping
Sleeping
| import React, { useState } from "react"; | |
| import { motion } from "framer-motion"; | |
| import { Button } from "@/components/ui/button"; | |
| import { Input } from "@/components/ui/input"; | |
| import { Separator } from "@/components/ui/separator"; | |
| import { | |
| Zap, | |
| Target, | |
| Upload, | |
| CheckCircle2, | |
| ArrowRight, | |
| Mail, | |
| Sparkles, | |
| Shield, | |
| Globe, | |
| AlertCircle, | |
| Loader2, | |
| } from "lucide-react"; | |
| import { useAuth } from "@/contexts/AuthContext"; | |
| export default function LoginForm() { | |
| const { firebaseLogin, requestOTP, verifyOTP } = useAuth(); | |
| const [email, setEmail] = useState(""); | |
| const [showOtp, setShowOtp] = useState(false); | |
| const [otp, setOtp] = useState(["", "", "", "", "", ""]); | |
| const [loading, setLoading] = useState(false); | |
| const [error, setError] = useState(""); | |
| // Business email validation | |
| const PERSONAL_EMAIL_DOMAINS = [ | |
| "gmail.com", | |
| "yahoo.com", | |
| "hotmail.com", | |
| "outlook.com", | |
| "aol.com", | |
| "icloud.com", | |
| "mail.com", | |
| "protonmail.com", | |
| "yandex.com", | |
| "zoho.com", | |
| "gmx.com", | |
| "live.com", | |
| "msn.com", | |
| ]; | |
| const isBusinessEmail = (email) => { | |
| if (!email || !email.includes("@")) return false; | |
| const domain = email.split("@")[1].toLowerCase(); | |
| return !PERSONAL_EMAIL_DOMAINS.includes(domain); | |
| }; | |
| const handleGoogleLogin = async () => { | |
| setLoading(true); | |
| setError(""); | |
| try { | |
| await firebaseLogin(); | |
| } catch (err) { | |
| setError(err.message || "Failed to sign in with Google"); | |
| } finally { | |
| setLoading(false); | |
| } | |
| }; | |
| const handleEmailSubmit = async (e) => { | |
| e.preventDefault(); | |
| setLoading(true); | |
| setError(""); | |
| if (!email) { | |
| setError("Please enter your email address"); | |
| setLoading(false); | |
| return; | |
| } | |
| if (!isBusinessEmail(email)) { | |
| setError("Only business email addresses are allowed. Personal email accounts (Gmail, Yahoo, etc.) are not permitted."); | |
| setLoading(false); | |
| return; | |
| } | |
| try { | |
| await requestOTP(email); | |
| setShowOtp(true); | |
| } catch (err) { | |
| setError(err.message || "Failed to send OTP"); | |
| } finally { | |
| setLoading(false); | |
| } | |
| }; | |
| const handleOtpChange = (index, value) => { | |
| if (value.length <= 1 && /^\d*$/.test(value)) { | |
| const newOtp = [...otp]; | |
| newOtp[index] = value; | |
| setOtp(newOtp); | |
| setError(""); | |
| // Auto-focus next input | |
| if (value && index < 5) { | |
| const nextInput = document.getElementById(`otp-${index + 1}`); | |
| nextInput?.focus(); | |
| } | |
| } | |
| }; | |
| const handleOtpPaste = (e, startIndex = 0) => { | |
| e.preventDefault(); | |
| const pastedData = e.clipboardData.getData("text"); | |
| // Extract only digits from pasted content | |
| const digits = pastedData.replace(/\D/g, "").slice(0, 6); | |
| if (digits.length > 0) { | |
| const newOtp = [...otp]; | |
| // Fill the OTP array with pasted digits starting from the current field | |
| for (let i = 0; i < digits.length && (startIndex + i) < 6; i++) { | |
| newOtp[startIndex + i] = digits[i]; | |
| } | |
| setOtp(newOtp); | |
| setError(""); | |
| // Focus on the next empty input or the last input if all are filled | |
| const nextEmptyIndex = Math.min(startIndex + digits.length, 5); | |
| const nextInput = document.getElementById(`otp-${nextEmptyIndex}`); | |
| nextInput?.focus(); | |
| } | |
| }; | |
| const handleOtpKeyDown = (index, e) => { | |
| if (e.key === "Backspace" && !otp[index] && index > 0) { | |
| const prevInput = document.getElementById(`otp-${index - 1}`); | |
| prevInput?.focus(); | |
| } | |
| }; | |
| const handleOtpVerify = async (e) => { | |
| e.preventDefault(); | |
| setLoading(true); | |
| setError(""); | |
| const otpString = otp.join(""); | |
| if (otpString.length !== 6) { | |
| setError("Please enter a valid 6-digit OTP"); | |
| setLoading(false); | |
| return; | |
| } | |
| try { | |
| await verifyOTP(email, otpString); | |
| // Success - user will be redirected by AuthContext | |
| } catch (err) { | |
| setError(err.message || "Invalid OTP. Please try again."); | |
| setOtp(["", "", "", "", "", ""]); | |
| } finally { | |
| setLoading(false); | |
| } | |
| }; | |
| const features = [ | |
| { | |
| icon: Zap, | |
| title: "Lightning Fast", | |
| description: "Process documents in seconds and get outputs for ERP ingestion", | |
| color: "text-amber-500", | |
| bg: "bg-amber-50", | |
| }, | |
| { | |
| icon: Target, | |
| title: "100% Accuracy", | |
| description: "Industry-leading extraction with Visual Reasoning Processor", | |
| color: "text-emerald-500", | |
| bg: "bg-emerald-50", | |
| }, | |
| { | |
| icon: Globe, | |
| title: "Any Format, Any Language", | |
| description: "PDF, images, scanned docs — multi-lingual support included", | |
| color: "text-blue-500", | |
| bg: "bg-blue-50", | |
| }, | |
| ]; | |
| const supportedFormats = [ | |
| { ext: "PDF", color: "bg-red-500" }, | |
| { ext: "PNG", color: "bg-blue-500" }, | |
| { ext: "JPG", color: "bg-green-500" }, | |
| { ext: "TIFF", color: "bg-purple-500" }, | |
| ]; | |
| return ( | |
| <div className="min-h-screen bg-gradient-to-br from-slate-50 via-white to-blue-50 flex"> | |
| {/* Left Side - Product Showcase */} | |
| <div className="hidden lg:flex lg:w-[56%] flex-col justify-between p-8 relative overflow-hidden"> | |
| {/* Background Elements */} | |
| <div className="absolute top-0 right-0 w-96 h-96 bg-blue-100/40 rounded-full blur-3xl -translate-y-1/2 translate-x-1/2" /> | |
| <div className="absolute bottom-0 left-0 w-80 h-80 bg-emerald-100/40 rounded-full blur-3xl translate-y-1/2 -translate-x-1/2" /> | |
| {/* Logo & Brand */} | |
| <motion.div | |
| initial={{ opacity: 0, y: -20 }} | |
| animate={{ opacity: 1, y: 0 }} | |
| className="relative z-10 mb-6" | |
| > | |
| <div className="flex items-center gap-3"> | |
| <div className="h-12 w-12 flex items-center justify-center flex-shrink-0"> | |
| <img | |
| src="/logo.png" | |
| alt="EZOFIS AI Logo" | |
| className="h-full w-full object-contain" | |
| onError={(e) => { | |
| // Fallback: hide image if logo not found | |
| e.target.style.display = 'none'; | |
| }} | |
| /> | |
| </div> | |
| <div> | |
| <h1 className="text-2xl font-bold text-slate-900 tracking-tight">EZOFISOCR</h1> | |
| <p className="text-sm text-slate-500 font-medium">VRP Intelligence</p> | |
| </div> | |
| </div> | |
| </motion.div> | |
| {/* Main Content */} | |
| <motion.div | |
| initial={{ opacity: 0, y: 20 }} | |
| animate={{ opacity: 1, y: 0 }} | |
| transition={{ delay: 0.1 }} | |
| className="relative z-10 space-y-5 flex-1 flex flex-col justify-center ml-24 xl:ml-36" | |
| > | |
| <div className="space-y-3"> | |
| <h2 className="text-3xl xl:text-4xl font-bold text-slate-900 leading-tight"> | |
| Pure Agentic | |
| <span className="block text-transparent bg-clip-text bg-gradient-to-r from-blue-600 to-indigo-600"> | |
| Document Intelligence | |
| </span> | |
| </h2> | |
| <p className="text-base text-slate-600 max-w-lg leading-relaxed"> | |
| Deterministic, layout-aware extraction (without LLM) using our proprietary{" "} | |
| <span className="font-semibold text-slate-800">Visual Reasoning Processor (VRP)</span> | |
| </p> | |
| </div> | |
| {/* Product Preview Card */} | |
| <motion.div | |
| initial={{ opacity: 0, scale: 0.95 }} | |
| animate={{ opacity: 1, scale: 1 }} | |
| transition={{ delay: 0.3 }} | |
| className="bg-white rounded-2xl border border-slate-200/80 shadow-xl shadow-slate-200/50 p-4 max-w-lg" | |
| > | |
| <div className="border-2 border-dashed border-slate-200 rounded-xl p-5 text-center bg-slate-50/50"> | |
| <div className="w-12 h-12 rounded-full bg-slate-100 flex items-center justify-center mx-auto mb-3"> | |
| <Upload className="w-5 h-5 text-slate-400" /> | |
| </div> | |
| <p className="text-slate-700 font-medium mb-1 text-sm">Drop a document to extract data</p> | |
| <p className="text-xs text-slate-400">Invoices, purchase orders, delivery notes, receipts, and operational documents</p> | |
| <div className="flex items-center justify-center gap-2 mt-3"> | |
| {supportedFormats.map((format, i) => ( | |
| <span key={i} className={`${format.color} text-white text-xs font-bold px-2 py-1 rounded`}> | |
| {format.ext} | |
| </span> | |
| ))} | |
| </div> | |
| </div> | |
| <div className="flex items-center justify-between mt-3 pt-3 border-t border-slate-100"> | |
| <div className="flex items-center gap-2"> | |
| <div className="w-2 h-2 rounded-full bg-emerald-500 animate-pulse" /> | |
| <span className="text-xs text-slate-600">Ready to extract</span> | |
| </div> | |
| <div className="flex items-center gap-1 text-emerald-600"> | |
| <CheckCircle2 className="w-3.5 h-3.5" /> | |
| <span className="text-xs font-semibold">99.8% Accuracy</span> | |
| </div> | |
| </div> | |
| </motion.div> | |
| {/* Features */} | |
| <div className="grid gap-3"> | |
| {features.map((feature, index) => ( | |
| <motion.div | |
| key={feature.title} | |
| initial={{ opacity: 0, x: -20 }} | |
| animate={{ opacity: 1, x: 0 }} | |
| transition={{ delay: 0.4 + index * 0.1 }} | |
| className="flex items-start gap-3 group" | |
| > | |
| <div | |
| className={`w-9 h-9 rounded-xl ${feature.bg} flex items-center justify-center flex-shrink-0 group-hover:scale-110 transition-transform`} | |
| > | |
| <feature.icon className={`w-4 h-4 ${feature.color}`} /> | |
| </div> | |
| <div> | |
| <h3 className="font-semibold text-slate-900 text-sm">{feature.title}</h3> | |
| <p className="text-xs text-slate-500">{feature.description}</p> | |
| </div> | |
| </motion.div> | |
| ))} | |
| </div> | |
| </motion.div> | |
| {/* Trust Badge */} | |
| <motion.div | |
| initial={{ opacity: 0 }} | |
| animate={{ opacity: 1 }} | |
| transition={{ delay: 0.6 }} | |
| className="relative z-10 flex items-center gap-3 text-xs text-slate-500 mt-6" | |
| > | |
| <Shield className="w-4 h-4" /> | |
| <span>Enterprise-grade security • SOC 2 Compliant • GDPR Ready</span> | |
| </motion.div> | |
| </div> | |
| {/* Right Side - Sign In Form */} | |
| <div className="w-full lg:w-[44%] flex items-center justify-center p-6 sm:p-10"> | |
| <motion.div | |
| initial={{ opacity: 0, y: 20 }} | |
| animate={{ opacity: 1, y: 0 }} | |
| transition={{ delay: 0.2 }} | |
| className="w-full max-w-md" | |
| > | |
| {/* Mobile Logo */} | |
| <div className="lg:hidden flex items-center justify-center gap-3 mb-8"> | |
| <div className="h-12 w-12 flex items-center justify-center flex-shrink-0"> | |
| <img | |
| src="/logo.png" | |
| alt="EZOFIS AI Logo" | |
| className="h-full w-full object-contain" | |
| onError={(e) => { | |
| // Fallback: hide image if logo not found | |
| e.target.style.display = 'none'; | |
| }} | |
| /> | |
| </div> | |
| <div> | |
| <h1 className="text-2xl font-bold text-slate-900 tracking-tight">EZOFISOCR</h1> | |
| <p className="text-sm text-slate-500 font-medium">VRP Intelligence</p> | |
| </div> | |
| </div> | |
| <div className="bg-white rounded-3xl border border-slate-200/80 shadow-2xl shadow-slate-200/50 p-8 sm:p-10"> | |
| <div className="text-center mb-8"> | |
| <h2 className="text-2xl font-bold text-slate-900 mb-2"> | |
| {showOtp ? "Enter verification code" : "Secure Access"} | |
| </h2> | |
| <p className="text-slate-500"> | |
| {showOtp ? `We sent a code to ${email}` : "Access your document intelligence workspace"} | |
| </p> | |
| </div> | |
| {/* Error Message */} | |
| {error && ( | |
| <motion.div | |
| initial={{ opacity: 0, y: -10 }} | |
| animate={{ opacity: 1, y: 0 }} | |
| className="mb-6 p-3 bg-red-50 border border-red-200 rounded-xl flex items-start gap-2 text-sm text-red-700" | |
| > | |
| <AlertCircle className="h-4 w-4 flex-shrink-0 mt-0.5" /> | |
| <p>{error}</p> | |
| </motion.div> | |
| )} | |
| {!showOtp ? ( | |
| <> | |
| {/* Google Sign In */} | |
| <Button | |
| onClick={handleGoogleLogin} | |
| disabled={loading} | |
| variant="outline" | |
| className="w-full h-12 text-base font-medium border-slate-200 hover:bg-slate-50 hover:border-slate-300 transition-all group" | |
| > | |
| {loading ? ( | |
| <Loader2 className="w-5 h-5 mr-3 animate-spin" /> | |
| ) : ( | |
| <svg className="w-5 h-5 mr-3" viewBox="0 0 24 24"> | |
| <path fill="#4285F4" d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z" /> | |
| <path fill="#34A853" d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" /> | |
| <path fill="#FBBC05" d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z" /> | |
| <path fill="#EA4335" d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z" /> | |
| </svg> | |
| )} | |
| Continue with Google | |
| <ArrowRight className="w-4 h-4 ml-auto opacity-0 -translate-x-2 group-hover:opacity-100 group-hover:translate-x-0 transition-all" /> | |
| </Button> | |
| <div className="relative my-8"> | |
| <Separator /> | |
| <span className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 bg-white px-4 text-sm text-slate-400"> | |
| or continue with email | |
| </span> | |
| </div> | |
| {/* Email Input */} | |
| <form onSubmit={handleEmailSubmit} className="space-y-4"> | |
| <div className="relative"> | |
| <Mail className="absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5 text-slate-400" /> | |
| <Input | |
| type="email" | |
| placeholder="name@company.com" | |
| value={email} | |
| onChange={(e) => { | |
| setEmail(e.target.value); | |
| setError(""); | |
| }} | |
| className="h-12 pl-12 text-base border-slate-200 focus:border-blue-500 focus:ring-blue-500" | |
| /> | |
| </div> | |
| <Button | |
| type="submit" | |
| disabled={loading} | |
| className="w-full h-12 text-base font-medium bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-700 hover:to-indigo-700 shadow-lg shadow-blue-500/25 transition-all" | |
| > | |
| {loading ? ( | |
| <> | |
| <Loader2 className="w-4 h-4 mr-2 animate-spin" /> | |
| Sending... | |
| </> | |
| ) : ( | |
| <> | |
| Continue with Email | |
| <ArrowRight className="w-4 h-4 ml-2" /> | |
| </> | |
| )} | |
| </Button> | |
| </form> | |
| </> | |
| ) : ( | |
| /* OTP Input */ | |
| <form onSubmit={handleOtpVerify} className="space-y-6"> | |
| <div className="flex justify-center gap-2"> | |
| {otp.map((digit, index) => ( | |
| <Input | |
| key={index} | |
| id={`otp-${index}`} | |
| type="text" | |
| inputMode="numeric" | |
| maxLength={1} | |
| value={digit} | |
| onChange={(e) => handleOtpChange(index, e.target.value)} | |
| onKeyDown={(e) => handleOtpKeyDown(index, e)} | |
| onPaste={(e) => handleOtpPaste(e, index)} | |
| className="w-12 h-14 text-center text-xl font-semibold border-slate-200 focus:border-blue-500 focus:ring-blue-500" | |
| /> | |
| ))} | |
| </div> | |
| <Button | |
| type="submit" | |
| disabled={loading || otp.join("").length !== 6} | |
| className="w-full h-12 text-base font-medium bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-700 hover:to-indigo-700 shadow-lg shadow-blue-500/25" | |
| > | |
| {loading ? ( | |
| <> | |
| <Loader2 className="w-4 h-4 mr-2 animate-spin" /> | |
| Verifying... | |
| </> | |
| ) : ( | |
| <> | |
| Verify & Sign In | |
| <ArrowRight className="w-4 h-4 ml-2" /> | |
| </> | |
| )} | |
| </Button> | |
| <button | |
| type="button" | |
| onClick={() => { | |
| setShowOtp(false); | |
| setOtp(["", "", "", "", "", ""]); | |
| setError(""); | |
| }} | |
| className="w-full text-sm text-slate-500 hover:text-slate-700 transition-colors" | |
| > | |
| ← Back to sign in options | |
| </button> | |
| </form> | |
| )} | |
| {/* Notice */} | |
| <div className="mt-8 pt-6 border-t border-slate-100"> | |
| <div className="flex items-start gap-2 text-xs text-slate-400 mb-4"> | |
| <Shield className="w-4 h-4 flex-shrink-0 mt-0.5" /> | |
| <span>Only business email addresses are allowed</span> | |
| </div> | |
| <p className="text-xs text-slate-400 text-center leading-relaxed"> | |
| By signing in, you agree to our{" "} | |
| <a href="#" className="text-blue-600 hover:underline"> | |
| Terms of Service | |
| </a>{" "} | |
| and{" "} | |
| <a href="#" className="text-blue-600 hover:underline"> | |
| Privacy Policy | |
| </a> | |
| </p> | |
| </div> | |
| </div> | |
| {/* Mobile Features */} | |
| <div className="lg:hidden mt-8 space-y-4"> | |
| {features.map((feature) => ( | |
| <div key={feature.title} className="flex items-center gap-3 text-sm"> | |
| <div className={`w-8 h-8 rounded-lg ${feature.bg} flex items-center justify-center`}> | |
| <feature.icon className={`w-4 h-4 ${feature.color}`} /> | |
| </div> | |
| <span className="text-slate-600">{feature.title}</span> | |
| </div> | |
| ))} | |
| </div> | |
| </motion.div> | |
| </div> | |
| </div> | |
| ); | |
| } | |