| | 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(""); |
| |
|
| | |
| | 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(""); |
| |
|
| | |
| | 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"); |
| | |
| | const digits = pastedData.replace(/\D/g, "").slice(0, 6); |
| | |
| | if (digits.length > 0) { |
| | const newOtp = [...otp]; |
| | |
| | for (let i = 0; i < digits.length && (startIndex + i) < 6; i++) { |
| | newOtp[startIndex + i] = digits[i]; |
| | } |
| | setOtp(newOtp); |
| | setError(""); |
| | |
| | |
| | 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); |
| | |
| | } 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> |
| | )} |
| |
|
| | {} |
| | <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> |
| |
|
| | {} |
| | <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> |
| | ); |
| | } |
| |
|