389 lines
17 KiB
TypeScript
389 lines
17 KiB
TypeScript
"use client";
|
||
import React, { useState, useEffect, useRef } from "react";
|
||
import { motion, AnimatePresence } from "motion/react";
|
||
import { X, User, Building2, Heart, ShieldCheck, Phone, Key, LogIn, ArrowRight, RefreshCw } from "lucide-react";
|
||
import { useUserStore, UserRole } from "../lib/store/userStore";
|
||
import { authService } from "../lib/services/authService";
|
||
import { toast } from "sonner";
|
||
import { toPersian } from "../lib/utils";
|
||
|
||
interface AuthModalProps {
|
||
isOpen: boolean;
|
||
onClose: () => void;
|
||
}
|
||
|
||
export default function AuthModal({ isOpen, onClose }: AuthModalProps) {
|
||
const { setRole, setLoggedIn, fetchProfile } = useUserStore();
|
||
const [view, setView] = useState<"quick" | "phone" | "otp">("quick");
|
||
const [phoneNumber, setPhoneNumber] = useState("");
|
||
const [otpCode, setOtpCode] = useState("");
|
||
const [isLoading, setIsLoading] = useState(false);
|
||
const [countdown, setCountdown] = useState(0);
|
||
const otpInputRef = useRef<HTMLInputElement>(null);
|
||
|
||
useEffect(() => {
|
||
if (view === "otp") {
|
||
const timer = setTimeout(() => {
|
||
otpInputRef.current?.focus();
|
||
}, 100);
|
||
return () => clearTimeout(timer);
|
||
}
|
||
}, [view]);
|
||
|
||
// Reset modal state on open/close
|
||
useEffect(() => {
|
||
if (!isOpen) {
|
||
setView("quick");
|
||
setPhoneNumber("");
|
||
setOtpCode("");
|
||
setIsLoading(false);
|
||
setCountdown(0);
|
||
}
|
||
}, [isOpen]);
|
||
|
||
// Cooldown countdown timer
|
||
useEffect(() => {
|
||
if (countdown > 0) {
|
||
const timer = setTimeout(() => setCountdown(countdown - 1), 1000);
|
||
return () => clearTimeout(timer);
|
||
}
|
||
}, [countdown]);
|
||
|
||
const handleQuickLogin = async (targetRole: UserRole) => {
|
||
if (targetRole === "User_Guest") {
|
||
setRole("User_Guest");
|
||
setLoggedIn(false);
|
||
toast.success("به عنوان کاربر مهمان وارد شدید.");
|
||
onClose();
|
||
return;
|
||
}
|
||
|
||
setIsLoading(true);
|
||
const testPhone = targetRole === "User_PetOwner" ? "09121111111" : "09122222222";
|
||
|
||
try {
|
||
// Step 1: Send OTP to test phone
|
||
const sendRes = await authService.sendOtp(testPhone);
|
||
const code = (sendRes as any).code;
|
||
if (!code) {
|
||
throw new Error("کد تایید تستی تولید نشد");
|
||
}
|
||
|
||
// Step 2: Verify OTP
|
||
const verifyRes = await authService.verifyOtp(testPhone, code);
|
||
if (verifyRes.success) {
|
||
// Force state sync and fetch profile
|
||
setRole(targetRole);
|
||
await fetchProfile();
|
||
toast.success(`ورود سریع موفق! در نقش ${targetRole === "User_Partner" ? 'همکار' : 'صاحب پت'} وارد شدید.`);
|
||
onClose();
|
||
}
|
||
} catch (err: any) {
|
||
toast.error(`خطا در ورود سریع: ${err.message}`);
|
||
} finally {
|
||
setIsLoading(false);
|
||
}
|
||
};
|
||
|
||
const handleSendOtp = async (e: React.FormEvent) => {
|
||
e.preventDefault();
|
||
const cleanPhone = phoneNumber.trim();
|
||
if (!/^09\d{9}$/.test(cleanPhone)) {
|
||
toast.error("شماره موبایل نامعتبر است");
|
||
return;
|
||
}
|
||
|
||
setIsLoading(true);
|
||
try {
|
||
const res = await authService.sendOtp(cleanPhone);
|
||
if ((res as any).code) {
|
||
toast.info(`کد تایید (تست): ${(res as any).code}`, { duration: 10000 });
|
||
} else {
|
||
toast.success("کد تایید پیامک شد");
|
||
}
|
||
setView("otp");
|
||
setCountdown(120);
|
||
} catch (err: any) {
|
||
toast.error(err.message || "خطا در ارسال کد تایید");
|
||
} finally {
|
||
setIsLoading(false);
|
||
}
|
||
};
|
||
|
||
const handleVerifyOtp = async (e: React.FormEvent) => {
|
||
e.preventDefault();
|
||
const cleanCode = otpCode.trim();
|
||
if (cleanCode.length !== 5) {
|
||
toast.error("کد باید ۵ رقم باشد");
|
||
return;
|
||
}
|
||
|
||
setIsLoading(true);
|
||
try {
|
||
const response = await authService.verifyOtp(phoneNumber.trim(), cleanCode);
|
||
if (response.success) {
|
||
await fetchProfile();
|
||
toast.success("ورود موفقیتآمیز بود");
|
||
onClose();
|
||
}
|
||
} catch (err: any) {
|
||
toast.error(err.message || "کد تایید نامعتبر است");
|
||
} finally {
|
||
setIsLoading(false);
|
||
}
|
||
};
|
||
|
||
const handleResendOtp = async () => {
|
||
if (countdown > 0) return;
|
||
setIsLoading(true);
|
||
try {
|
||
await authService.sendOtp(phoneNumber.trim());
|
||
toast.success("کد تایید جدید ارسال شد");
|
||
setCountdown(120);
|
||
} catch (err: any) {
|
||
toast.error(err.message || "خطا در ارسال مجدد کد");
|
||
} finally {
|
||
setIsLoading(false);
|
||
}
|
||
};
|
||
|
||
return (
|
||
<AnimatePresence>
|
||
{isOpen && (
|
||
<>
|
||
{/* Backdrop */}
|
||
<motion.div
|
||
initial={{ opacity: 0 }}
|
||
animate={{ opacity: 1 }}
|
||
exit={{ opacity: 0 }}
|
||
className="fixed inset-0 bg-black/60 backdrop-blur-sm z-[60]"
|
||
onClick={onClose}
|
||
/>
|
||
|
||
{/* Modal content */}
|
||
<motion.div
|
||
initial={{ opacity: 0, scale: 0.9, y: 20 }}
|
||
animate={{ opacity: 1, scale: 1, y: 0 }}
|
||
exit={{ opacity: 0, scale: 0.9, y: 20 }}
|
||
className="fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-full max-w-md bg-white rounded-[2.5rem] shadow-2xl z-[70] overflow-hidden border border-medical-gray-100 font-vazir"
|
||
dir="rtl"
|
||
>
|
||
{/* Top highlight bar */}
|
||
<div className="absolute top-0 left-0 w-full h-2 bg-gradient-to-r from-canina-blue to-indigo-500" />
|
||
|
||
<div className="p-8 relative">
|
||
{/* Close Button */}
|
||
<button
|
||
onClick={onClose}
|
||
className="absolute top-6 left-6 p-2 rounded-full hover:bg-medical-gray-50 text-medical-gray-400 hover:text-medical-gray-600 transition-colors"
|
||
>
|
||
<X className="w-6 h-6" />
|
||
</button>
|
||
|
||
{/* Title Header */}
|
||
<div className="flex flex-col items-center text-center mb-8 pt-4">
|
||
<div className="w-20 h-20 bg-canina-blue/10 rounded-3xl flex items-center justify-center mb-4 shadow-inner">
|
||
<ShieldCheck className="w-10 h-10 text-canina-blue" />
|
||
</div>
|
||
<h2 className="text-3xl font-black text-medical-gray-900 mb-2 italic">
|
||
ورود به دنیای کانینا
|
||
</h2>
|
||
<p className="text-medical-gray-500 text-sm font-bold max-w-[280px]">
|
||
{view === "otp"
|
||
? `کد تایید ارسال شده به شماره ${toPersian(phoneNumber)} را وارد کنید`
|
||
: "برای دسترسی به پرونده سلامت و سفارشات وارد شوید"}
|
||
</p>
|
||
</div>
|
||
|
||
{/* Content Panel */}
|
||
<AnimatePresence mode="wait">
|
||
{view === "quick" && (
|
||
<motion.div
|
||
key="quick-view"
|
||
initial={{ opacity: 0, y: 10 }}
|
||
animate={{ opacity: 1, y: 0 }}
|
||
exit={{ opacity: 0, y: -10 }}
|
||
className="space-y-6"
|
||
>
|
||
<button
|
||
onClick={() => setView("phone")}
|
||
className="w-full bg-medical-gray-50 border border-medical-gray-200 rounded-2xl p-5 flex items-center justify-between group hover:border-canina-blue transition-all cursor-pointer shadow-sm"
|
||
>
|
||
<div className="flex items-center gap-3">
|
||
<Phone className="w-5 h-5 text-canina-blue" />
|
||
<span className="font-black text-sm text-medical-gray-700">ورود با شماره موبایل</span>
|
||
</div>
|
||
<div className="w-8 h-8 rounded-xl bg-white border border-medical-gray-100 flex items-center justify-center shadow-sm text-canina-blue group-hover:bg-canina-blue group-hover:text-white transition-all">←</div>
|
||
</button>
|
||
|
||
<div className="pt-6 border-t border-medical-gray-100">
|
||
<p className="text-[10px] font-black text-medical-gray-400 uppercase tracking-widest text-center mb-4">ورود سریع تستی (با دیتابیس واقعی)</p>
|
||
<div className="grid grid-cols-3 gap-3">
|
||
<button
|
||
onClick={() => handleQuickLogin("User_Guest")}
|
||
disabled={isLoading}
|
||
className="flex flex-col items-center gap-2 p-3 rounded-2xl border border-medical-gray-100 hover:bg-medical-gray-50 transition-all group cursor-pointer disabled:opacity-50"
|
||
>
|
||
<User className="w-6 h-6 text-medical-gray-400 group-hover:text-canina-blue" />
|
||
<span className="text-[10px] font-black text-medical-gray-700">مهمان</span>
|
||
</button>
|
||
<button
|
||
onClick={() => handleQuickLogin("User_PetOwner")}
|
||
disabled={isLoading}
|
||
className="flex flex-col items-center gap-2 p-3 rounded-2xl border border-medical-gray-100 hover:bg-medical-gray-50 transition-all group cursor-pointer disabled:opacity-50"
|
||
>
|
||
{isLoading ? (
|
||
<RefreshCw className="w-6 h-6 text-canina-blue animate-spin" />
|
||
) : (
|
||
<Heart className="w-6 h-6 text-medical-gray-400 group-hover:text-canina-blue" />
|
||
)}
|
||
<span className="text-[10px] font-black text-medical-gray-700">صاحب پت</span>
|
||
</button>
|
||
<button
|
||
onClick={() => handleQuickLogin("User_Partner")}
|
||
disabled={isLoading}
|
||
className="flex flex-col items-center gap-2 p-3 rounded-2xl border border-medical-gray-100 hover:bg-medical-gray-50 transition-all group cursor-pointer disabled:opacity-50"
|
||
>
|
||
{isLoading ? (
|
||
<RefreshCw className="w-6 h-6 text-canina-blue animate-spin" />
|
||
) : (
|
||
<Building2 className="w-6 h-6 text-medical-gray-400 group-hover:text-canina-blue" />
|
||
)}
|
||
<span className="text-[10px] font-black text-medical-gray-700">همکار</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</motion.div>
|
||
)}
|
||
|
||
{view === "phone" && (
|
||
<motion.form
|
||
key="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("quick")}
|
||
className="text-[10px] font-black text-canina-blue hover:underline"
|
||
>
|
||
برگشت به ورود سریع
|
||
</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 tracking-widest"
|
||
/>
|
||
</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("phone")}
|
||
className="text-[10px] font-black text-canina-blue flex items-center gap-1 hover:underline"
|
||
>
|
||
<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" />
|
||
<input
|
||
ref={otpInputRef}
|
||
type="text"
|
||
maxLength={5}
|
||
placeholder="کد ۵ رقمی"
|
||
value={otpCode}
|
||
onChange={(e) => setOtpCode(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-black text-2xl text-center tracking-[0.5em]"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="text-center">
|
||
{countdown > 0 ? (
|
||
<span className="text-xs font-bold text-medical-gray-400">
|
||
ارسال مجدد کد پس از {toPersian(countdown)} ثانیه
|
||
</span>
|
||
) : (
|
||
<button
|
||
type="button"
|
||
onClick={handleResendOtp}
|
||
disabled={isLoading}
|
||
className="text-xs font-black text-canina-blue hover:underline flex items-center gap-1.5 justify-center mx-auto"
|
||
>
|
||
<RefreshCw className="w-3.5 h-3.5" />
|
||
ارسال مجدد کد تایید
|
||
</button>
|
||
)}
|
||
</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" />
|
||
) : (
|
||
<>
|
||
<LogIn className="w-6 h-6" />
|
||
تایید کد و ورود
|
||
</>
|
||
)}
|
||
</button>
|
||
</motion.form>
|
||
)}
|
||
</AnimatePresence>
|
||
</div>
|
||
</motion.div>
|
||
</>
|
||
)}
|
||
</AnimatePresence>
|
||
);
|
||
}
|