248 lines
11 KiB
TypeScript
248 lines
11 KiB
TypeScript
import { useState } from 'react';
|
||
import { useNavigate } from 'react-router-dom';
|
||
import { Lock, Mail, Phone, ChevronLeft, KeyRound } from 'lucide-react';
|
||
import { toast } from 'react-hot-toast';
|
||
import api from '../services/api';
|
||
import { useAdminAuthStore } from '../store/adminAuthStore';
|
||
import type { ApiResponse, AuthResponseData } from '../types/admin';
|
||
|
||
export default function Login() {
|
||
const [loginMode, setLoginMode] = useState<'password' | 'otp'>('password');
|
||
const [email, setEmail] = useState('');
|
||
const [password, setPassword] = useState('');
|
||
|
||
const [phoneNumber, setPhoneNumber] = useState('');
|
||
const [otpCode, setOtpCode] = useState('');
|
||
const [otpSent, setOtpSent] = useState(false);
|
||
|
||
const [isLoading, setIsLoading] = useState(false);
|
||
const navigate = useNavigate();
|
||
const setAuth = useAdminAuthStore((state) => state.setAuth);
|
||
|
||
const handlePasswordLogin = async (e: React.FormEvent) => {
|
||
e.preventDefault();
|
||
setIsLoading(true);
|
||
|
||
try {
|
||
const response = await api.post<ApiResponse<AuthResponseData>>('/auth/admin-login', { email, password });
|
||
if (response.data?.success && response.data.data) {
|
||
setAuth(response.data.data.accessToken, response.data.data.user);
|
||
toast.success('ورود با موفقیت انجام شد');
|
||
navigate('/');
|
||
} else {
|
||
toast.error('اطلاعات ورود نامعتبر است');
|
||
}
|
||
} catch {
|
||
// Toast notification is handled by api.ts interceptor
|
||
} finally {
|
||
setIsLoading(false);
|
||
}
|
||
};
|
||
|
||
const handleSendOtp = async (e: React.FormEvent) => {
|
||
e.preventDefault();
|
||
if (!phoneNumber || phoneNumber.length < 10) {
|
||
toast.error('شماره موبایل معتبر وارد کنید');
|
||
return;
|
||
}
|
||
setIsLoading(true);
|
||
|
||
try {
|
||
const response = await api.post<ApiResponse>('/auth/send-otp', { phoneNumber });
|
||
if (response.data?.success) {
|
||
toast.success(response.data.message || 'کد تایید پیامک شد');
|
||
setOtpSent(true);
|
||
}
|
||
} catch {
|
||
toast.error('خطا در ارسال کد تایید');
|
||
} finally {
|
||
setIsLoading(false);
|
||
}
|
||
};
|
||
|
||
const handleVerifyOtp = async (e: React.FormEvent) => {
|
||
e.preventDefault();
|
||
if (!otpCode || otpCode.length < 4) {
|
||
toast.error('کد تایید معتبر وارد کنید');
|
||
return;
|
||
}
|
||
setIsLoading(true);
|
||
|
||
try {
|
||
const response = await api.post<ApiResponse<AuthResponseData>>('/auth/verify-otp', { phoneNumber, code: otpCode });
|
||
if (response.data?.success && response.data.data) {
|
||
setAuth(response.data.data.accessToken, response.data.data.user);
|
||
toast.success('ورود موفقیتآمیز بود');
|
||
navigate('/');
|
||
} else {
|
||
toast.error('کد تایید نامعتبر است');
|
||
}
|
||
} catch {
|
||
toast.error('کد تایید اشتباه یا منقضی شده است');
|
||
} finally {
|
||
setIsLoading(false);
|
||
}
|
||
};
|
||
|
||
return (
|
||
<div className="min-h-screen bg-gray-50 flex flex-col justify-center py-12 sm:px-6 lg:px-8 font-vazir" dir="rtl">
|
||
<div className="sm:mx-auto sm:w-full sm:max-w-md">
|
||
<div className="w-20 h-20 bg-purple-600 rounded-2xl mx-auto flex items-center justify-center shadow-lg shadow-purple-200">
|
||
<Lock className="w-10 h-10 text-white" />
|
||
</div>
|
||
<h2 className="mt-6 text-center text-3xl font-black text-gray-900">
|
||
ورود به پنل مدیریت
|
||
</h2>
|
||
<p className="mt-2 text-center text-sm text-gray-600 font-bold">
|
||
کانینا | لابراتوار تخصصی مکملهای درمانی حیوانات
|
||
</p>
|
||
</div>
|
||
|
||
<div className="mt-8 sm:mx-auto sm:w-full sm:max-w-md">
|
||
<div className="bg-white py-8 px-4 shadow-xl shadow-gray-100 sm:rounded-3xl sm:px-10 border border-gray-100">
|
||
{/* Toggle Login Mode */}
|
||
<div className="flex bg-gray-100 p-1 rounded-xl mb-6">
|
||
<button
|
||
type="button"
|
||
onClick={() => { setLoginMode('password'); setOtpSent(false); }}
|
||
className={`flex-1 py-2 text-xs font-black rounded-lg transition-all ${loginMode === 'password' ? 'bg-white text-purple-600 shadow-sm' : 'text-gray-500 hover:text-gray-900'}`}
|
||
>
|
||
ورود با رمز عبور
|
||
</button>
|
||
<button
|
||
type="button"
|
||
onClick={() => { setLoginMode('otp'); setOtpSent(false); }}
|
||
className={`flex-1 py-2 text-xs font-black rounded-lg transition-all ${loginMode === 'otp' ? 'bg-white text-purple-600 shadow-sm' : 'text-gray-500 hover:text-gray-900'}`}
|
||
>
|
||
ورود با پیامک (OTP)
|
||
</button>
|
||
</div>
|
||
|
||
{loginMode === 'password' ? (
|
||
<form className="space-y-6" onSubmit={handlePasswordLogin}>
|
||
<div>
|
||
<label className="block text-sm font-black text-gray-700">آدرس ایمیل ادمین</label>
|
||
<div className="mt-2 relative">
|
||
<div className="absolute inset-y-0 right-0 pl-3 flex items-center pr-3 pointer-events-none">
|
||
<Mail className="h-5 w-5 text-gray-400" />
|
||
</div>
|
||
<input
|
||
type="email"
|
||
required
|
||
value={email}
|
||
onChange={(e) => setEmail(e.target.value)}
|
||
className="block w-full pl-3 pr-10 py-3 border border-gray-200 rounded-xl focus:ring-purple-500 focus:border-purple-500 sm:text-sm bg-gray-50 font-bold transition-all"
|
||
placeholder="admin@canino-iran.com"
|
||
dir="ltr"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="block text-sm font-black text-gray-700">رمز عبور</label>
|
||
<div className="mt-2 relative">
|
||
<div className="absolute inset-y-0 right-0 pl-3 flex items-center pr-3 pointer-events-none">
|
||
<Lock className="h-5 w-5 text-gray-400" />
|
||
</div>
|
||
<input
|
||
type="password"
|
||
required
|
||
value={password}
|
||
onChange={(e) => setPassword(e.target.value)}
|
||
className="block w-full pl-3 pr-10 py-3 border border-gray-200 rounded-xl focus:ring-purple-500 focus:border-purple-500 sm:text-sm bg-gray-50 font-bold transition-all text-left"
|
||
placeholder="••••••••"
|
||
dir="ltr"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<div>
|
||
<button
|
||
type="submit"
|
||
disabled={isLoading}
|
||
className="w-full flex justify-center items-center gap-2 py-3 px-4 border border-transparent rounded-xl shadow-sm text-sm font-black text-white bg-purple-600 hover:bg-purple-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-purple-500 transition-all disabled:opacity-50 disabled:cursor-wait"
|
||
>
|
||
{isLoading ? 'در حال ورود...' : 'ورود به سیستم'}
|
||
{!isLoading && <ChevronLeft className="w-5 h-5" />}
|
||
</button>
|
||
</div>
|
||
</form>
|
||
) : (
|
||
<div className="space-y-6">
|
||
{!otpSent ? (
|
||
<form className="space-y-6" onSubmit={handleSendOtp}>
|
||
<div>
|
||
<label className="block text-sm font-black text-gray-700">شماره تلفن همراه</label>
|
||
<div className="mt-2 relative">
|
||
<div className="absolute inset-y-0 right-0 pl-3 flex items-center pr-3 pointer-events-none">
|
||
<Phone className="h-5 w-5 text-gray-400" />
|
||
</div>
|
||
<input
|
||
type="tel"
|
||
required
|
||
value={phoneNumber}
|
||
onChange={(e) => setPhoneNumber(e.target.value)}
|
||
className="block w-full pl-3 pr-10 py-3 border border-gray-200 rounded-xl focus:ring-purple-500 focus:border-purple-500 sm:text-sm bg-gray-50 font-bold transition-all text-left"
|
||
placeholder="09123456789"
|
||
dir="ltr"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<button
|
||
type="submit"
|
||
disabled={isLoading}
|
||
className="w-full flex justify-center items-center gap-2 py-3 px-4 border border-transparent rounded-xl shadow-sm text-sm font-black text-white bg-purple-600 hover:bg-purple-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-purple-500 transition-all disabled:opacity-50 disabled:cursor-wait"
|
||
>
|
||
{isLoading ? 'در حال ارسال کد...' : 'ارسال کد تایید پیامکی'}
|
||
{!isLoading && <ChevronLeft className="w-5 h-5" />}
|
||
</button>
|
||
</form>
|
||
) : (
|
||
<form className="space-y-6" onSubmit={handleVerifyOtp}>
|
||
<div>
|
||
<div className="flex justify-between items-center mb-1">
|
||
<label className="block text-sm font-black text-gray-700">کد تایید ۵ رقمی</label>
|
||
<button
|
||
type="button"
|
||
onClick={() => setOtpSent(false)}
|
||
className="text-xs text-purple-600 font-bold hover:underline"
|
||
>
|
||
تغییر شماره
|
||
</button>
|
||
</div>
|
||
<div className="mt-2 relative">
|
||
<div className="absolute inset-y-0 right-0 pl-3 flex items-center pr-3 pointer-events-none">
|
||
<KeyRound className="h-5 w-5 text-gray-400" />
|
||
</div>
|
||
<input
|
||
type="text"
|
||
required
|
||
maxLength={6}
|
||
value={otpCode}
|
||
onChange={(e) => setOtpCode(e.target.value)}
|
||
className="block w-full pl-3 pr-10 py-3 border border-gray-200 rounded-xl focus:ring-purple-500 focus:border-purple-500 sm:text-sm bg-gray-50 font-bold transition-all text-center tracking-widest text-lg"
|
||
placeholder="12345"
|
||
dir="ltr"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<button
|
||
type="submit"
|
||
disabled={isLoading}
|
||
className="w-full flex justify-center items-center gap-2 py-3 px-4 border border-transparent rounded-xl shadow-sm text-sm font-black text-white bg-purple-600 hover:bg-purple-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-purple-500 transition-all disabled:opacity-50 disabled:cursor-wait"
|
||
>
|
||
{isLoading ? 'در حال تایید...' : 'تایید و ورود به پنل'}
|
||
{!isLoading && <ChevronLeft className="w-5 h-5" />}
|
||
</button>
|
||
</form>
|
||
)}
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|