canina/frontend/admin-panel/src/pages/Login.tsx

248 lines
11 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.

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>
);
}