canina/frontend/application/components/AuthModal.tsx

990 lines
44 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client";
import React, { useState, useEffect, useRef, useCallback } from "react";
import { motion, AnimatePresence } from "motion/react";
import {
X,
Building2,
ShieldCheck,
Phone,
Key,
LogIn,
ArrowRight,
RefreshCw,
Lock,
Upload,
CheckCircle2,
Eye,
EyeOff,
} from "lucide-react";
import { useUserStore } from "../lib/store/userStore";
import { useSettingsStore } from "../lib/store/settingsStore";
import { authService } from "../lib/services/authService";
import { toast } from "sonner";
import { toPersian, toEnglishDigits } from "../lib/utils";
interface AuthModalProps {
isOpen: boolean;
onClose: () => void;
}
function extractOtpFromText(text: string): string {
if (!text) return "";
const clean = toEnglishDigits(text);
const match = clean.match(/(\d{5})/);
if (match) return match[1];
return clean.replace(/[^0-9]/g, "").slice(0, 5);
}
export default function AuthModal({ isOpen, onClose }: AuthModalProps) {
const { fetchProfile } = useUserStore();
const [view, setView] = useState<
| "login"
| "register"
| "otp-phone"
| "otp"
| "forgot-password"
| "forgot-otp"
| "reset-password"
| "wholesale-request"
>("login");
const [medicalLicense, setMedicalLicense] = useState("");
const [businessName, setBusinessName] = useState("");
const [documentUploaded, setDocumentUploaded] = useState(false);
const [phoneNumber, setPhoneNumber] = useState("");
const [password, setPassword] = useState("");
const [newPassword, setNewPassword] = useState("");
const [email, setEmail] = useState("");
const [firstName, setFirstName] = useState("");
const [lastName, setLastName] = useState("");
const [otpCode, setOtpCode] = useState("");
const [isLoading, setIsLoading] = useState(false);
const [countdown, setCountdown] = useState(0);
const [showPassword, setShowPassword] = useState(false);
const [showNewPassword, setShowNewPassword] = useState(false);
const otpInputRef = useRef<HTMLInputElement>(null);
const phoneNumberRef = useRef(phoneNumber);
const isB2BEnabled = useSettingsStore((s) => s.getBoolean('b2bRegistrationOpen', true) && s.getBoolean('b2b_enabled', true));
useEffect(() => {
phoneNumberRef.current = phoneNumber;
}, [phoneNumber]);
// Robust focus retry on view === "otp"
useEffect(() => {
if (view === "otp") {
const timers = [50, 150, 300, 500].map((delay) =>
setTimeout(() => {
if (otpInputRef.current) {
otpInputRef.current.focus({ preventScroll: true });
}
}, delay),
);
return () => timers.forEach((t) => clearTimeout(t));
}
}, [view]);
// Reset modal state on open/close
useEffect(() => {
if (!isOpen) {
Promise.resolve().then(() => {
setView("login");
setPhoneNumber("");
setPassword("");
setNewPassword("");
setEmail("");
setFirstName("");
setLastName("");
setOtpCode("");
setIsLoading(false);
setCountdown(0);
setShowPassword(false);
setShowNewPassword(false);
});
}
}, [isOpen]);
// Cooldown countdown timer
useEffect(() => {
if (countdown > 0) {
const timer = setTimeout(() => setCountdown(countdown - 1), 1000);
return () => clearTimeout(timer);
}
}, [countdown]);
const triggerVerifyOtp = useCallback(
async (codeToVerify: string) => {
const cleanCode = extractOtpFromText(codeToVerify);
const cleanPhone = toEnglishDigits(phoneNumberRef.current).replace(/[^0-9]/g, "");
if (cleanCode.length !== 5 || !cleanPhone) return;
setIsLoading(true);
try {
const response = await authService.verifyOtp(cleanPhone, cleanCode);
if (response.success) {
await fetchProfile();
toast.success("ورود با موفقیت انجام شد");
onClose();
}
} catch (err: unknown) {
const errObj = err as { message?: string };
toast.error(errObj.message || "کد تایید نامعتبر است");
} finally {
setIsLoading(false);
}
},
[fetchProfile, onClose],
);
// WebOTP API SMS Auto-read
useEffect(() => {
if (view !== "otp") return;
let isMounted = true;
const ac = new AbortController();
if (typeof window !== "undefined" && "OTPCredential" in window) {
(navigator as unknown as { credentials: { get: (opts: unknown) => Promise<{ code?: string }> } }).credentials
?.get({
otp: { transport: ["sms"] },
signal: ac.signal,
})
.then((otp) => {
if (!isMounted) return;
if (otp && typeof otp.code === "string") {
const clean = extractOtpFromText(otp.code);
if (clean) {
setOtpCode(clean);
if (otpInputRef.current) {
otpInputRef.current.value = clean;
}
if (clean.length === 5) {
triggerVerifyOtp(clean);
}
}
}
})
.catch(() => {});
}
// Direct DOM input / change listener for mobile keyboard autofill compatibility
const inputEl = otpInputRef.current;
const handleDomAutofill = (e: Event) => {
const target = e.target as HTMLInputElement;
if (target && target.value) {
const clean = extractOtpFromText(target.value);
setOtpCode(clean);
if (clean.length === 5) {
triggerVerifyOtp(clean);
}
}
};
if (inputEl) {
inputEl.addEventListener("input", handleDomAutofill);
inputEl.addEventListener("change", handleDomAutofill);
}
return () => {
isMounted = false;
ac.abort();
if (inputEl) {
inputEl.removeEventListener("input", handleDomAutofill);
inputEl.removeEventListener("change", handleDomAutofill);
}
};
}, [view, triggerVerifyOtp]);
const handleOtpChange = (val: string) => {
const clean = extractOtpFromText(val);
setOtpCode(clean);
if (otpInputRef.current && otpInputRef.current.value !== clean) {
otpInputRef.current.value = clean;
}
if (clean.length === 5) {
triggerVerifyOtp(clean);
}
};
const handleLogin = async (e: React.FormEvent) => {
e.preventDefault();
const cleanPhone = toEnglishDigits(phoneNumber).trim();
if (!cleanPhone || !password) return toast.error("شماره موبایل و رمز عبور الزامی است");
setIsLoading(true);
try {
const res = await authService.login(cleanPhone, password.trim());
if (res.success) {
await fetchProfile();
toast.success("با موفقیت وارد شدید");
onClose();
}
} catch (err: unknown) {
const errObj = err as { message?: string };
toast.error(errObj.message || "خطا در ورود");
} finally {
setIsLoading(false);
}
};
const handleRegister = async (e: React.FormEvent) => {
e.preventDefault();
const cleanPhone = toEnglishDigits(phoneNumber).trim();
if (!firstName || !lastName || !cleanPhone || !password) {
return toast.error("لطفاً فیلدهای الزامی را پر کنید");
}
setIsLoading(true);
try {
const res = await authService.register({
firstName: firstName.trim(),
lastName: lastName.trim(),
mobile: cleanPhone,
email: email ? email.trim() : undefined,
password: password.trim(),
});
if (res.success) {
await fetchProfile();
toast.success("ثبت‌نام با موفقیت انجام شد");
onClose();
}
} catch (err: unknown) {
const errObj = err as { message?: string };
toast.error(errObj.message || "خطا در ثبت‌نام");
} finally {
setIsLoading(false);
}
};
const handleSendOtp = async (e: React.FormEvent) => {
e.preventDefault();
const cleanPhone = toEnglishDigits(phoneNumber).trim();
if (!/^09\d{9}$/.test(cleanPhone)) {
toast.error("شماره موبایل نامعتبر است");
return;
}
setIsLoading(true);
try {
const res = await authService.sendOtp(cleanPhone);
const resObj = res as unknown as { code?: string };
if (resObj.code) {
toast.info(`کد تایید (تست): ${resObj.code}`, { duration: 10000 });
} else {
toast.success("کد تایید پیامک شد");
}
setView("otp");
setCountdown(120);
} catch (err: unknown) {
const errObj = err as { message?: string };
toast.error(errObj.message || "خطا در ارسال کد تایید");
} finally {
setIsLoading(false);
}
};
const handleVerifyOtp = async (e: React.FormEvent) => {
e.preventDefault();
triggerVerifyOtp(otpCode);
};
const handleResendOtp = async () => {
if (countdown > 0) return;
setIsLoading(true);
try {
await authService.sendOtp(toEnglishDigits(phoneNumber).trim());
toast.success("کد تایید جدید ارسال شد");
setCountdown(120);
} catch (err: unknown) {
const errObj = err as { message?: string };
toast.error(errObj.message || "خطا در ارسال مجدد کد");
} finally {
setIsLoading(false);
}
};
const handleForgotSendOtp = async (e: React.FormEvent) => {
e.preventDefault();
const cleanPhone = toEnglishDigits(phoneNumber).trim();
if (!/^09\d{9}$/.test(cleanPhone)) {
toast.error("شماره موبایل نامعتبر است");
return;
}
setIsLoading(true);
try {
const res = await authService.sendOtp(cleanPhone);
const resObj = res as unknown as { code?: string };
if (resObj.code) {
toast.info(`کد تایید (تست): ${resObj.code}`, { duration: 10000 });
} else {
toast.success("کد بازیابی رمز پیامک شد");
}
setView("forgot-otp");
setCountdown(120);
} catch (err: unknown) {
const errObj = err as { message?: string };
toast.error(errObj.message || "خطا در ارسال کد");
} finally {
setIsLoading(false);
}
};
const handleForgotVerifyOtp = async (e: React.FormEvent) => {
e.preventDefault();
const cleanCode = toEnglishDigits(otpCode).trim();
if (cleanCode.length !== 5) {
toast.error("کد باید ۵ رقم باشد");
return;
}
setIsLoading(true);
try {
await authService.verifyOtp(toEnglishDigits(phoneNumber).trim(), cleanCode);
setView("reset-password");
} catch (err: unknown) {
const errObj = err as { message?: string };
toast.error(errObj.message || "کد تایید نامعتبر است");
} finally {
setIsLoading(false);
}
};
const handleResetPassword = async (e: React.FormEvent) => {
e.preventDefault();
if (!newPassword || newPassword.length < 6) {
return toast.error("رمز عبور جدید باید حداقل ۶ کاراکتر باشد");
}
setIsLoading(true);
try {
await authService.updateProfile({ password: newPassword.trim() });
await fetchProfile();
toast.success("رمز عبور جدید با موفقیت تنظیم شد");
onClose();
} catch (err: unknown) {
const errObj = err as { message?: string };
toast.error(errObj.message || "خطا در تغییر رمز عبور");
} finally {
setIsLoading(false);
}
};
return (
<AnimatePresence>
{isOpen && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
onClick={onClose}
className="absolute inset-0 bg-medical-gray-900/60 backdrop-blur-sm"
/>
<motion.div
initial={{ opacity: 0, scale: 0.95, y: 20 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.95, y: 20 }}
className="relative w-full max-w-md bg-white rounded-[2.5rem] shadow-2xl overflow-hidden border border-medical-gray-100 z-10"
>
{/* Header */}
<div className="p-6 pb-0 flex justify-between items-center">
<div className="flex items-center gap-2 text-canina-blue font-black text-sm">
<ShieldCheck className="w-5 h-5" />
<span>ورود امن به کنینا</span>
</div>
<button
onClick={onClose}
className="w-10 h-10 rounded-full bg-medical-gray-50 flex items-center justify-center text-medical-gray-400 hover:bg-medical-gray-100 transition-colors"
>
<X className="w-5 h-5" />
</button>
</div>
<div className="p-8">
<AnimatePresence mode="wait">
{view === "login" && (
<motion.form
key="login-view"
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -20 }}
onSubmit={handleLogin}
className="space-y-4"
>
<div className="space-y-1">
<div className="flex justify-between items-center">
<label className="text-xs font-bold text-medical-gray-500">شماره موبایل</label>
<button
type="button"
onClick={() => setView("otp-phone")}
className="text-xs font-bold text-canina-blue hover:underline"
>
ورود با کد پیامکی (OTP)
</button>
</div>
<div className="relative">
<Phone className="absolute right-4 top-1/2 -translate-y-1/2 w-4 h-4 text-medical-gray-400" />
<input
autoFocus
type="tel"
maxLength={11}
required
placeholder="۰۹۱۲۳۴۵۶۷۸۹"
value={phoneNumber}
onChange={(e) => setPhoneNumber(e.target.value.replace(/[^0-9]/g, ""))}
className="w-full bg-medical-gray-50 border border-medical-gray-200 rounded-xl py-3 pr-11 pl-4 focus:ring-2 focus:ring-canina-blue/20 outline-none text-sm text-left font-mono tracking-wider"
dir="ltr"
/>
</div>
</div>
<div className="space-y-1">
<div className="flex justify-between items-center">
<label className="text-xs font-bold text-medical-gray-500">رمز عبور</label>
<button
data-testid="forgot-password-link"
type="button"
onClick={() => setView("forgot-password")}
className="text-xs font-bold text-medical-gray-400 hover:text-canina-blue"
>
فراموشی رمز عبور؟
</button>
</div>
<div className="relative">
<Lock className="absolute right-4 top-1/2 -translate-y-1/2 w-4 h-4 text-medical-gray-400 pointer-events-none" />
<input
type={showPassword ? "text" : "password"}
autoComplete="current-password"
required
placeholder="رمز عبور..."
value={password}
onChange={(e) => setPassword(e.target.value)}
className="w-full bg-medical-gray-50 border border-medical-gray-200 rounded-xl py-3 pr-11 pl-11 focus:ring-2 focus:ring-canina-blue/20 outline-none text-sm text-left font-mono tracking-wider"
dir="ltr"
/>
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute left-3 top-1/2 -translate-y-1/2 p-1.5 text-medical-gray-400 hover:text-medical-gray-700 transition-colors cursor-pointer"
tabIndex={-1}
title={showPassword ? "مخفی کردن رمز" : "نمایش رمز"}
>
{showPassword ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
</button>
</div>
</div>
<div className="flex items-center justify-between text-xs pt-1">
<div className="flex items-center gap-1 text-medical-gray-400">
<span>حساب کاربری ندارید؟</span>
<button
type="button"
onClick={() => setView("register")}
className="font-bold text-canina-blue hover:underline"
>
ثبتنام عادی
</button>
</div>
</div>
{isB2BEnabled && (
<div className="pt-3 border-t border-medical-gray-100">
<button
type="button"
onClick={() => setView("wholesale-request")}
className="w-full py-3 bg-amber-50 text-amber-900 border border-amber-200 rounded-xl text-xs font-black flex items-center justify-center gap-2 hover:bg-amber-100 transition-colors cursor-pointer"
>
<Building2 className="w-4 h-4 text-amber-600" />
درخواست حساب خریدار عمده (پتشاپ / کلینیک)
</button>
</div>
)}
<button
type="submit"
disabled={isLoading || !phoneNumber || !password}
className="w-full py-4 mt-4 bg-canina-blue text-white rounded-xl font-black hover:bg-indigo-700 disabled:opacity-50 transition-all shadow-lg shadow-canina-blue/20 flex items-center justify-center gap-2 cursor-pointer"
>
{isLoading ? <RefreshCw className="w-5 h-5 animate-spin" /> : <>ورود به حساب</>}
</button>
</motion.form>
)}
{view === "wholesale-request" && (
<motion.form
key="wholesale-view"
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -20 }}
onSubmit={(e) => {
e.preventDefault();
toast.success(
"درخواست احراز هویت خریدار عمده با موفقیت ارسال شد و پس از بررسی ادمین تایید خواهد شد.",
);
setView("login");
}}
className="space-y-3"
>
<div className="p-3 bg-blue-50 border border-blue-200 rounded-xl text-xs text-canina-blue font-bold">
ثبت شماره نظام دامپزشکی یا پروانه کسب جهت دسترسی به قیمتها و سفارش حجمی الزامی است.
</div>
<div className="space-y-1">
<label className="text-xs font-bold text-medical-gray-500">نام کلینیک / داروخانه / مجموعه *</label>
<input
type="text"
required
value={businessName}
onChange={(e) => setBusinessName(e.target.value)}
placeholder="مثال: کلینیک دامپزشکی رازی"
className="w-full bg-medical-gray-50 border border-medical-gray-200 rounded-xl py-2 px-3 focus:ring-2 focus:ring-canina-blue/20 outline-none text-sm"
/>
</div>
<div className="space-y-1">
<label className="text-xs font-bold text-medical-gray-500">شماره نظام دامپزشکی / پروانه کسب *</label>
<input
type="text"
required
value={medicalLicense}
onChange={(e) => setMedicalLicense(e.target.value)}
placeholder="مثال: ۹۸۷۶۵۴۳۲۱"
className="w-full bg-medical-gray-50 border border-medical-gray-200 rounded-xl py-2 px-3 focus:ring-2 focus:ring-canina-blue/20 outline-none text-sm text-left font-mono"
dir="ltr"
/>
</div>
<div className="space-y-1">
<label className="text-xs font-bold text-medical-gray-500">موبایل مسئول *</label>
<input
type="tel"
required
maxLength={11}
value={phoneNumber}
onChange={(e) => setPhoneNumber(e.target.value.replace(/[^0-9]/g, ""))}
placeholder="۰۹۱۲..."
className="w-full bg-medical-gray-50 border border-medical-gray-200 rounded-xl py-2 px-3 focus:ring-2 focus:ring-canina-blue/20 outline-none text-sm text-left font-mono"
dir="ltr"
/>
</div>
<div
onClick={() => {
setDocumentUploaded(true);
toast.success("تصویر پروانه با موفقیت بارگذاری شد.");
}}
className={`p-4 border-2 border-dashed rounded-xl text-center cursor-pointer transition-all ${
documentUploaded ? "border-green-500 bg-green-50" : "border-medical-gray-300 hover:border-canina-blue"
}`}
>
{documentUploaded ? (
<div className="flex items-center justify-center gap-2 text-green-700 font-bold text-xs">
<CheckCircle2 className="w-4 h-4" />
تصویر مدرک بارگذاری شد
</div>
) : (
<div className="flex flex-col items-center gap-1 text-medical-gray-500 text-xs">
<Upload className="w-5 h-5 text-canina-blue" />
<span>بارگذاری تصویر کارت نظام / پروانه</span>
</div>
)}
</div>
<div className="flex justify-start pt-1">
<button
type="button"
onClick={() => setView("login")}
className="text-xs font-bold text-canina-blue hover:underline cursor-pointer"
>
بازگشت به ورود
</button>
</div>
<button
type="submit"
disabled={!businessName || !medicalLicense || !phoneNumber}
className="w-full py-3 bg-canina-blue text-white rounded-xl font-black hover:bg-indigo-700 disabled:opacity-50 transition-all cursor-pointer"
>
ارسال درخواست بررسی به ادمین
</button>
</motion.form>
)}
{view === "register" && (
<motion.form
key="register-view"
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -20 }}
onSubmit={handleRegister}
className="space-y-3"
>
<div className="grid grid-cols-2 gap-3">
<div className="space-y-1">
<label htmlFor="reg-fname" className="text-xs font-bold text-medical-gray-500">نام</label>
<input
id="reg-fname"
type="text"
required
value={firstName}
onChange={(e) => setFirstName(e.target.value)}
className="w-full bg-medical-gray-50 border border-medical-gray-200 rounded-xl py-2 px-3 focus:ring-2 focus:ring-canina-blue/20 outline-none text-sm"
/>
</div>
<div className="space-y-1">
<label htmlFor="reg-lname" className="text-xs font-bold text-medical-gray-500">نام خانوادگی</label>
<input
id="reg-lname"
type="text"
required
value={lastName}
onChange={(e) => setLastName(e.target.value)}
className="w-full bg-medical-gray-50 border border-medical-gray-200 rounded-xl py-2 px-3 focus:ring-2 focus:ring-canina-blue/20 outline-none text-sm"
/>
</div>
</div>
<div className="space-y-1">
<label htmlFor="reg-phone" className="text-xs font-bold text-medical-gray-500">شماره موبایل</label>
<input
id="reg-phone"
type="tel"
required
maxLength={11}
placeholder="۰۹۱۲۳۴۵۶۷۸۹"
value={phoneNumber}
onChange={(e) => setPhoneNumber(e.target.value.replace(/[^0-9]/g, ""))}
className="w-full bg-medical-gray-50 border border-medical-gray-200 rounded-xl py-2 px-3 focus:ring-2 focus:ring-canina-blue/20 outline-none text-sm text-left font-mono tracking-wider"
dir="ltr"
/>
</div>
<div className="space-y-1">
<label htmlFor="reg-email" className="text-xs font-bold text-medical-gray-500">ایمیل (اختیاری)</label>
<input
id="reg-email"
type="email"
placeholder="name@example.com"
value={email}
onChange={(e) => setEmail(e.target.value)}
className="w-full bg-medical-gray-50 border border-medical-gray-200 rounded-xl py-2 px-3 focus:ring-2 focus:ring-canina-blue/20 outline-none text-sm text-left font-mono"
dir="ltr"
/>
</div>
<div className="space-y-1">
<label htmlFor="reg-pass" className="text-xs font-bold text-medical-gray-500">رمز عبور</label>
<div className="relative">
<input
id="reg-pass"
type={showPassword ? "text" : "password"}
autoComplete="new-password"
required
placeholder="حداقل ۶ کاراکتر"
value={password}
onChange={(e) => setPassword(e.target.value)}
className="w-full bg-medical-gray-50 border border-medical-gray-200 rounded-xl py-2 pr-3 pl-10 focus:ring-2 focus:ring-canina-blue/20 outline-none text-sm text-left font-mono"
dir="ltr"
/>
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute left-2.5 top-1/2 -translate-y-1/2 p-1 text-medical-gray-400 hover:text-medical-gray-700 transition-colors cursor-pointer"
tabIndex={-1}
>
{showPassword ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
</button>
</div>
</div>
<div className="flex justify-start pt-1">
<button
type="button"
onClick={() => setView("login")}
className="text-xs font-bold text-canina-blue hover:underline cursor-pointer"
>
بازگشت به ورود
</button>
</div>
<button
type="submit"
disabled={isLoading || !phoneNumber || !password || !firstName || !lastName}
className="w-full py-3 mt-2 bg-canina-blue text-white rounded-xl font-black hover:bg-indigo-700 disabled:opacity-50 transition-all flex items-center justify-center gap-2 cursor-pointer"
>
{isLoading ? <RefreshCw className="w-4 h-4 animate-spin" /> : <>ثبتنام</>}
</button>
</motion.form>
)}
{view === "otp-phone" && (
<motion.form
key="otp-phone-view"
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -20 }}
onSubmit={handleSendOtp}
className="space-y-6"
>
<div className="space-y-2">
<div className="flex justify-between items-center">
<label className="text-[10px] font-black text-medical-gray-400 uppercase tracking-widest">شماره موبایل</label>
<button
type="button"
onClick={() => setView("login")}
className="text-[10px] font-black text-canina-blue hover:underline cursor-pointer"
>
برگشت به صفحه ورود
</button>
</div>
<div className="relative">
<Phone className="absolute right-4 top-1/2 -translate-y-1/2 w-5 h-5 text-medical-gray-400" />
<input
autoFocus
type="tel"
maxLength={11}
placeholder="مثال: ۰۹۱۲۳۴۵۶۷۸۹"
value={phoneNumber}
onChange={(e) => setPhoneNumber(e.target.value.replace(/[^0-9]/g, ""))}
disabled={isLoading}
className="w-full bg-medical-gray-50 border border-medical-gray-200 rounded-2xl py-4 pr-12 pl-4 focus:ring-2 focus:ring-canina-blue/20 outline-none font-bold text-lg text-left font-mono tracking-widest"
dir="ltr"
/>
</div>
</div>
<button
type="submit"
disabled={isLoading || phoneNumber.length < 11}
className="w-full py-5 bg-canina-blue text-white rounded-2xl font-black text-xl hover:bg-indigo-700 disabled:opacity-50 transition-all shadow-xl shadow-canina-blue/20 flex items-center justify-center gap-3 cursor-pointer"
>
{isLoading ? (
<RefreshCw className="w-6 h-6 animate-spin" />
) : (
<>
<LogIn className="w-6 h-6" />
ارسال کد تایید
</>
)}
</button>
</motion.form>
)}
{view === "otp" && (
<motion.form
key="otp-view"
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -20 }}
onSubmit={handleVerifyOtp}
className="space-y-6"
>
<div className="space-y-2">
<div className="flex justify-between items-center">
<label className="text-[10px] font-black text-medical-gray-400 uppercase tracking-widest">کد تایید پیامکی</label>
<button
type="button"
onClick={() => setView("otp-phone")}
className="text-[10px] font-black text-canina-blue flex items-center gap-1 hover:underline cursor-pointer"
>
<ArrowRight className="w-3 h-3" />
تغییر شماره
</button>
</div>
<div className="relative">
<Key className="absolute right-4 top-1/2 -translate-y-1/2 w-5 h-5 text-medical-gray-400 pointer-events-none" />
<input
ref={otpInputRef}
id="otp-code-input"
name="one-time-code"
type="text"
inputMode="numeric"
autoComplete="one-time-code"
placeholder="کد ۵ رقمی"
value={otpCode}
onChange={(e) => handleOtpChange(e.target.value)}
onInput={(e) => handleOtpChange((e.target as HTMLInputElement).value)}
onPaste={(e) => {
e.preventDefault();
const pasted = e.clipboardData.getData('text');
handleOtpChange(pasted);
}}
disabled={isLoading}
className="w-full bg-medical-gray-50 border border-medical-gray-200 rounded-2xl py-4 pr-12 pl-4 focus:ring-2 focus:ring-canina-blue/20 outline-none font-bold text-center text-2xl font-mono tracking-[0.5em]"
dir="ltr"
/>
</div>
<p className="text-[11px] text-medical-gray-400 text-center font-medium pt-1">
کد پیامکشده به صورت خودکار شناسایی و وارد میشود.
</p>
</div>
<button
type="submit"
disabled={isLoading || otpCode.length < 5}
className="w-full py-5 bg-canina-blue text-white rounded-2xl font-black text-xl hover:bg-indigo-700 disabled:opacity-50 transition-all shadow-xl shadow-canina-blue/20 flex items-center justify-center gap-3 cursor-pointer"
>
{isLoading ? (
<RefreshCw className="w-6 h-6 animate-spin" />
) : (
<>
<ShieldCheck className="w-6 h-6" />
تایید و ورود
</>
)}
</button>
<div className="text-center pt-2">
{countdown > 0 ? (
<span className="text-xs font-bold text-medical-gray-400">
ارسال مجدد کد تا {toPersian(countdown.toString())} ثانیه دیگر
</span>
) : (
<button
type="button"
onClick={handleResendOtp}
className="text-xs font-black text-canina-blue hover:underline cursor-pointer"
>
ارسال مجدد کد تایید
</button>
)}
</div>
</motion.form>
)}
{view === "forgot-password" && (
<motion.form
key="forgot-view"
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -20 }}
onSubmit={handleForgotSendOtp}
className="space-y-4"
>
<div className="space-y-1">
<label className="text-xs font-bold text-medical-gray-500">شماره موبایل حساب</label>
<div className="relative">
<Phone className="absolute right-4 top-1/2 -translate-y-1/2 w-4 h-4 text-medical-gray-400" />
<input
autoFocus
type="tel"
maxLength={11}
required
placeholder="۰۹۱۲۳۴۵۶۷۸۹"
value={phoneNumber}
onChange={(e) => setPhoneNumber(e.target.value.replace(/[^0-9]/g, ""))}
className="w-full bg-medical-gray-50 border border-medical-gray-200 rounded-xl py-3 pr-11 pl-4 focus:ring-2 focus:ring-canina-blue/20 outline-none text-sm text-left font-mono tracking-wider"
dir="ltr"
/>
</div>
</div>
<div className="flex justify-start pt-1">
<button
type="button"
onClick={() => setView("login")}
className="text-xs font-bold text-canina-blue hover:underline cursor-pointer"
>
بازگشت به ورود
</button>
</div>
<button
type="submit"
disabled={isLoading || phoneNumber.length < 11}
className="w-full py-4 bg-canina-blue text-white rounded-xl font-black hover:bg-indigo-700 disabled:opacity-50 transition-all shadow-lg shadow-canina-blue/20 flex items-center justify-center gap-2 cursor-pointer"
>
{isLoading ? <RefreshCw className="w-5 h-5 animate-spin" /> : <>ارسال کد بازیابی</>}
</button>
</motion.form>
)}
{view === "forgot-otp" && (
<motion.form
key="forgot-otp-view"
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -20 }}
onSubmit={handleForgotVerifyOtp}
className="space-y-4"
>
<div className="space-y-1">
<label className="text-xs font-bold text-medical-gray-500">کد تایید بازیابی</label>
<div className="relative">
<Key className="absolute right-4 top-1/2 -translate-y-1/2 w-4 h-4 text-medical-gray-400 pointer-events-none" />
<input
autoFocus
type="text"
inputMode="numeric"
autoComplete="one-time-code"
placeholder="کد ۵ رقمی"
value={otpCode}
onChange={(e) => setOtpCode(toEnglishDigits(e.target.value).replace(/[^0-9]/g, "").slice(0, 5))}
onInput={(e) => setOtpCode(toEnglishDigits((e.target as HTMLInputElement).value).replace(/[^0-9]/g, "").slice(0, 5))}
className="w-full bg-medical-gray-50 border border-medical-gray-200 rounded-xl py-3 pr-11 pl-4 focus:ring-2 focus:ring-canina-blue/20 outline-none text-center font-mono font-bold text-xl tracking-[0.4em]"
dir="ltr"
/>
</div>
</div>
<button
type="submit"
disabled={isLoading || otpCode.length < 5}
className="w-full py-4 bg-canina-blue text-white rounded-xl font-black hover:bg-indigo-700 disabled:opacity-50 transition-all shadow-lg shadow-canina-blue/20 flex items-center justify-center gap-2 cursor-pointer"
>
{isLoading ? <RefreshCw className="w-5 h-5 animate-spin" /> : <>تایید و ادامه</>}
</button>
</motion.form>
)}
{view === "reset-password" && (
<motion.form
key="reset-pass-view"
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -20 }}
onSubmit={handleResetPassword}
className="space-y-4"
>
<div className="space-y-1">
<label className="text-xs font-bold text-medical-gray-500">رمز عبور جدید</label>
<div className="relative">
<Lock className="absolute right-4 top-1/2 -translate-y-1/2 w-4 h-4 text-medical-gray-400 pointer-events-none" />
<input
autoFocus
type={showNewPassword ? "text" : "password"}
autoComplete="new-password"
required
placeholder="حداقل ۶ کاراکتر"
value={newPassword}
onChange={(e) => setNewPassword(e.target.value)}
className="w-full bg-medical-gray-50 border border-medical-gray-200 rounded-xl py-3 pr-11 pl-11 focus:ring-2 focus:ring-canina-blue/20 outline-none text-sm text-left font-mono"
dir="ltr"
/>
<button
type="button"
onClick={() => setShowNewPassword(!showNewPassword)}
className="absolute left-3 top-1/2 -translate-y-1/2 p-1 text-medical-gray-400 hover:text-medical-gray-700 transition-colors cursor-pointer"
tabIndex={-1}
>
{showNewPassword ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
</button>
</div>
</div>
<button
type="submit"
disabled={isLoading || newPassword.length < 6}
className="w-full py-4 bg-canina-blue text-white rounded-xl font-black hover:bg-indigo-700 disabled:opacity-50 transition-all shadow-lg shadow-canina-blue/20 flex items-center justify-center gap-2 cursor-pointer"
>
{isLoading ? <RefreshCw className="w-5 h-5 animate-spin" /> : <>ثبت رمز عبور جدید</>}
</button>
</motion.form>
)}
</AnimatePresence>
</div>
</motion.div>
</div>
)}
</AnimatePresence>
);
}