1119 lines
55 KiB
TypeScript
1119 lines
55 KiB
TypeScript
"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,
|
||
UserPlus,
|
||
Sparkles,
|
||
Smartphone,
|
||
Mail,
|
||
User,
|
||
ArrowLeft,
|
||
KeyRound,
|
||
Check
|
||
} 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, cn } from "../lib/utils";
|
||
|
||
import OtpInput5 from "./OtpInput5";
|
||
|
||
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);
|
||
}
|
||
|
||
// Global OTP expiry timestamp cache by phone number so switching back and forth preserves timer
|
||
const otpCooldownExpiries: Record<string, number> = {};
|
||
|
||
export default function AuthModal({ isOpen, onClose }: AuthModalProps) {
|
||
const { fetchProfile } = useUserStore();
|
||
|
||
// Auth navigation state
|
||
const [authMode, setAuthMode] = useState<"login" | "register">("login");
|
||
const [loginMethod, setLoginMethod] = useState<"otp" | "password">("otp");
|
||
const [subView, setSubView] = useState<"main" | "otp-verify" | "forgot-phone" | "forgot-otp" | "reset-password" | "b2b">("main");
|
||
|
||
// Form fields
|
||
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 [businessName, setBusinessName] = useState("");
|
||
const [medicalLicense, setMedicalLicense] = useState("");
|
||
const [documentUploaded, setDocumentUploaded] = useState(false);
|
||
|
||
// States
|
||
const [isLoading, setIsLoading] = useState(false);
|
||
const [countdown, setCountdown] = useState(0);
|
||
const [showPassword, setShowPassword] = useState(false);
|
||
const [showNewPassword, setShowNewPassword] = useState(false);
|
||
|
||
const phoneNumberRef = useRef(phoneNumber);
|
||
const subViewRef = useRef(subView);
|
||
const isOpenRef = useRef(isOpen);
|
||
// Guard against double-verify (onComplete fires, then form submit also fires)
|
||
const isVerifyingRef = useRef(false);
|
||
const isB2BEnabled = useSettingsStore((s) => s.getBoolean('b2bRegistrationOpen', true) && s.getBoolean('b2b_enabled', true));
|
||
|
||
useEffect(() => {
|
||
phoneNumberRef.current = phoneNumber;
|
||
}, [phoneNumber]);
|
||
|
||
useEffect(() => {
|
||
subViewRef.current = subView;
|
||
}, [subView]);
|
||
|
||
useEffect(() => {
|
||
isOpenRef.current = isOpen;
|
||
}, [isOpen]);
|
||
|
||
// Handle hardware / browser back button for Android & mobile navigation
|
||
// IMPORTANT: only depend on isOpen here — NOT subView. If we depend on subView,
|
||
// every subView change pushes a new history entry and back button never closes the modal.
|
||
useEffect(() => {
|
||
if (!isOpen) return;
|
||
|
||
// Push ONE history entry when the modal opens so back button hits this entry first
|
||
window.history.pushState({ __authModalOpen: true }, "");
|
||
|
||
const handlePopState = () => {
|
||
if (subViewRef.current !== "main") {
|
||
// Navigate back within the modal
|
||
if (subViewRef.current === "forgot-otp") {
|
||
setSubView("forgot-phone");
|
||
} else if (subViewRef.current === "reset-password") {
|
||
setSubView("forgot-phone");
|
||
} else {
|
||
setSubView("main");
|
||
}
|
||
// Push another sentinel so the next back also hits this handler
|
||
window.history.pushState({ __authModalOpen: true }, "");
|
||
} else {
|
||
// Already at main view — close the modal
|
||
onClose();
|
||
}
|
||
};
|
||
|
||
window.addEventListener("popstate", handlePopState);
|
||
return () => {
|
||
window.removeEventListener("popstate", handlePopState);
|
||
};
|
||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||
}, [isOpen]);
|
||
|
||
// Reset modal state on open/close
|
||
useEffect(() => {
|
||
if (!isOpen) {
|
||
Promise.resolve().then(() => {
|
||
setAuthMode("login");
|
||
setLoginMethod("otp");
|
||
setSubView("main");
|
||
setPhoneNumber("");
|
||
setPassword("");
|
||
setNewPassword("");
|
||
setEmail("");
|
||
setFirstName("");
|
||
setLastName("");
|
||
setOtpCode("");
|
||
setBusinessName("");
|
||
setMedicalLicense("");
|
||
setDocumentUploaded(false);
|
||
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;
|
||
// Prevent double-call from onComplete + form submit firing simultaneously
|
||
if (isVerifyingRef.current) return;
|
||
isVerifyingRef.current = true;
|
||
|
||
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 || "کد تایید نامعتبر است");
|
||
isVerifyingRef.current = false;
|
||
} finally {
|
||
setIsLoading(false);
|
||
}
|
||
},
|
||
[fetchProfile, onClose],
|
||
);
|
||
|
||
|
||
// Keep a stable ref to triggerVerifyOtp so the WebOTP effect doesn't need
|
||
// it as a dependency (which was causing premature AbortController cancellation).
|
||
const triggerVerifyOtpRef = useRef(triggerVerifyOtp);
|
||
useEffect(() => { triggerVerifyOtpRef.current = triggerVerifyOtp; }, [triggerVerifyOtp]);
|
||
|
||
// WebOTP API - auto-fill OTP from incoming SMS.
|
||
// Bug-fix: only depend on [subView] here — NOT triggerVerifyOtp.
|
||
// The old implementation had [subView, triggerVerifyOtp] as deps, which meant any
|
||
// reference change in triggerVerifyOtp (caused by onClose/fetchProfile re-creation)
|
||
// would abort() the pending WebOTP Promise just as the user was clicking "Allow".
|
||
// Using a ref breaks the dependency cycle while keeping the callback up-to-date.
|
||
useEffect(() => {
|
||
if (subView !== "otp-verify" && subView !== "forgot-otp") return;
|
||
if (typeof window === "undefined" || !("OTPCredential" in window)) return;
|
||
|
||
let isMounted = true;
|
||
const ac = new AbortController();
|
||
|
||
const credApi = (navigator as unknown as {
|
||
credentials?: { get?: (opts: unknown) => Promise<{ code?: string }> };
|
||
}).credentials;
|
||
|
||
credApi?.get?.({
|
||
otp: { transport: ["sms"] },
|
||
signal: ac.signal,
|
||
}).then((otp) => {
|
||
if (!isMounted || !otp) return;
|
||
// OTPCredential.code is the extracted code, but fallback to any string property
|
||
const rawCode = (otp as { code?: string })?.code || (otp as { id?: string })?.id || "";
|
||
const digits = extractOtpFromText(rawCode);
|
||
if (digits.length === 5) {
|
||
setOtpCode(digits);
|
||
// 400 ms delay so the user can see the filled boxes before verification
|
||
setTimeout(() => {
|
||
if (isMounted) triggerVerifyOtpRef.current(digits);
|
||
}, 400);
|
||
}
|
||
}).catch(() => {});
|
||
|
||
return () => {
|
||
isMounted = false;
|
||
ac.abort();
|
||
};
|
||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||
}, [subView]);
|
||
|
||
const handleOtpChange = (val: string) => {
|
||
const clean = extractOtpFromText(val);
|
||
setOtpCode(clean);
|
||
// Auto-verify is handled by OtpInput5's onComplete callback only
|
||
// to prevent double-submission
|
||
};
|
||
|
||
|
||
const handleSendOtp = async (e?: React.FormEvent) => {
|
||
if (e) e.preventDefault();
|
||
const cleanPhone = toEnglishDigits(phoneNumber).trim();
|
||
if (!/^09\d{9}$/.test(cleanPhone)) {
|
||
toast.error("لطفاً شماره موبایل ۱۱ رقمی معتبر وارد کنید (مثال: ۰۹۱۲۳۴۵۶۷۸۹)");
|
||
return;
|
||
}
|
||
|
||
// Check if cooldown is still active for this phone number
|
||
const now = Date.now();
|
||
const existingExpiry = otpCooldownExpiries[cleanPhone] || 0;
|
||
const remainingSeconds = Math.max(0, Math.ceil((existingExpiry - now) / 1000));
|
||
|
||
if (remainingSeconds > 0) {
|
||
setCountdown(remainingSeconds);
|
||
setSubView("otp-verify");
|
||
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("کد تایید با موفقیت پیامک شد");
|
||
}
|
||
setSubView("otp-verify");
|
||
otpCooldownExpiries[cleanPhone] = Date.now() + 120 * 1000;
|
||
setCountdown(120);
|
||
} catch (err: unknown) {
|
||
const errObj = err as { message?: string };
|
||
toast.error(errObj.message || "خطا در ارسال کد تایید");
|
||
} finally {
|
||
setIsLoading(false);
|
||
}
|
||
};
|
||
|
||
const handlePasswordLogin = 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?.trim() || !lastName?.trim() || !cleanPhone || !password) {
|
||
return toast.error("لطفاً تمامی فیلدهای الزامی را تکمیل کنید");
|
||
}
|
||
if (!/^09\d{9}$/.test(cleanPhone)) {
|
||
return toast.error("شماره موبایل نامعتبر است");
|
||
}
|
||
if (password.length < 6) {
|
||
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 handleResendOtp = async () => {
|
||
if (countdown > 0) return;
|
||
const cleanPhone = toEnglishDigits(phoneNumber).trim();
|
||
setIsLoading(true);
|
||
try {
|
||
await authService.sendOtp(cleanPhone);
|
||
toast.success("کد تایید جدید پیامک شد");
|
||
otpCooldownExpiries[cleanPhone] = Date.now() + 120 * 1000;
|
||
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("کد تایید جهت بازیابی رمز پیامک شد");
|
||
}
|
||
setSubView("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);
|
||
setSubView("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-end sm:items-center justify-center p-0 sm:p-4 font-vazir overflow-y-auto" dir="rtl">
|
||
{/* Backdrop */}
|
||
<motion.div
|
||
initial={{ opacity: 0 }}
|
||
animate={{ opacity: 1 }}
|
||
exit={{ opacity: 0 }}
|
||
onClick={onClose}
|
||
className="fixed inset-0 bg-medical-gray-900/60 backdrop-blur-md transition-opacity"
|
||
/>
|
||
|
||
{/* Modal Container */}
|
||
<motion.div
|
||
initial={{ opacity: 0, scale: 0.96, y: 20 }}
|
||
animate={{ opacity: 1, scale: 1, y: 0 }}
|
||
exit={{ opacity: 0, scale: 0.96, y: 20 }}
|
||
className="relative w-full max-w-md bg-white rounded-t-[2.5rem] sm:rounded-[2rem] shadow-2xl overflow-hidden border border-medical-gray-100 z-10 max-h-[92vh] flex flex-col my-auto pb-safe font-vazir text-right"
|
||
dir="rtl"
|
||
>
|
||
{/* Top Bar / Header */}
|
||
<div className="px-6 pt-5 pb-3 border-b border-medical-gray-100/80 flex items-center justify-between bg-medical-gray-50/50 shrink-0">
|
||
<div className="flex items-center gap-2 text-medical-gray-900">
|
||
<div className="w-8 h-8 rounded-xl bg-canina-blue/10 text-canina-blue flex items-center justify-center">
|
||
<ShieldCheck className="w-4 h-4" />
|
||
</div>
|
||
<div>
|
||
<h3 className="text-sm font-black tracking-tight text-medical-gray-900 font-vazir">احراز هویت کنینا</h3>
|
||
<p className="text-[10px] text-medical-gray-400 font-bold font-vazir">ورود امن به حساب کاربری</p>
|
||
</div>
|
||
</div>
|
||
<button
|
||
onClick={onClose}
|
||
className="w-8 h-8 rounded-full bg-white border border-medical-gray-200 flex items-center justify-center text-medical-gray-400 hover:text-medical-gray-700 hover:bg-medical-gray-100 transition-all cursor-pointer"
|
||
>
|
||
<X className="w-4 h-4" />
|
||
</button>
|
||
</div>
|
||
|
||
{/* Modal Body */}
|
||
<div className="p-6 sm:p-7 overflow-y-auto">
|
||
<AnimatePresence mode="wait">
|
||
{/* 1. MAIN FLOW: Login or Register */}
|
||
{subView === "main" && (
|
||
<motion.div
|
||
key="main-auth"
|
||
initial={{ opacity: 0, y: 10 }}
|
||
animate={{ opacity: 1, y: 0 }}
|
||
exit={{ opacity: 0, y: -10 }}
|
||
className="space-y-5"
|
||
>
|
||
{/* Primary Tab: Login vs Register */}
|
||
<div className="p-1 bg-medical-gray-100/90 rounded-2xl flex gap-1 font-vazir">
|
||
<button
|
||
type="button"
|
||
onClick={() => {
|
||
setAuthMode("login");
|
||
setPassword("");
|
||
}}
|
||
className={cn(
|
||
"flex-1 py-2.5 rounded-xl text-xs font-black transition-all flex items-center justify-center gap-1.5 cursor-pointer font-vazir",
|
||
authMode === "login"
|
||
? "bg-white text-canina-blue shadow-xs font-black"
|
||
: "text-medical-gray-500 hover:text-medical-gray-800"
|
||
)}
|
||
>
|
||
<LogIn className="w-3.5 h-3.5" />
|
||
<span>ورود به حساب</span>
|
||
</button>
|
||
<button
|
||
type="button"
|
||
onClick={() => {
|
||
setAuthMode("register");
|
||
setPassword("");
|
||
}}
|
||
className={cn(
|
||
"flex-1 py-2.5 rounded-xl text-xs font-black transition-all flex items-center justify-center gap-1.5 cursor-pointer font-vazir",
|
||
authMode === "register"
|
||
? "bg-white text-canina-blue shadow-xs font-black"
|
||
: "text-medical-gray-500 hover:text-medical-gray-800"
|
||
)}
|
||
>
|
||
<UserPlus className="w-3.5 h-3.5" />
|
||
<span>ثبتنام کاربر جدید</span>
|
||
</button>
|
||
</div>
|
||
|
||
{/* Sub-Tabs for Login Mode: Password vs OTP */}
|
||
{authMode === "login" && (
|
||
<div className="flex items-center justify-center gap-2 pt-1 pb-1 font-vazir">
|
||
<span className="text-[11px] font-bold text-medical-gray-400 font-vazir">روش ورود:</span>
|
||
<div className="inline-flex p-1 bg-medical-gray-50 border border-medical-gray-200 rounded-xl gap-1">
|
||
<button
|
||
type="button"
|
||
onClick={() => setLoginMethod("password")}
|
||
className={cn(
|
||
"px-3 py-1.5 rounded-lg text-[11px] font-bold transition-all flex items-center gap-1 cursor-pointer font-vazir",
|
||
loginMethod === "password"
|
||
? "bg-canina-blue text-white shadow-xs font-black"
|
||
: "text-medical-gray-600 hover:text-medical-gray-900"
|
||
)}
|
||
>
|
||
<KeyRound className="w-3 h-3" />
|
||
<span>رمز عبور</span>
|
||
</button>
|
||
<button
|
||
type="button"
|
||
onClick={() => setLoginMethod("otp")}
|
||
className={cn(
|
||
"px-3 py-1.5 rounded-lg text-[11px] font-bold transition-all flex items-center gap-1 cursor-pointer font-vazir",
|
||
loginMethod === "otp"
|
||
? "bg-canina-blue text-white shadow-xs font-black"
|
||
: "text-medical-gray-600 hover:text-medical-gray-900"
|
||
)}
|
||
>
|
||
<Smartphone className="w-3 h-3" />
|
||
<span>کد پیامکی یکبار مصرف</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Form for Login (Password Method) */}
|
||
{authMode === "login" && loginMethod === "password" && (
|
||
<form onSubmit={handlePasswordLogin} className="space-y-4 pt-1 font-vazir">
|
||
<div className="space-y-1.5">
|
||
<label className="text-[11px] font-black text-medical-gray-500 block pr-1 font-vazir">شماره تلفن همراه</label>
|
||
<div className="relative">
|
||
<Phone 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="tel"
|
||
inputMode="numeric"
|
||
pattern="[0-9]*"
|
||
maxLength={11}
|
||
required
|
||
placeholder="۰۹۱۲۳۴۵۶۷۸۹"
|
||
value={phoneNumber}
|
||
onChange={(e) => setPhoneNumber(e.target.value.replace(/[^0-9]/g, ""))}
|
||
className="w-full bg-medical-gray-50/80 border border-medical-gray-200 rounded-2xl py-3.5 pr-11 pl-4 focus:ring-2 focus:ring-canina-blue/20 outline-none text-sm text-left font-vazir placeholder:font-vazir tracking-wider transition-all"
|
||
dir="ltr"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="space-y-1.5">
|
||
<div className="flex justify-between items-center pr-1">
|
||
<label className="text-[11px] font-black text-medical-gray-500 font-vazir">رمز عبور</label>
|
||
<button
|
||
type="button"
|
||
data-testid="forgot-password-link"
|
||
onClick={() => setSubView("forgot-phone")}
|
||
className="text-[11px] font-bold text-canina-blue hover:underline cursor-pointer font-vazir"
|
||
>
|
||
فراموشی رمز عبور؟
|
||
</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/80 border border-medical-gray-200 rounded-2xl py-3.5 pr-11 pl-11 focus:ring-2 focus:ring-canina-blue/20 outline-none text-sm text-left font-vazir placeholder:font-vazir tracking-wider transition-all"
|
||
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}
|
||
>
|
||
{showPassword ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<button
|
||
type="submit"
|
||
disabled={isLoading || !phoneNumber || !password}
|
||
className="w-full py-3.5 bg-canina-blue text-white rounded-2xl font-black text-sm hover:bg-canina-blue/90 disabled:opacity-50 transition-all shadow-md shadow-canina-blue/20 flex items-center justify-center gap-2 cursor-pointer mt-2 font-vazir"
|
||
>
|
||
{isLoading ? (
|
||
<RefreshCw className="w-4 h-4 animate-spin" />
|
||
) : (
|
||
<>
|
||
<LogIn className="w-4 h-4" />
|
||
<span>ورود با رمز عبور</span>
|
||
</>
|
||
)}
|
||
</button>
|
||
</form>
|
||
)}
|
||
|
||
{/* Form for Login (OTP Method) */}
|
||
{authMode === "login" && loginMethod === "otp" && (
|
||
<form onSubmit={handleSendOtp} className="space-y-4 pt-1 font-vazir">
|
||
<div className="space-y-1.5">
|
||
<label className="text-[11px] font-black text-medical-gray-500 block pr-1 font-vazir">شماره تلفن همراه</label>
|
||
<div className="relative">
|
||
<Phone 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="tel"
|
||
inputMode="numeric"
|
||
pattern="[0-9]*"
|
||
maxLength={11}
|
||
required
|
||
placeholder="۰۹۱۲۳۴۵۶۷۸۹"
|
||
value={phoneNumber}
|
||
onChange={(e) => setPhoneNumber(e.target.value.replace(/[^0-9]/g, ""))}
|
||
className="w-full bg-medical-gray-50/80 border border-medical-gray-200 rounded-2xl py-3.5 pr-11 pl-4 focus:ring-2 focus:ring-canina-blue/20 outline-none text-sm text-left font-vazir placeholder:font-vazir tracking-wider transition-all"
|
||
dir="ltr"
|
||
/>
|
||
</div>
|
||
<p className="text-[10px] text-medical-gray-400 pr-1 font-vazir">کد تایید ۵ رقمی برای این شماره پیامک خواهد شد.</p>
|
||
</div>
|
||
|
||
<button
|
||
type="submit"
|
||
disabled={isLoading || phoneNumber.length < 11}
|
||
className="w-full py-3.5 bg-canina-blue text-white rounded-2xl font-black text-sm hover:bg-canina-blue/90 disabled:opacity-50 transition-all shadow-md shadow-canina-blue/20 flex items-center justify-center gap-2 cursor-pointer mt-2 font-vazir"
|
||
>
|
||
{isLoading ? (
|
||
<RefreshCw className="w-4 h-4 animate-spin" />
|
||
) : (
|
||
<>
|
||
<span>دریافت کد تایید و ورود</span>
|
||
<ArrowLeft className="w-4 h-4" />
|
||
</>
|
||
)}
|
||
</button>
|
||
</form>
|
||
)}
|
||
|
||
{/* Form for Register */}
|
||
{authMode === "register" && (
|
||
<form onSubmit={handleRegister} className="space-y-3.5 pt-1 font-vazir">
|
||
<div className="grid grid-cols-2 gap-3">
|
||
<div className="space-y-1">
|
||
<label htmlFor="reg-fname" className="text-[10px] font-black text-medical-gray-500 block pr-1 font-vazir">نام *</label>
|
||
<div className="relative">
|
||
<User className="absolute right-3 top-1/2 -translate-y-1/2 w-3.5 h-3.5 text-medical-gray-400 pointer-events-none" />
|
||
<input
|
||
id="reg-fname"
|
||
type="text"
|
||
required
|
||
value={firstName}
|
||
onChange={(e) => setFirstName(e.target.value)}
|
||
placeholder="نام..."
|
||
className="w-full bg-medical-gray-50/80 border border-medical-gray-200 rounded-xl py-2.5 pr-9 pl-3 focus:ring-2 focus:ring-canina-blue/20 outline-none text-xs font-bold font-vazir placeholder:font-vazir"
|
||
/>
|
||
</div>
|
||
</div>
|
||
<div className="space-y-1">
|
||
<label htmlFor="reg-lname" className="text-[10px] font-black text-medical-gray-500 block pr-1 font-vazir">نام خانوادگی *</label>
|
||
<input
|
||
id="reg-lname"
|
||
type="text"
|
||
required
|
||
value={lastName}
|
||
onChange={(e) => setLastName(e.target.value)}
|
||
placeholder="نام خانوادگی..."
|
||
className="w-full bg-medical-gray-50/80 border border-medical-gray-200 rounded-xl py-2.5 px-3 focus:ring-2 focus:ring-canina-blue/20 outline-none text-xs font-bold font-vazir placeholder:font-vazir"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="space-y-1">
|
||
<label htmlFor="reg-phone" className="text-[10px] font-black text-medical-gray-500 block pr-1 font-vazir">شماره همراه *</label>
|
||
<div className="relative">
|
||
<Phone className="absolute right-3 top-1/2 -translate-y-1/2 w-3.5 h-3.5 text-medical-gray-400 pointer-events-none" />
|
||
<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/80 border border-medical-gray-200 rounded-xl py-2.5 pr-9 pl-3 focus:ring-2 focus:ring-canina-blue/20 outline-none text-xs text-left font-vazir placeholder:font-vazir tracking-wider"
|
||
dir="ltr"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="space-y-1">
|
||
<label htmlFor="reg-email" className="text-[10px] font-black text-medical-gray-500 block pr-1 font-vazir">ایمیل (اختیاری)</label>
|
||
<div className="relative">
|
||
<Mail className="absolute right-3 top-1/2 -translate-y-1/2 w-3.5 h-3.5 text-medical-gray-400 pointer-events-none" />
|
||
<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/80 border border-medical-gray-200 rounded-xl py-2.5 pr-9 pl-3 focus:ring-2 focus:ring-canina-blue/20 outline-none text-xs text-left font-vazir placeholder:font-vazir"
|
||
dir="ltr"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="space-y-1">
|
||
<label htmlFor="reg-pass" className="text-[10px] font-black text-medical-gray-500 block pr-1 font-vazir">رمز عبور (حداقل ۶ کاراکتر) *</label>
|
||
<div className="relative">
|
||
<Lock className="absolute right-3 top-1/2 -translate-y-1/2 w-3.5 h-3.5 text-medical-gray-400 pointer-events-none" />
|
||
<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/80 border border-medical-gray-200 rounded-xl py-2.5 pr-9 pl-9 focus:ring-2 focus:ring-canina-blue/20 outline-none text-xs text-left font-vazir placeholder:font-vazir"
|
||
dir="ltr"
|
||
/>
|
||
<button
|
||
type="button"
|
||
onClick={() => setShowPassword(!showPassword)}
|
||
className="absolute left-2 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-3.5 h-3.5" /> : <Eye className="w-3.5 h-3.5" />}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<button
|
||
type="submit"
|
||
disabled={isLoading || !phoneNumber || !password || !firstName || !lastName}
|
||
className="w-full py-3.5 bg-canina-blue text-white rounded-2xl font-black text-xs hover:bg-canina-blue/90 disabled:opacity-50 transition-all shadow-md shadow-canina-blue/20 flex items-center justify-center gap-2 cursor-pointer mt-3 font-vazir"
|
||
>
|
||
{isLoading ? <RefreshCw className="w-4 h-4 animate-spin" /> : <>تکمیل و ایجاد حساب کاربری</>}
|
||
</button>
|
||
</form>
|
||
)}
|
||
|
||
{/* B2B Portal Action Link */}
|
||
{isB2BEnabled && (
|
||
<div className="pt-3 border-t border-medical-gray-100 font-vazir">
|
||
<button
|
||
type="button"
|
||
onClick={() => setSubView("b2b")}
|
||
className="w-full py-2.5 bg-amber-500/10 text-amber-900 border border-amber-300/60 rounded-xl text-[11px] font-black flex items-center justify-center gap-2 hover:bg-amber-500/15 transition-all cursor-pointer font-vazir"
|
||
>
|
||
<Building2 className="w-4 h-4 text-amber-600" />
|
||
<span>ثبتنام و احراز هویت خریداران عمده و کلینیکها</span>
|
||
</button>
|
||
</div>
|
||
)}
|
||
</motion.div>
|
||
)}
|
||
|
||
{/* 2. OTP VERIFICATION SUB-VIEW */}
|
||
{subView === "otp-verify" && (
|
||
<motion.div
|
||
key="otp-verify-subview"
|
||
initial={{ opacity: 0, x: 20 }}
|
||
animate={{ opacity: 1, x: 0 }}
|
||
exit={{ opacity: 0, x: -20 }}
|
||
className="space-y-5"
|
||
>
|
||
<div className="flex items-center justify-between pb-1">
|
||
<div className="flex items-center gap-2">
|
||
<button
|
||
type="button"
|
||
onClick={() => setSubView("main")}
|
||
className="p-1.5 rounded-lg bg-medical-gray-100 text-medical-gray-600 hover:bg-medical-gray-200 transition-all cursor-pointer"
|
||
>
|
||
<ArrowRight className="w-4 h-4" />
|
||
</button>
|
||
<span className="text-xs font-black text-medical-gray-900">تایید شماره تلفن</span>
|
||
</div>
|
||
<span className="text-[11px] font-mono text-canina-blue font-bold dir-ltr">{toPersian(phoneNumber)}</span>
|
||
</div>
|
||
|
||
<form onSubmit={(e) => { e.preventDefault(); triggerVerifyOtp(otpCode); }} className="space-y-4">
|
||
<div className="space-y-2">
|
||
<label className="text-[11px] font-black text-medical-gray-500 block text-center">
|
||
کد ۵ رقمی ارسالشده به موبایل خود را وارد نمایید
|
||
</label>
|
||
<OtpInput5
|
||
value={otpCode}
|
||
onChange={handleOtpChange}
|
||
disabled={isLoading}
|
||
autoFocus={true}
|
||
onComplete={(code) => triggerVerifyOtp(code)}
|
||
/>
|
||
<p className="text-[10px] text-medical-gray-400 text-center font-medium">
|
||
در صورت دریافت پیامک، کد به صورت خودکار شناسایی و وارد میشود.
|
||
</p>
|
||
</div>
|
||
|
||
<button
|
||
type="submit"
|
||
disabled={isLoading || otpCode.length < 5}
|
||
className="w-full py-4 bg-canina-blue text-white rounded-2xl font-black text-sm hover:bg-canina-blue/90 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" />
|
||
) : (
|
||
<>
|
||
<CheckCircle2 className="w-5 h-5" />
|
||
<span>تایید و ورود به سایت</span>
|
||
</>
|
||
)}
|
||
</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>
|
||
</form>
|
||
</motion.div>
|
||
)}
|
||
|
||
{/* 3. FORGOT PASSWORD PHONE SUB-VIEW */}
|
||
{subView === "forgot-phone" && (
|
||
<motion.div
|
||
key="forgot-phone-subview"
|
||
initial={{ opacity: 0, x: 20 }}
|
||
animate={{ opacity: 1, x: 0 }}
|
||
exit={{ opacity: 0, x: -20 }}
|
||
className="space-y-5 font-vazir"
|
||
>
|
||
<div className="flex items-center justify-between pb-1">
|
||
<div className="flex items-center gap-2">
|
||
<button
|
||
type="button"
|
||
onClick={() => setSubView("main")}
|
||
className="p-1.5 rounded-lg bg-medical-gray-100 text-medical-gray-600 hover:bg-medical-gray-200 transition-all cursor-pointer"
|
||
>
|
||
<ArrowRight className="w-4 h-4" />
|
||
</button>
|
||
<span className="text-xs font-black text-medical-gray-900 font-vazir">بازیابی رمز عبور</span>
|
||
</div>
|
||
</div>
|
||
|
||
<form onSubmit={handleForgotSendOtp} className="space-y-4 font-vazir">
|
||
<div className="space-y-1.5">
|
||
<label className="text-[11px] font-black text-medical-gray-500 block pr-1 font-vazir">شماره موبایل حساب کاربری</label>
|
||
<div className="relative">
|
||
<Phone 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="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-2xl py-3.5 pr-11 pl-4 focus:ring-2 focus:ring-canina-blue/20 outline-none text-sm text-left font-vazir placeholder:font-vazir tracking-wider"
|
||
dir="ltr"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<button
|
||
type="submit"
|
||
disabled={isLoading || phoneNumber.length < 11}
|
||
className="w-full py-3.5 bg-canina-blue text-white rounded-2xl font-black text-sm hover:bg-canina-blue/90 disabled:opacity-50 transition-all shadow-md shadow-canina-blue/20 flex items-center justify-center gap-2 cursor-pointer font-vazir"
|
||
>
|
||
{isLoading ? <RefreshCw className="w-4 h-4 animate-spin" /> : <>ارسال کد بازیابی</>}
|
||
</button>
|
||
</form>
|
||
</motion.div>
|
||
)}
|
||
|
||
{/* 4. FORGOT PASSWORD OTP SUB-VIEW */}
|
||
{subView === "forgot-otp" && (
|
||
<motion.div
|
||
key="forgot-otp-subview"
|
||
initial={{ opacity: 0, x: 20 }}
|
||
animate={{ opacity: 1, x: 0 }}
|
||
exit={{ opacity: 0, x: -20 }}
|
||
className="space-y-5 font-vazir"
|
||
>
|
||
<div className="flex items-center justify-between pb-1">
|
||
<div className="flex items-center gap-2">
|
||
<button
|
||
type="button"
|
||
onClick={() => setSubView("forgot-phone")}
|
||
className="p-1.5 rounded-lg bg-medical-gray-100 text-medical-gray-600 hover:bg-medical-gray-200 transition-all cursor-pointer"
|
||
>
|
||
<ArrowRight className="w-4 h-4" />
|
||
</button>
|
||
<span className="text-xs font-black text-medical-gray-900 font-vazir">کد تایید بازیابی رمز</span>
|
||
</div>
|
||
<span className="text-[11px] font-mono text-canina-blue font-bold dir-ltr">{toPersian(phoneNumber)}</span>
|
||
</div>
|
||
|
||
<form onSubmit={handleForgotVerifyOtp} className="space-y-4 font-vazir">
|
||
<div className="space-y-2">
|
||
<label className="text-[11px] font-black text-medical-gray-500 block text-center font-vazir">
|
||
کد ۵ رقمی بازیابی پیامکشده را وارد کنید
|
||
</label>
|
||
<OtpInput5
|
||
value={otpCode}
|
||
onChange={(val) => setOtpCode(val)}
|
||
disabled={isLoading}
|
||
autoFocus={true}
|
||
/>
|
||
|
||
</div>
|
||
|
||
<button
|
||
type="submit"
|
||
disabled={isLoading || otpCode.length < 5}
|
||
className="w-full py-4 bg-canina-blue text-white rounded-2xl font-black text-sm hover:bg-canina-blue/90 disabled:opacity-50 transition-all shadow-md shadow-canina-blue/20 flex items-center justify-center gap-2 cursor-pointer font-vazir"
|
||
>
|
||
{isLoading ? <RefreshCw className="w-4 h-4 animate-spin" /> : <>تایید و ادامه</>}
|
||
</button>
|
||
</form>
|
||
</motion.div>
|
||
)}
|
||
|
||
{/* 5. RESET PASSWORD SUB-VIEW */}
|
||
{subView === "reset-password" && (
|
||
<motion.div
|
||
key="reset-password-subview"
|
||
initial={{ opacity: 0, x: 20 }}
|
||
animate={{ opacity: 1, x: 0 }}
|
||
exit={{ opacity: 0, x: -20 }}
|
||
className="space-y-5 font-vazir"
|
||
>
|
||
<div className="flex items-center gap-2 pb-1">
|
||
<span className="text-xs font-black text-medical-gray-900 font-vazir">تنظیم رمز عبور جدید</span>
|
||
</div>
|
||
|
||
<form onSubmit={handleResetPassword} className="space-y-4 font-vazir">
|
||
<div className="space-y-1.5">
|
||
<label className="text-[11px] font-black text-medical-gray-500 block pr-1 font-vazir">رمز عبور جدید (حداقل ۶ کاراکتر)</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-2xl py-3.5 pr-11 pl-11 focus:ring-2 focus:ring-canina-blue/20 outline-none text-sm text-left font-vazir placeholder:font-vazir"
|
||
dir="ltr"
|
||
/>
|
||
<button
|
||
type="button"
|
||
onClick={() => setShowNewPassword(!showNewPassword)}
|
||
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}
|
||
>
|
||
{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-2xl font-black text-sm hover:bg-canina-blue/90 disabled:opacity-50 transition-all shadow-md shadow-canina-blue/20 flex items-center justify-center gap-2 cursor-pointer font-vazir"
|
||
>
|
||
{isLoading ? <RefreshCw className="w-4 h-4 animate-spin" /> : <>ثبت رمز عبور جدید و ورود</>}
|
||
</button>
|
||
</form>
|
||
</motion.div>
|
||
)}
|
||
|
||
{/* 6. B2B WHOLESALE REQUEST SUB-VIEW */}
|
||
{subView === "b2b" && (
|
||
<motion.div
|
||
key="b2b-subview"
|
||
initial={{ opacity: 0, x: 20 }}
|
||
animate={{ opacity: 1, x: 0 }}
|
||
exit={{ opacity: 0, x: -20 }}
|
||
className="space-y-4 font-vazir"
|
||
>
|
||
<div className="flex items-center justify-between pb-1">
|
||
<div className="flex items-center gap-2">
|
||
<button
|
||
type="button"
|
||
onClick={() => setSubView("main")}
|
||
className="p-1.5 rounded-lg bg-medical-gray-100 text-medical-gray-600 hover:bg-medical-gray-200 transition-all cursor-pointer"
|
||
>
|
||
<ArrowRight className="w-4 h-4" />
|
||
</button>
|
||
<span className="text-xs font-black text-medical-gray-900 font-vazir">ثبتنام خریدار عمده و همکار</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="p-3 bg-amber-50/80 border border-amber-200 rounded-xl text-[11px] text-amber-900 font-bold leading-relaxed font-vazir">
|
||
ثبت شماره نظام دامپزشکی یا پروانه کسب جهت دسترسی به قیمتهای همکاری و سفارشات عمده الزامی است.
|
||
</div>
|
||
|
||
<form
|
||
onSubmit={(e) => {
|
||
e.preventDefault();
|
||
toast.success("درخواست احراز هویت خریدار عمده با موفقیت ثبت شد و پس از بررسی فعال خواهد گردید.");
|
||
setSubView("main");
|
||
}}
|
||
className="space-y-3 font-vazir"
|
||
>
|
||
<div className="space-y-1">
|
||
<label className="text-[10px] font-black text-medical-gray-500 block pr-1 font-vazir">نام مجموعه / داروخانه / کلینیک *</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.5 px-3 focus:ring-2 focus:ring-canina-blue/20 outline-none text-xs font-bold font-vazir placeholder:font-vazir"
|
||
/>
|
||
</div>
|
||
|
||
<div className="space-y-1">
|
||
<label className="text-[10px] font-black text-medical-gray-500 block pr-1 font-vazir">شماره نظام دامپزشکی / پروانه کسب *</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.5 px-3 focus:ring-2 focus:ring-canina-blue/20 outline-none text-xs text-left font-vazir placeholder:font-vazir tracking-wider"
|
||
dir="ltr"
|
||
/>
|
||
</div>
|
||
|
||
<div className="space-y-1">
|
||
<label className="text-[10px] font-black text-medical-gray-500 block pr-1 font-vazir">موبایل مسئول *</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.5 px-3 focus:ring-2 focus:ring-canina-blue/20 outline-none text-xs text-left font-vazir placeholder:font-vazir tracking-wider"
|
||
dir="ltr"
|
||
/>
|
||
</div>
|
||
|
||
<div
|
||
onClick={() => {
|
||
setDocumentUploaded(true);
|
||
toast.success("تصویر مدرک با موفقیت بارگذاری شد.");
|
||
}}
|
||
className={cn(
|
||
"p-3.5 border-2 border-dashed rounded-xl text-center cursor-pointer transition-all font-vazir",
|
||
documentUploaded ? "border-emerald-500 bg-emerald-50" : "border-medical-gray-300 hover:border-canina-blue bg-medical-gray-50/50"
|
||
)}
|
||
>
|
||
{documentUploaded ? (
|
||
<div className="flex items-center justify-center gap-2 text-emerald-700 font-bold text-xs font-vazir">
|
||
<Check className="w-4 h-4" />
|
||
<span>تصویر مدرک بارگذاری شد</span>
|
||
</div>
|
||
) : (
|
||
<div className="flex flex-col items-center gap-1 text-medical-gray-500 text-xs font-vazir">
|
||
<Upload className="w-4 h-4 text-canina-blue" />
|
||
<span className="text-[11px] font-bold font-vazir">بارگذاری تصویر کارت نظام یا پروانه کسب</span>
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
<button
|
||
type="submit"
|
||
disabled={!businessName || !medicalLicense || !phoneNumber}
|
||
className="w-full py-3 bg-canina-blue text-white rounded-xl font-black text-xs hover:bg-canina-blue/90 disabled:opacity-50 transition-all cursor-pointer mt-2 font-vazir"
|
||
>
|
||
ارسال درخواست بررسی به کارشناسان
|
||
</button>
|
||
</form>
|
||
</motion.div>
|
||
)}
|
||
</AnimatePresence>
|
||
</div>
|
||
</motion.div>
|
||
</div>
|
||
)}
|
||
</AnimatePresence>
|
||
);
|
||
}
|