Files
clinicpro/assets/admin/pages/LoginPage.tsx
T
hamed 942634c98e refactor: update UI components for consistency and dark mode support
- Refactored PaymentsPage, RatingsPage, RepresentationDetailPage, RepresentationsPage, SecretariesPage, SettlementsPage, SmsPage, UserDetailPage, and UsersPage to use consistent class names for styling.
- Updated button styles to use new utility classes for primary, secondary, and danger buttons.
- Enhanced dark mode support across various components by adjusting text and background colors.
- Introduced new utility classes for form inputs, labels, and info rows to standardize styling.
- Implemented Zustand for persistent UI state management, including dark mode toggle functionality.
- Updated CSS to include new styles for skeleton loading and animations.
- Added optional dependencies for improved compatibility with different platforms.
2026-06-10 12:30:14 +03:30

192 lines
8.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
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 React, { useEffect, useState } from 'react';
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 { useAuthStore } from '../stores/authStore';
import { useUiStore } from '../stores/uiStore';
const schema = z.object({
mobile: z.string().min(10, 'شماره موبایل معتبر نیست'),
password: z.string().min(6, 'رمز عبور باید حداقل ۶ کاراکتر باشد'),
});
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', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ mobile_number: data.mobile, password: data.password }),
});
if (!res.ok) {
const err = await res.json();
toast.error(err?.errors?.[0]?.message ?? 'خطا در ورود');
return;
}
const json = await res.json();
login(json.access_token, json.refresh_token ?? '');
toast.success('خوش آمدید');
} catch {
toast.error('خطا در اتصال به سرور');
}
};
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>
{/* 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>
</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>
</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 */}
<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"
>
{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>
) : 'ورود به سیستم'}
</button>
</form>
</div>
</div>
</div>
</div>
);
}