Refactor code structure for improved readability and maintainability
This commit is contained in:
@@ -3,9 +3,8 @@ import { useForm } from 'react-hook-form';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { z } from 'zod';
|
||||
import { toast } from 'sonner';
|
||||
import { EyeIcon, EyeSlashIcon, HeartIcon, SunIcon, MoonIcon } from '@heroicons/react/24/outline';
|
||||
import { EyeIcon, EyeSlashIcon } from '@heroicons/react/24/outline';
|
||||
import { useAuthStore } from '../stores/authStore';
|
||||
import { useUiStore } from '../stores/uiStore';
|
||||
|
||||
const schema = z.object({
|
||||
mobile: z.string().min(10, 'شماره موبایل معتبر نیست'),
|
||||
@@ -16,18 +15,12 @@ type FormData = z.infer<typeof schema>;
|
||||
|
||||
export default function LoginPage() {
|
||||
const login = useAuthStore((s) => s.login);
|
||||
const darkMode = useUiStore((s) => s.darkMode);
|
||||
const toggleDarkMode = useUiStore((s) => s.toggleDarkMode);
|
||||
const [showPass, setShowPass] = useState(false);
|
||||
|
||||
const { register, handleSubmit, formState: { errors, isSubmitting } } = useForm<FormData>({
|
||||
resolver: zodResolver(schema),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
document.documentElement.classList.toggle('dark', darkMode);
|
||||
}, [darkMode]);
|
||||
|
||||
const onSubmit = async (data: FormData) => {
|
||||
try {
|
||||
const res = await fetch('/api/v1/user/login', {
|
||||
@@ -51,140 +44,81 @@ export default function LoginPage() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex bg-slate-50 dark:bg-gray-950 transition-colors duration-200">
|
||||
|
||||
{/* ── Left panel: branding ───────────────── */}
|
||||
<div className="hidden lg:flex flex-col flex-1 relative overflow-hidden bg-[#100c22]">
|
||||
{/* Gradient orbs */}
|
||||
<div className="absolute top-1/4 right-1/4 w-72 h-72 bg-violet-600/30 rounded-full blur-3xl" />
|
||||
<div className="absolute bottom-1/4 left-1/4 w-96 h-96 bg-purple-800/20 rounded-full blur-3xl" />
|
||||
|
||||
<div className="relative z-10 flex flex-col h-full p-12">
|
||||
{/* Logo */}
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-xl bg-gradient-to-br from-violet-500 to-purple-700 flex items-center justify-center shadow-lg shadow-violet-900/50">
|
||||
<HeartIcon className="w-5 h-5 text-white" />
|
||||
</div>
|
||||
<span className="text-white font-bold text-xl tracking-tight">ClinicPro</span>
|
||||
<div style={{
|
||||
minHeight: '100vh', display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
background: 'var(--bg)', padding: 20,
|
||||
}}>
|
||||
<div className="card card-pad" style={{ width: '100%', maxWidth: 420 }}>
|
||||
{/* Logo */}
|
||||
<div style={{ textAlign: 'center', marginBottom: 28 }}>
|
||||
<div className="brand-logo" style={{
|
||||
margin: '0 auto 16px', width: 54, height: 54, borderRadius: 16,
|
||||
fontSize: 26,
|
||||
}}>
|
||||
♥
|
||||
</div>
|
||||
|
||||
{/* Center content */}
|
||||
<div className="flex-1 flex flex-col items-center justify-center text-center px-8">
|
||||
<div className="w-20 h-20 rounded-3xl bg-gradient-to-br from-violet-500/20 to-purple-700/20 border border-violet-500/20 flex items-center justify-center mb-8">
|
||||
<HeartIcon className="w-10 h-10 text-violet-400" />
|
||||
</div>
|
||||
<h2 className="text-3xl font-bold text-white mb-4 leading-snug">
|
||||
سیستم مدیریت<br />کلینیک هوشمند
|
||||
</h2>
|
||||
<p className="text-slate-400 text-base leading-relaxed max-w-sm">
|
||||
مدیریت نوبتدهی، پزشکان، کلینیکها و پرداختها در یک پنل یکپارچه
|
||||
</p>
|
||||
|
||||
{/* Feature pills */}
|
||||
<div className="flex flex-wrap justify-center gap-2 mt-8">
|
||||
{['نوبتدهی آنلاین', 'مدیریت پزشکان', 'گزارشگیری', 'پرداخت امن'].map((f) => (
|
||||
<span key={f} className="px-3 py-1.5 rounded-full text-xs font-medium bg-white/5 text-slate-300 border border-white/10">
|
||||
{f}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Bottom note */}
|
||||
<p className="text-slate-600 text-xs text-center">نسخه ۱.۰.۰ — تمامی حقوق محفوظ است</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ── Right panel: form ──────────────────── */}
|
||||
<div className="flex flex-col w-full lg:w-[480px] shrink-0 relative">
|
||||
{/* Dark mode toggle */}
|
||||
<div className="absolute top-5 left-5">
|
||||
<button
|
||||
onClick={toggleDarkMode}
|
||||
className="w-9 h-9 flex items-center justify-center rounded-xl bg-slate-200/70 dark:bg-gray-800 hover:bg-slate-300 dark:hover:bg-gray-700 text-slate-500 dark:text-slate-400 transition-colors"
|
||||
title={darkMode ? 'حالت روشن' : 'حالت تاریک'}
|
||||
>
|
||||
{darkMode
|
||||
? <SunIcon className="w-5 h-5 text-amber-400" />
|
||||
: <MoonIcon className="w-5 h-5" />}
|
||||
</button>
|
||||
<h1 style={{ fontSize: 22, fontWeight: 800, marginBottom: 6 }}>ورود به پنل ادمین</h1>
|
||||
<p className="muted" style={{ fontSize: 13 }}>اطلاعات حساب مدیریتی خود را وارد کنید</p>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col flex-1 items-center justify-center px-8 sm:px-12 py-12">
|
||||
{/* Mobile logo */}
|
||||
<div className="lg:hidden flex items-center gap-3 mb-10">
|
||||
<div className="w-10 h-10 rounded-xl bg-gradient-to-br from-violet-500 to-purple-700 flex items-center justify-center shadow-md">
|
||||
<HeartIcon className="w-5 h-5 text-white" />
|
||||
</div>
|
||||
<span className="text-slate-900 dark:text-white font-bold text-xl">ClinicPro</span>
|
||||
<form onSubmit={handleSubmit(onSubmit)} noValidate>
|
||||
{/* Mobile */}
|
||||
<div className="form-row">
|
||||
<label>شماره موبایل</label>
|
||||
<input
|
||||
{...register('mobile')}
|
||||
className={`input${errors.mobile ? ' err' : ''}`}
|
||||
type="tel"
|
||||
dir="ltr"
|
||||
placeholder="09xxxxxxxxx"
|
||||
autoComplete="username"
|
||||
style={{ textAlign: 'right' }}
|
||||
/>
|
||||
{errors.mobile && <div className="err-text">{errors.mobile.message}</div>}
|
||||
</div>
|
||||
|
||||
<div className="w-full max-w-sm">
|
||||
<div className="mb-8">
|
||||
<h1 className="text-2xl font-bold text-slate-900 dark:text-slate-50">ورود به پنل</h1>
|
||||
<p className="text-slate-500 dark:text-slate-400 text-sm mt-1.5">اطلاعات حساب مدیریتی خود را وارد کنید</p>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit(onSubmit)} className="space-y-5" noValidate>
|
||||
{/* Mobile number */}
|
||||
<div>
|
||||
<label className="cp-label">شماره موبایل</label>
|
||||
<input
|
||||
{...register('mobile')}
|
||||
type="tel"
|
||||
dir="ltr"
|
||||
placeholder="09xxxxxxxxx"
|
||||
autoComplete="username"
|
||||
className={`cp-input text-left placeholder:text-right placeholder:dir-rtl ${errors.mobile ? 'border-red-400 dark:border-red-500 focus:ring-red-400/30' : ''}`}
|
||||
/>
|
||||
{errors.mobile && (
|
||||
<p className="text-red-500 dark:text-red-400 text-xs mt-1.5">{errors.mobile.message}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Password */}
|
||||
<div>
|
||||
<label className="cp-label">رمز عبور</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
{...register('password')}
|
||||
type={showPass ? 'text' : 'password'}
|
||||
placeholder="••••••••"
|
||||
autoComplete="current-password"
|
||||
className={`cp-input pl-10 ${errors.password ? 'border-red-400 dark:border-red-500 focus:ring-red-400/30' : ''}`}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowPass((v) => !v)}
|
||||
className="absolute left-3 top-1/2 -translate-y-1/2 text-slate-400 hover:text-slate-600 dark:hover:text-slate-300 transition-colors"
|
||||
>
|
||||
{showPass ? <EyeSlashIcon className="w-4 h-4" /> : <EyeIcon className="w-4 h-4" />}
|
||||
</button>
|
||||
</div>
|
||||
{errors.password && (
|
||||
<p className="text-red-500 dark:text-red-400 text-xs mt-1.5">{errors.password.message}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Submit */}
|
||||
{/* Password */}
|
||||
<div className="form-row">
|
||||
<label>رمز عبور</label>
|
||||
<div style={{ position: 'relative' }}>
|
||||
<input
|
||||
{...register('password')}
|
||||
className={`input${errors.password ? ' err' : ''}`}
|
||||
type={showPass ? 'text' : 'password'}
|
||||
placeholder="••••••••"
|
||||
autoComplete="current-password"
|
||||
style={{ paddingLeft: 44 }}
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isSubmitting}
|
||||
className="w-full h-11 bg-primary-600 hover:bg-primary-700 active:bg-primary-800 disabled:opacity-60 text-white font-semibold rounded-xl transition-all duration-150 shadow-md shadow-primary-500/25 hover:shadow-lg hover:shadow-primary-500/30 mt-2"
|
||||
type="button"
|
||||
onClick={() => setShowPass((v) => !v)}
|
||||
style={{
|
||||
position: 'absolute', left: 12, top: '50%', transform: 'translateY(-50%)',
|
||||
color: 'var(--text-3)', background: 'none', border: 'none', cursor: 'pointer',
|
||||
display: 'flex', alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
{isSubmitting ? (
|
||||
<span className="flex items-center justify-center gap-2">
|
||||
<svg className="w-4 h-4 animate-spin" viewBox="0 0 24 24" fill="none">
|
||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
|
||||
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v8z" />
|
||||
</svg>
|
||||
در حال ورود...
|
||||
</span>
|
||||
) : 'ورود به سیستم'}
|
||||
{showPass
|
||||
? <EyeSlashIcon style={{ width: 18, height: 18 }} />
|
||||
: <EyeIcon style={{ width: 18, height: 18 }} />}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
{errors.password && <div className="err-text">{errors.password.message}</div>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
className="btn primary block"
|
||||
disabled={isSubmitting}
|
||||
style={{ marginTop: 8, height: 46, fontSize: 15 }}
|
||||
>
|
||||
{isSubmitting ? 'در حال ورود...' : 'ورود به سیستم'}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<p className="muted" style={{ textAlign: 'center', fontSize: 12, marginTop: 20 }}>
|
||||
ClinicPro — نسخه ۱.۰.۰
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user