feat(admin): standardize Buttons and Modals across CMS, Catalog, Payment, and FAQ managers

This commit is contained in:
parsa aghaei 2026-08-23 10:07:35 +03:30
parent 32f8b8b656
commit a92cd7fc85
16 changed files with 896 additions and 749 deletions

View File

@ -42,32 +42,33 @@ export default function Button({
const baseClasses =
'inline-flex items-center justify-center font-bold whitespace-nowrap transition-all select-none rounded-xl cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed disabled:pointer-events-none focus:outline-none focus:ring-2 focus:ring-offset-2';
// Size styling
// Size styling - crisp, modern and compact
const sizeClasses: Record<ButtonSize, string> = {
xs: 'px-2.5 py-1 text-[11px] gap-1.5',
sm: 'px-3.5 py-1.5 text-xs gap-2',
md: 'px-4 py-2.5 text-xs sm:text-sm gap-2',
lg: 'px-6 py-3 text-sm sm:text-base gap-2.5 shadow-md',
sm: 'px-3 py-1.5 text-xs gap-1.5',
md: 'px-3.5 py-2 text-xs sm:text-sm gap-2',
lg: 'px-5 py-2.5 text-sm sm:text-base gap-2 shadow-sm',
};
// Variant styling
const variantClasses: Record<ButtonVariant, string> = {
primary:
'bg-indigo-600 hover:bg-indigo-700 text-white shadow-sm shadow-indigo-200 focus:ring-indigo-500 border border-indigo-600',
'bg-purple-600 hover:bg-purple-700 text-white shadow-xs focus:ring-purple-500 border border-purple-600',
secondary:
'bg-slate-100 hover:bg-slate-200 text-slate-700 focus:ring-slate-400 border border-slate-200',
danger:
'bg-rose-600 hover:bg-rose-700 text-white shadow-sm shadow-rose-200 focus:ring-rose-500 border border-rose-600',
'bg-rose-600 hover:bg-rose-700 text-white shadow-xs focus:ring-rose-500 border border-rose-600',
success:
'bg-emerald-600 hover:bg-emerald-700 text-white shadow-sm shadow-emerald-200 focus:ring-emerald-500 border border-emerald-600',
'bg-emerald-600 hover:bg-emerald-700 text-white shadow-xs focus:ring-emerald-500 border border-emerald-600',
warning:
'bg-amber-500 hover:bg-amber-600 text-white shadow-sm shadow-amber-200 focus:ring-amber-400 border border-amber-500',
'bg-amber-500 hover:bg-amber-600 text-white shadow-xs focus:ring-amber-400 border border-amber-500',
outline:
'bg-white hover:bg-gray-50 text-gray-700 border border-gray-300 focus:ring-indigo-500',
'bg-white hover:bg-gray-50 text-gray-700 border border-gray-300 focus:ring-purple-500',
ghost:
'bg-transparent hover:bg-gray-100 text-gray-600 focus:ring-gray-300 border border-transparent',
};
const iconSizes: Record<ButtonSize, string> = {
xs: 'w-3 h-3',
sm: 'w-3.5 h-3.5',

View File

@ -5,8 +5,11 @@ import api, { BASE_DOMAIN } from '../services/api';
import Spinner from '../components/ui/Spinner';
import ConfirmModal from '../components/ui/ConfirmModal';
import MediaSelector from '../components/ui/MediaSelector';
import Button from '../components/ui/Button';
import Modal from '../components/ui/Modal';
import type { Banner } from '../types/admin';
export default function BannersManager() {
const [banners, setBanners] = useState<Banner[]>([]);
const [isLoading, setIsLoading] = useState(true);
@ -166,13 +169,14 @@ export default function BannersManager() {
</h2>
<p className="text-gray-500 font-medium mt-1">مدیریت اسلایدر صفحه اصلی و تبلیغات بنری</p>
</div>
<button
<Button
variant="primary"
size="sm"
startIcon={Plus}
onClick={() => openModal()}
className="bg-purple-600 hover:bg-purple-700 text-white px-5 py-2.5 rounded-xl flex items-center gap-2 font-bold transition-all shadow-md shadow-purple-200"
>
<Plus className="w-5 h-5" />
افزودن بنر جدید
</button>
</Button>
</div>
<div className="bg-white rounded-2xl shadow-sm border border-gray-200 overflow-hidden">
@ -198,7 +202,7 @@ export default function BannersManager() {
</tr>
) : banners.length === 0 ? (
<tr>
<td colSpan={7} className="py-12 text-center text-gray-500 font-medium">هیچ بنری ثبت نشده است</td>
<td colSpan={7} className="py-12 text-center text-gray-500 font-medium">هیچ بنری یافت نشد</td>
</tr>
) : (
banners.map((banner, index) => (
@ -207,71 +211,79 @@ export default function BannersManager() {
<div className="flex items-center gap-1">
<button
disabled={index === 0}
onClick={() => handleMoveOrder(index, 'up')}
className="p-1 text-gray-400 hover:text-purple-600 disabled:opacity-30"
onClick={() => handleReorder(index, 'up')}
className="p-1 text-gray-400 hover:text-purple-600 disabled:opacity-30 disabled:hover:text-gray-400 cursor-pointer"
>
<MoveUp className="w-4 h-4" />
</button>
<span className="font-mono text-sm font-bold text-gray-600 px-1">{index + 1}</span>
<button
disabled={index === banners.length - 1}
onClick={() => handleMoveOrder(index, 'down')}
className="p-1 text-gray-400 hover:text-purple-600 disabled:opacity-30"
onClick={() => handleReorder(index, 'down')}
className="p-1 text-gray-400 hover:text-purple-600 disabled:opacity-30 disabled:hover:text-gray-400 cursor-pointer"
>
<MoveDown className="w-4 h-4" />
</button>
</div>
</td>
<td className="py-3 px-6">
<div className="w-20 h-12 rounded-xl bg-gray-100 border border-gray-200 overflow-hidden flex items-center justify-center">
{banner.imageUrl ? (
<img src={getImageFullUrl(banner.imageUrl)} alt={banner.title} className="w-full h-full object-cover" />
) : (
<ImageIcon className="w-6 h-6 text-gray-400" />
)}
</div>
</td>
<td className="py-4 px-6">
<div className="flex flex-col">
<span className="font-bold text-gray-900">{banner.title}</span>
{banner.subtitle && <span className="text-xs text-gray-500 mt-0.5">{banner.subtitle}</span>}
</div>
</td>
<td className="py-4 px-6">
<span className="inline-block px-3 py-1 bg-purple-50 text-purple-700 text-xs font-bold rounded-lg border border-purple-100">
{banner.position === 'home_hero'
? 'اسلایدر هیرو اصلی'
: banner.position === 'home_middle'
? 'بنر میانی صفحه نخست'
: banner.position === 'home_bottom'
? 'بنر عریض پایین خانه'
: banner.position === 'shop_top'
? 'بالای فروشگاه و کاتالوگ'
: banner.position === 'category_top'
? 'بالای دسته‌بندی'
: 'سایدبار'}
</span>
</td>
<td className="py-4 px-6 text-gray-500 font-mono text-xs dir-ltr text-right">
{banner.linkUrl || '-'}
</td>
<td className="py-4 px-6">
{banner.isActive ? (
<span className="inline-flex items-center gap-1 px-2.5 py-1 bg-green-50 text-green-700 text-xs font-bold rounded-full border border-green-200">
<CheckCircle2 className="w-3.5 h-3.5" /> فعال
</span>
{banner.imageUrl ? (
<img
src={getImageFullUrl(banner.imageUrl)}
alt={banner.title || 'Banner'}
className="w-20 h-10 object-cover rounded-lg border border-gray-100 shadow-xs"
/>
) : (
<span className="inline-flex items-center gap-1 px-2.5 py-1 bg-gray-100 text-gray-600 text-xs font-bold rounded-full border border-gray-200">
<XCircle className="w-3.5 h-3.5" /> غیرفعال
</span>
<div className="w-20 h-10 bg-gray-100 rounded-lg flex items-center justify-center text-gray-400">
<ImageIcon className="w-4 h-4" />
</div>
)}
</td>
<td className="py-4 px-6">
<div className="font-bold text-gray-900">{banner.title || '-'}</div>
<div className="text-xs text-gray-400 mt-0.5">{banner.subtitle || '-'}</div>
</td>
<td className="py-4 px-6">
<span className="px-2.5 py-1 rounded-full text-xs font-bold bg-purple-50 text-purple-700">
{banner.position === 'home_hero' ? 'اسلایدر اصلی' : banner.position === 'home_promo' ? 'بنر تبلیغاتی میانی' : banner.position}
</span>
</td>
<td className="py-4 px-6 text-sm text-gray-500 font-mono" dir="ltr">
{banner.linkUrl || '-'}
</td>
<td className="py-4 px-6">
<button
onClick={() => handleToggleActive(banner)}
className={`inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-xs font-bold transition-colors cursor-pointer ${
banner.isActive
? 'bg-emerald-50 text-emerald-700 hover:bg-emerald-100'
: 'bg-rose-50 text-rose-700 hover:bg-rose-100'
}`}
>
{banner.isActive ? (
<>
<CheckCircle2 className="w-3.5 h-3.5" />
فعال
</>
) : (
<>
<XCircle className="w-3.5 h-3.5" />
غیرفعال
</>
)}
</button>
</td>
<td className="py-4 px-6">
<div className="flex items-center gap-2">
<button onClick={() => openModal(banner)} className="p-2 text-blue-600 hover:bg-blue-50 rounded-lg transition-colors">
<button
onClick={() => openModal(banner)}
className="p-2 text-blue-600 hover:bg-blue-50 rounded-lg transition-colors cursor-pointer"
>
<Edit2 className="w-4 h-4" />
</button>
<button onClick={() => setDeleteTargetId(banner.id)} className="p-2 text-red-600 hover:bg-red-50 rounded-lg transition-colors">
<button
onClick={() => setDeleteTargetId(banner.id)}
className="p-2 text-red-600 hover:bg-red-50 rounded-lg transition-colors cursor-pointer"
>
<Trash2 className="w-4 h-4" />
</button>
</div>
@ -284,96 +296,122 @@ export default function BannersManager() {
</div>
</div>
{/* Form Modal */}
{/* Modal */}
{isModalOpen && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-gray-900/60 backdrop-blur-sm">
<div className="bg-white rounded-2xl w-full max-w-lg shadow-2xl overflow-hidden animate-in zoom-in duration-200">
<div className="px-6 py-4 border-b border-gray-100 flex justify-between items-center bg-gray-50/50">
<h3 className="text-lg font-bold text-gray-900">
{editingBanner ? 'ویرایش بنر' : 'افزودن بنر جدید'}
</h3>
<button onClick={() => setIsModalOpen(false)} className="text-gray-400 hover:text-red-500 transition-colors">
<X className="w-6 h-6" />
</button>
<Modal
isOpen={isModalOpen}
onClose={() => setIsModalOpen(false)}
title={editingBanner ? 'ویرایش بنر' : 'افزودن بنر جدید'}
icon={ImageIcon}
maxWidth="2xl"
footer={
<div className="flex justify-end gap-2.5 w-full">
<Button
variant="secondary"
size="sm"
type="button"
onClick={() => setIsModalOpen(false)}
>
انصراف
</Button>
<Button
variant="primary"
size="sm"
type="submit"
form="bannerForm"
isLoading={isSaving}
>
ذخیره تغییرات
</Button>
</div>
<form onSubmit={handleSave} className="p-6 space-y-4">
}
>
<div className="space-y-6 text-right font-vazir">
<form id="bannerForm" onSubmit={handleSave} className="space-y-4">
<div>
<label className="block text-sm font-bold text-gray-700 mb-1">عنوان بنر *</label>
<label className="block text-sm font-bold text-gray-700 mb-1">عنوان بنر</label>
<input
required
type="text"
value={formData.title}
onChange={(e) => setFormData({ ...formData, title: e.target.value })}
placeholder="مثال: تخصص دارویی از آلمان"
placeholder="مثال: تخفیف ویژه مکمل‌های غذایی"
className="w-full px-4 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 outline-none text-sm"
/>
</div>
<div>
<label className="block text-sm font-bold text-gray-700 mb-1">زیرعنوان (اختیاری)</label>
<label className="block text-sm font-bold text-gray-700 mb-1">زیرعنوان (توضیحات کوتاه)</label>
<input
type="text"
value={formData.subtitle}
onChange={(e) => setFormData({ ...formData, subtitle: e.target.value })}
placeholder="مثال: بیش از ۴۰ سال تجربه"
placeholder="مثال: تا ۳۰٪ تخفیف برای مدت محدود"
className="w-full px-4 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 outline-none text-sm"
/>
</div>
<div>
<label className="block text-sm font-bold text-gray-700 mb-1">آدرس تصویر (imageUrl) *</label>
<div className="flex gap-2">
<input
required
type="text"
value={formData.imageUrl}
onChange={(e) => setFormData({ ...formData, imageUrl: e.target.value })}
placeholder="https://... یا /uploads/..."
className="flex-1 px-4 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 outline-none text-xs font-mono"
dir="ltr"
/>
<button
type="button"
onClick={() => setIsMediaSelectorOpen(true)}
className="px-3 py-2.5 bg-purple-50 text-purple-600 hover:bg-purple-100 rounded-xl font-bold text-xs shrink-0"
>
انتخاب از رسانه
</button>
</div>
</div>
<div>
<label className="block text-sm font-bold text-gray-700 mb-1">لینک هدف (linkUrl)</label>
<input
type="text"
value={formData.linkUrl}
onChange={(e) => setFormData({ ...formData, linkUrl: e.target.value })}
placeholder="مثال: /products/joints"
className="w-full px-4 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 outline-none text-xs font-mono"
dir="ltr"
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-bold text-gray-700 mb-1">موقعیت نمایش</label>
<select
value={formData.position}
onChange={(e) => setFormData({ ...formData, position: e.target.value })}
className="w-full px-4 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 outline-none text-sm font-bold bg-white"
className="w-full px-4 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 outline-none text-sm bg-white"
>
<option value="home_hero">اسلایدر هیرو اصلی (home_hero)</option>
<option value="home_middle">بنر میانی صفحه نخست (home_middle)</option>
<option value="home_bottom">بنر عریض پایین خانه (home_bottom)</option>
<option value="shop_top">بالای فروشگاه و کاتالوگ (shop_top)</option>
<option value="category_top">بالای دستهبندی (category_top)</option>
<option value="product_sidebar">سایدبار صفحات محصول و مقالات (product_sidebar)</option>
<option value="home_hero">اسلایدر اصلی (Home Hero)</option>
<option value="home_promo">بنر تبلیغاتی میانی (Home Promo)</option>
</select>
</div>
<div>
<label className="block text-sm font-bold text-gray-700 mb-1">ترتیب نمایش (order)</label>
<label className="block text-sm font-bold text-gray-700 mb-1">لینک هدف (URL)</label>
<input
type="text"
value={formData.linkUrl}
onChange={(e) => setFormData({ ...formData, linkUrl: e.target.value })}
placeholder="/shop یا https://..."
dir="ltr"
className="w-full px-4 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 outline-none text-sm font-mono"
/>
</div>
</div>
<div>
<label className="block text-sm font-bold text-gray-700 mb-1">تصویر بنر *</label>
<div className="flex gap-2">
<input
type="text"
required
value={formData.imageUrl}
onChange={(e) => setFormData({ ...formData, imageUrl: e.target.value })}
placeholder="آدرس تصویر یا انتخاب از رسانه‌ها..."
dir="ltr"
className="flex-1 px-4 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 outline-none text-sm font-mono"
/>
<Button
type="button"
variant="outline"
size="sm"
startIcon={ImageIcon}
onClick={() => setIsMediaSelectorOpen(true)}
>
انتخاب تصویر
</Button>
</div>
{formData.imageUrl && (
<div className="mt-3">
<img
src={getImageFullUrl(formData.imageUrl)}
alt="Preview"
className="h-28 w-full object-cover rounded-xl border border-gray-200"
/>
</div>
)}
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 items-center">
<div>
<label className="block text-sm font-bold text-gray-700 mb-1">ترتیب نمایش</label>
<input
type="number"
value={formData.order}
@ -395,29 +433,12 @@ export default function BannersManager() {
<div className="w-11 h-6 bg-gray-200 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-purple-600"></div>
</label>
</div>
<div className="pt-4 flex justify-end gap-3 border-t border-gray-100">
<button
type="button"
onClick={() => setIsModalOpen(false)}
className="px-5 py-2.5 rounded-xl border border-gray-200 text-gray-600 font-bold hover:bg-gray-50 transition-colors"
>
انصراف
</button>
<button
type="submit"
disabled={isSaving}
className="px-6 py-2.5 rounded-xl bg-purple-600 text-white font-bold hover:bg-purple-700 transition-colors shadow-md shadow-purple-200 flex items-center gap-2"
>
{isSaving && <Spinner size="sm" />}
ذخیره تغییرات
</button>
</div>
</form>
</div>
</div>
</Modal>
)}
{/* Delete Confirmation Modal */}
{deleteTargetId && (
<ConfirmModal

View File

@ -6,6 +6,10 @@ import Spinner from '../components/ui/Spinner';
import Pagination from '../components/ui/Pagination';
import MediaSelector from '../components/ui/MediaSelector';
import ConfirmModal from '../components/ui/ConfirmModal';
import Button from '../components/ui/Button';
import Modal from '../components/ui/Modal';
export interface BlogPost {
author?: {
@ -134,13 +138,14 @@ export default function Blogs() {
</h2>
<p className="text-gray-500 font-medium mt-1">مدیریت محتوای آموزشی و مقالات علمی سایت</p>
</div>
<button
<Button
variant="primary"
size="sm"
startIcon={Plus}
onClick={() => openModal()}
className="bg-purple-600 hover:bg-purple-700 text-white px-5 py-2.5 rounded-xl flex items-center gap-2 font-bold transition-all shadow-md shadow-purple-200"
>
<Plus className="w-5 h-5" />
مقاله جدید
</button>
</Button>
</div>
<div className="bg-white p-4 rounded-2xl shadow-sm border border-gray-200 flex flex-col sm:flex-row gap-4">
@ -196,8 +201,8 @@ export default function Blogs() {
</td>
<td className="py-4 px-6">
<div className="flex items-center gap-2">
<button onClick={() => openModal(blog)} className="p-2 text-blue-600 hover:bg-blue-50 rounded-lg transition-colors"><Edit2 className="w-4 h-4" /></button>
<button onClick={() => setDeleteTargetId(blog.id)} className="p-2 text-red-600 hover:bg-red-50 rounded-lg transition-colors"><Trash2 className="w-4 h-4" /></button>
<button onClick={() => openModal(blog)} className="p-2 text-blue-600 hover:bg-blue-50 rounded-lg transition-colors cursor-pointer"><Edit2 className="w-4 h-4" /></button>
<button onClick={() => setDeleteTargetId(blog.id)} className="p-2 text-red-600 hover:bg-red-50 rounded-lg transition-colors cursor-pointer"><Trash2 className="w-4 h-4" /></button>
</div>
</td>
</tr>
@ -214,82 +219,99 @@ export default function Blogs() {
</div>
{isModalOpen && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-gray-900/60 backdrop-blur-sm">
<div className="bg-white rounded-2xl w-full max-w-4xl max-h-[95vh] flex flex-col shadow-2xl animate-in zoom-in duration-200">
<div className="px-6 py-4 border-b border-gray-100 flex justify-between items-center bg-gray-50/50">
<h3 className="text-xl font-bold text-gray-900">
{editingBlog ? 'ویرایش مقاله' : 'افزودن مقاله جدید'}
</h3>
<button onClick={() => setIsModalOpen(false)} className="text-gray-400 hover:text-red-500 transition-colors">
<XCircle className="w-6 h-6" />
</button>
<Modal
isOpen={isModalOpen}
onClose={() => setIsModalOpen(false)}
title={editingBlog ? 'ویرایش مقاله' : 'افزودن مقاله جدید'}
icon={FileText}
maxWidth="4xl"
footer={
<div className="flex justify-end gap-2.5 w-full">
<Button
variant="secondary"
size="sm"
type="button"
onClick={() => setIsModalOpen(false)}
>
انصراف
</Button>
<Button
variant="primary"
size="sm"
type="submit"
form="blogForm"
>
ذخیره مقاله
</Button>
</div>
<div className="p-6 overflow-y-auto flex-1">
<form id="blogForm" onSubmit={handleSave} className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<label className="text-sm font-bold text-gray-700">عنوان مقاله *</label>
<input required type="text" value={formData.title} onChange={(e) => setFormData({ ...formData, title: e.target.value })} className="w-full px-4 py-3 rounded-xl border border-gray-200 focus:border-purple-500 outline-none" />
</div>
<div className="space-y-2">
<label className="text-sm font-bold text-gray-700">اسلاگ (Slug) *</label>
<input required type="text" value={formData.slug} dir="ltr" onChange={(e) => setFormData({ ...formData, slug: e.target.value })} className="w-full px-4 py-3 rounded-xl border border-gray-200 focus:border-purple-500 outline-none font-mono text-sm" />
</div>
</div>
}
>
<div className="space-y-6 text-right font-vazir">
<form id="blogForm" onSubmit={handleSave} className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<label className="text-sm font-bold text-gray-700">محتوای اصلی مقاله *</label>
<textarea required value={formData.content} onChange={(e) => setFormData({ ...formData, content: e.target.value })} rows={10} className="w-full px-4 py-3 rounded-xl border border-gray-200 focus:border-purple-500 outline-none font-mono text-sm" dir="rtl" placeholder="محتوای مقاله (متن ساده یا HTML)"></textarea>
<label className="text-sm font-bold text-gray-700">عنوان مقاله *</label>
<input required type="text" value={formData.title} onChange={(e) => setFormData({ ...formData, title: e.target.value })} className="w-full px-4 py-3 rounded-xl border border-gray-200 focus:border-purple-500 outline-none" />
</div>
<div className="space-y-2">
<label className="text-sm font-bold text-gray-700">اسلاگ (Slug) *</label>
<input required type="text" value={formData.slug} dir="ltr" onChange={(e) => setFormData({ ...formData, slug: e.target.value })} className="w-full px-4 py-3 rounded-xl border border-gray-200 focus:border-purple-500 outline-none font-mono text-sm" />
</div>
</div>
<div className="space-y-2">
<label className="text-sm font-bold text-gray-700">محتوای اصلی مقاله *</label>
<textarea required value={formData.content} onChange={(e) => setFormData({ ...formData, content: e.target.value })} rows={10} className="w-full px-4 py-3 rounded-xl border border-gray-200 focus:border-purple-500 outline-none font-mono text-sm" dir="rtl" placeholder="محتوای مقاله (متن ساده یا HTML)"></textarea>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="space-y-4">
<label className="text-sm font-bold text-gray-700">تصویر شاخص</label>
<div className="flex items-center gap-4">
{formData.imageUrl ? (
<img src={formData.imageUrl.startsWith('http') ? formData.imageUrl : `${BASE_DOMAIN}${formData.imageUrl}`} alt="preview" className="w-24 h-16 rounded-xl object-cover border border-gray-200" />
) : (
<div className="w-24 h-16 rounded-xl bg-gray-100 flex items-center justify-center text-gray-400">
<ImageIcon className="w-6 h-6" />
</div>
)}
<Button
type="button"
variant="outline"
size="sm"
onClick={() => setIsMediaSelectorOpen(true)}
>
انتخاب از گالری
</Button>
</div>
<label className="flex items-center gap-3 cursor-pointer p-4 border border-gray-200 rounded-xl hover:bg-gray-50 transition-colors">
<input type="checkbox" checked={formData.isPublished} onChange={(e) => setFormData({ ...formData, isPublished: e.target.checked })} className="w-5 h-5 text-purple-600 rounded focus:ring-purple-500" />
<span className="font-bold text-gray-700">انتشار فوری در سایت</span>
</label>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="space-y-4">
<label className="text-sm font-bold text-gray-700">تصویر شاخص</label>
<div className="flex items-center gap-4">
{formData.imageUrl ? (
<img src={formData.imageUrl.startsWith('http') ? formData.imageUrl : `${BASE_DOMAIN}${formData.imageUrl}`} alt="preview" className="w-24 h-16 rounded-xl object-cover border border-gray-200" />
) : (
<div className="w-24 h-16 rounded-xl bg-gray-100 flex items-center justify-center text-gray-400">
<ImageIcon className="w-6 h-6" />
</div>
)}
<button type="button" onClick={() => setIsMediaSelectorOpen(true)} className="px-4 py-2 border-2 border-dashed border-gray-300 rounded-xl text-gray-600 font-bold hover:border-purple-500 hover:text-purple-600">انتخاب از گالری</button>
</div>
<label className="flex items-center gap-3 cursor-pointer p-4 border border-gray-200 rounded-xl hover:bg-gray-50 transition-colors">
<input type="checkbox" checked={formData.isPublished} onChange={(e) => setFormData({ ...formData, isPublished: e.target.checked })} className="w-5 h-5 text-purple-600 rounded focus:ring-purple-500" />
<span className="font-bold text-gray-700">انتشار فوری در سایت</span>
</label>
<div className="space-y-4 border border-gray-200 p-4 rounded-xl bg-gray-50/50">
<h4 className="font-bold text-purple-700 text-sm mb-2">تنظیمات سئو (SEO)</h4>
<div className="space-y-2">
<label className="text-xs font-bold text-gray-700">عنوان متا</label>
<input type="text" value={formData.metaTitle} onChange={(e) => setFormData({ ...formData, metaTitle: e.target.value })} className="w-full px-3 py-2 rounded-lg border border-gray-200 focus:border-purple-500 outline-none text-sm" />
</div>
<div className="space-y-4 border border-gray-200 p-4 rounded-xl bg-gray-50/50">
<h4 className="font-bold text-purple-700 text-sm mb-2">تنظیمات سئو (SEO)</h4>
<div className="space-y-2">
<label className="text-xs font-bold text-gray-700">عنوان متا</label>
<input type="text" value={formData.metaTitle} onChange={(e) => setFormData({ ...formData, metaTitle: e.target.value })} className="w-full px-3 py-2 rounded-lg border border-gray-200 focus:border-purple-500 outline-none text-sm" />
</div>
<div className="space-y-2">
<label className="text-xs font-bold text-gray-700">کلمات کلیدی</label>
<input type="text" value={formData.keywords} onChange={(e) => setFormData({ ...formData, keywords: e.target.value })} className="w-full px-3 py-2 rounded-lg border border-gray-200 focus:border-purple-500 outline-none text-sm" />
</div>
<div className="space-y-2">
<label className="text-xs font-bold text-gray-700">توضیحات متا</label>
<textarea rows={2} value={formData.metaDescription} onChange={(e) => setFormData({ ...formData, metaDescription: e.target.value })} className="w-full px-3 py-2 rounded-lg border border-gray-200 focus:border-purple-500 outline-none text-sm"></textarea>
</div>
<div className="space-y-2">
<label className="text-xs font-bold text-gray-700">کلمات کلیدی</label>
<input type="text" value={formData.keywords} onChange={(e) => setFormData({ ...formData, keywords: e.target.value })} className="w-full px-3 py-2 rounded-lg border border-gray-200 focus:border-purple-500 outline-none text-sm" />
</div>
<div className="space-y-2">
<label className="text-xs font-bold text-gray-700">توضیحات متا</label>
<textarea rows={2} value={formData.metaDescription} onChange={(e) => setFormData({ ...formData, metaDescription: e.target.value })} className="w-full px-3 py-2 rounded-lg border border-gray-200 focus:border-purple-500 outline-none text-sm"></textarea>
</div>
</div>
</form>
</div>
<div className="p-4 border-t border-gray-100 bg-gray-50 flex justify-end gap-3 rounded-b-2xl">
<button type="button" onClick={() => setIsModalOpen(false)} className="px-5 py-2.5 rounded-xl font-bold text-gray-600 hover:bg-gray-200 transition-colors">انصراف</button>
<button type="submit" form="blogForm" className="bg-purple-600 hover:bg-purple-700 text-white px-8 py-2.5 rounded-xl font-bold transition-all shadow-md shadow-purple-200">ذخیره مقاله</button>
</div>
</div>
</form>
</div>
</div>
</Modal>
)}
<MediaSelector isOpen={isMediaSelectorOpen} onClose={() => setIsMediaSelectorOpen(false)} onSelect={(url) => setFormData({ ...formData, imageUrl: url })} />
<ConfirmModal

View File

@ -6,6 +6,10 @@ import Spinner from '../components/ui/Spinner';
import Pagination from '../components/ui/Pagination';
import MediaSelector from '../components/ui/MediaSelector';
import ConfirmModal from '../components/ui/ConfirmModal';
import Button from '../components/ui/Button';
import Modal from '../components/ui/Modal';
export interface Category {
id: string;
@ -127,15 +131,17 @@ export default function Categories() {
</h2>
<p className="text-gray-500 font-medium mt-1">ایجاد دستهبندی محصولات و تنظیمات سئو (SEO)</p>
</div>
<button
<Button
variant="primary"
size="sm"
startIcon={Plus}
onClick={() => openModal()}
className="bg-purple-600 hover:bg-purple-700 text-white px-5 py-2.5 rounded-xl flex items-center gap-2 font-bold transition-all shadow-md shadow-purple-200"
>
<Plus className="w-5 h-5" />
دستهبندی جدید
</button>
</Button>
</div>
{/* Filters */}
<div className="bg-white p-4 rounded-2xl shadow-sm border border-gray-200 flex flex-col sm:flex-row gap-4">
<div className="relative flex-1">
@ -197,10 +203,10 @@ export default function Categories() {
</td>
<td className="py-4 px-6">
<div className="flex items-center gap-2">
<button onClick={() => openModal(cat)} className="p-2 text-blue-600 hover:bg-blue-50 rounded-lg transition-colors">
<button onClick={() => openModal(cat)} className="p-2 text-blue-600 hover:bg-blue-50 rounded-lg transition-colors cursor-pointer">
<Edit2 className="w-4 h-4" />
</button>
<button onClick={() => setDeleteTargetId(cat.id)} className="p-2 text-red-600 hover:bg-red-50 rounded-lg transition-colors">
<button onClick={() => setDeleteTargetId(cat.id)} className="p-2 text-red-600 hover:bg-red-50 rounded-lg transition-colors cursor-pointer">
<Trash2 className="w-4 h-4" />
</button>
</div>
@ -220,120 +226,120 @@ export default function Categories() {
{/* Modal */}
{isModalOpen && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-gray-900/60 backdrop-blur-sm">
<div className="bg-white rounded-2xl w-full max-w-2xl max-h-[90vh] flex flex-col shadow-2xl animate-in zoom-in duration-200">
<div className="px-6 py-4 border-b border-gray-100 flex justify-between items-center bg-gray-50/50">
<h3 className="text-xl font-bold text-gray-900">
{editingCategory ? 'ویرایش دسته‌بندی' : 'افزودن دسته‌بندی جدید'}
</h3>
<button onClick={() => setIsModalOpen(false)} className="text-gray-400 hover:text-red-500 transition-colors">
<X className="w-6 h-6" />
</button>
<Modal
isOpen={isModalOpen}
onClose={() => setIsModalOpen(false)}
title={editingCategory ? 'ویرایش دسته‌بندی' : 'افزودن دسته‌بندی جدید'}
icon={FolderTree}
maxWidth="2xl"
footer={
<div className="flex justify-end gap-2.5 w-full">
<Button
variant="secondary"
size="sm"
type="button"
onClick={() => setIsModalOpen(false)}
>
انصراف
</Button>
<Button
variant="primary"
size="sm"
type="submit"
form="categoryForm"
>
ذخیره دستهبندی
</Button>
</div>
<div className="p-6 overflow-y-auto flex-1">
<form id="categoryForm" onSubmit={handleSave} className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
}
>
<div className="space-y-6 text-right font-vazir">
<form id="categoryForm" onSubmit={handleSave} className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<label className="text-sm font-bold text-gray-700">نام دستهبندی *</label>
<input
required
type="text"
value={formData.name}
onChange={(e) => setFormData({...formData, name: e.target.value})}
className="w-full px-4 py-3 rounded-xl border border-gray-200 focus:border-purple-500 outline-none transition-all"
/>
</div>
<div className="space-y-2">
<label className="text-sm font-bold text-gray-700">اسلاگ (Slug) *</label>
<input
required
type="text"
value={formData.slug}
dir="ltr"
onChange={(e) => setFormData({...formData, slug: e.target.value})}
className="w-full px-4 py-3 rounded-xl border border-gray-200 focus:border-purple-500 outline-none transition-all font-mono"
/>
</div>
</div>
<div className="space-y-2">
<label className="text-sm font-bold text-gray-700">تصویر دستهبندی</label>
<div className="flex items-center gap-4">
{formData.imageUrl ? (
<img src={formData.imageUrl.startsWith('http') ? formData.imageUrl : `${BASE_DOMAIN}${formData.imageUrl}`} alt="preview" className="w-16 h-16 rounded-xl object-cover border border-gray-200" />
) : (
<div className="w-16 h-16 rounded-xl bg-gray-100 flex items-center justify-center text-gray-400">
<ImageIcon className="w-5 h-5" />
</div>
)}
<Button
type="button"
variant="outline"
size="sm"
onClick={() => setIsMediaSelectorOpen(true)}
>
انتخاب از گالری
</Button>
</div>
</div>
<div className="space-y-2">
<label className="text-sm font-bold text-gray-700">توضیحات دستهبندی</label>
<textarea
value={formData.description}
onChange={(e) => setFormData({...formData, description: e.target.value})}
rows={3}
className="w-full px-4 py-3 rounded-xl border border-gray-200 focus:border-purple-500 outline-none transition-all"
></textarea>
</div>
<div className="pt-4 border-t border-gray-100">
<h4 className="font-bold text-purple-700 mb-4">تنظیمات سئو (SEO)</h4>
<div className="space-y-4">
<div className="space-y-2">
<label className="text-sm font-bold text-gray-700">نام دستهبندی *</label>
<label className="text-sm font-bold text-gray-700">عنوان متا (Meta Title)</label>
<input
required
type="text"
value={formData.name}
onChange={(e) => setFormData({...formData, name: e.target.value})}
value={formData.metaTitle}
onChange={(e) => setFormData({...formData, metaTitle: e.target.value})}
className="w-full px-4 py-3 rounded-xl border border-gray-200 focus:border-purple-500 outline-none transition-all"
/>
</div>
<div className="space-y-2">
<label className="text-sm font-bold text-gray-700">اسلاگ (Slug) *</label>
<input
required
type="text"
value={formData.slug}
dir="ltr"
onChange={(e) => setFormData({...formData, slug: e.target.value})}
className="w-full px-4 py-3 rounded-xl border border-gray-200 focus:border-purple-500 outline-none transition-all font-mono"
/>
<label className="text-sm font-bold text-gray-700">توضیحات متا (Meta Description)</label>
<textarea
value={formData.metaDescription}
onChange={(e) => setFormData({...formData, metaDescription: e.target.value})}
rows={2}
className="w-full px-4 py-3 rounded-xl border border-gray-200 focus:border-purple-500 outline-none transition-all"
></textarea>
</div>
</div>
<div className="space-y-2">
<label className="text-sm font-bold text-gray-700">تصویر دستهبندی</label>
<div className="flex items-center gap-4">
{formData.imageUrl ? (
<img src={formData.imageUrl.startsWith('http') ? formData.imageUrl : `${BASE_DOMAIN}${formData.imageUrl}`} alt="preview" className="w-16 h-16 rounded-xl object-cover border border-gray-200" />
) : (
<div className="w-16 h-16 rounded-xl bg-gray-100 flex items-center justify-center text-gray-400">
<ImageIcon className="w-6 h-6" />
</div>
)}
<button
type="button"
onClick={() => setIsMediaSelectorOpen(true)}
className="px-4 py-2 border-2 border-dashed border-gray-300 rounded-xl text-gray-600 font-bold hover:border-purple-500 hover:text-purple-600 transition-colors"
>
انتخاب از گالری
</button>
</div>
</div>
<div className="space-y-2">
<label className="text-sm font-bold text-gray-700">توضیحات دستهبندی</label>
<textarea
value={formData.description}
onChange={(e) => setFormData({...formData, description: e.target.value})}
rows={3}
className="w-full px-4 py-3 rounded-xl border border-gray-200 focus:border-purple-500 outline-none transition-all"
></textarea>
</div>
<div className="pt-4 border-t border-gray-100">
<h4 className="font-bold text-purple-700 mb-4">تنظیمات سئو (SEO)</h4>
<div className="space-y-4">
<div className="space-y-2">
<label className="text-sm font-bold text-gray-700">عنوان متا (Meta Title)</label>
<input
type="text"
value={formData.metaTitle}
onChange={(e) => setFormData({...formData, metaTitle: e.target.value})}
className="w-full px-4 py-3 rounded-xl border border-gray-200 focus:border-purple-500 outline-none transition-all"
/>
</div>
<div className="space-y-2">
<label className="text-sm font-bold text-gray-700">توضیحات متا (Meta Description)</label>
<textarea
value={formData.metaDescription}
onChange={(e) => setFormData({...formData, metaDescription: e.target.value})}
rows={2}
className="w-full px-4 py-3 rounded-xl border border-gray-200 focus:border-purple-500 outline-none transition-all"
></textarea>
</div>
</div>
</div>
</form>
</div>
<div className="p-4 border-t border-gray-100 bg-gray-50 flex justify-end gap-3 rounded-b-2xl">
<button
type="button"
onClick={() => setIsModalOpen(false)}
className="px-5 py-2.5 rounded-xl font-bold text-gray-600 hover:bg-gray-200 transition-colors"
>
انصراف
</button>
<button
type="submit"
form="categoryForm"
className="bg-purple-600 hover:bg-purple-700 text-white px-8 py-2.5 rounded-xl font-bold transition-all shadow-md shadow-purple-200"
>
ذخیره دستهبندی
</button>
</div>
</div>
</form>
</div>
</div>
</Modal>
)}
{/* Media Selector Modal */}
<MediaSelector
isOpen={isMediaSelectorOpen}
onClose={() => setIsMediaSelectorOpen(false)}

View File

@ -165,12 +165,17 @@ export default function Coupons() {
</h2>
<p className="text-gray-500 font-medium mt-1">مدیریت کدهای تخفیف با اهداف چندگانه (کاربران، حیوانات، محصولات و ...)</p>
</div>
<button onClick={() => openModal()} className="bg-purple-600 hover:bg-purple-700 text-white px-5 py-2.5 rounded-xl flex items-center gap-2 font-bold transition-all shadow-md shadow-purple-200">
<Plus className="w-5 h-5" />
<Button
variant="primary"
size="sm"
startIcon={Plus}
onClick={() => openModal()}
>
کد تخفیف جدید
</button>
</Button>
</div>
<div className="bg-white p-4 rounded-2xl shadow-sm border border-gray-200 flex flex-col sm:flex-row gap-4">
<div className="relative flex-1">
<Search className="w-5 h-5 absolute right-4 top-1/2 -translate-y-1/2 text-gray-400" />

View File

@ -4,6 +4,9 @@ import { toast } from 'react-hot-toast';
import api from '../services/api';
import Spinner from '../components/ui/Spinner';
import ConfirmModal from '../components/ui/ConfirmModal';
import Button from '../components/ui/Button';
import Modal from '../components/ui/Modal';
interface FAQ {
id: string;
@ -129,13 +132,14 @@ export default function FAQManager() {
سوالات و پاسخهایی که در صفحه اصلی سایت نمایش داده میشوند با Schema.org FAQPage برای SEO
</p>
</div>
<button
<Button
variant="primary"
size="sm"
startIcon={Plus}
onClick={() => openModal()}
className="bg-orange-500 hover:bg-orange-600 text-white px-5 py-2.5 rounded-xl flex items-center gap-2 font-bold transition-all shadow-md shadow-orange-200"
>
<Plus className="w-5 h-5" />
افزودن سوال جدید
</button>
</Button>
</div>
{/* Info Box */}
@ -147,7 +151,7 @@ export default function FAQManager() {
<div className="space-y-3">
{isLoading ? (
<div className="flex justify-center py-12">
<Spinner size="lg" className="text-orange-500" />
<Spinner size="lg" className="text-purple-600" />
</div>
) : faqs.length === 0 ? (
<div className="bg-white rounded-2xl border border-gray-200 py-16 text-center">
@ -155,56 +159,71 @@ export default function FAQManager() {
<p className="text-gray-500 font-medium">هیچ سوالی ثبت نشده است</p>
<button
onClick={() => openModal()}
className="mt-4 text-orange-500 font-bold hover:underline text-sm"
className="mt-4 text-purple-600 font-bold hover:underline text-sm cursor-pointer"
>
اولین سوال را اضافه کنید
</button>
</div>
) : (
faqs.map((faq, idx) => (
faqs.map((faq) => (
<div
key={faq.id}
className={`bg-white rounded-2xl border p-5 transition-all ${
faq.isActive ? 'border-gray-200' : 'border-dashed border-gray-300 opacity-60'
faq.isActive ? 'border-gray-200 shadow-sm' : 'border-gray-100 opacity-60 bg-gray-50'
}`}
>
<div className="flex items-start gap-4">
<div className="flex items-center gap-2 shrink-0 mt-1">
<GripVertical className="w-4 h-4 text-gray-300" />
<span className="text-xs font-black text-gray-400 w-5 text-center">{idx + 1}</span>
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-2">
<span className="font-black text-gray-900 text-sm">{faq.question}</span>
{!faq.isActive && (
<span className="px-2 py-0.5 bg-gray-100 text-gray-500 text-[10px] font-bold rounded">
غیرفعال
</span>
)}
<div className="flex items-start justify-between gap-4">
<div className="flex items-start gap-3 flex-1">
<div className="p-2 bg-orange-50 text-orange-600 rounded-xl mt-0.5 shrink-0">
<HelpCircle className="w-5 h-5" />
</div>
<div className="space-y-2 flex-1">
<div className="flex items-center gap-2 flex-wrap">
<span className="text-xs font-bold px-2 py-0.5 bg-gray-100 text-gray-600 rounded-md">
ترتیب: {faq.order}
</span>
<span
className={`text-xs font-bold px-2 py-0.5 rounded-md ${
faq.isActive
? 'bg-green-100 text-green-700'
: 'bg-gray-100 text-gray-500'
}`}
>
{faq.isActive ? 'فعال' : 'غیرفعال'}
</span>
</div>
<h3 className="font-bold text-gray-900 text-base">{faq.question}</h3>
<p className="text-gray-600 text-sm leading-relaxed whitespace-pre-line">
{faq.answer}
</p>
</div>
<p className="text-gray-600 text-sm leading-relaxed line-clamp-2">{faq.answer}</p>
</div>
<div className="flex items-center gap-1 shrink-0">
<div className="flex items-center gap-2 shrink-0">
<button
onClick={() => toggleActive(faq)}
className={`p-2 rounded-lg transition-colors ${
title={faq.isActive ? 'غیرفعال‌سازی' : 'فعال‌سازی'}
className={`p-2 rounded-xl transition-colors cursor-pointer ${
faq.isActive
? 'text-green-600 hover:bg-green-50'
: 'text-gray-400 hover:bg-gray-50'
: 'text-gray-400 hover:bg-gray-100'
}`}
title={faq.isActive ? 'غیرفعال کردن' : 'فعال کردن'}
>
{faq.isActive ? <ToggleRight className="w-5 h-5" /> : <ToggleLeft className="w-5 h-5" />}
{faq.isActive ? (
<ToggleRight className="w-5 h-5" />
) : (
<ToggleLeft className="w-5 h-5" />
)}
</button>
<button
onClick={() => openModal(faq)}
className="p-2 text-blue-600 hover:bg-blue-50 rounded-lg transition-colors"
className="p-2 text-blue-600 hover:bg-blue-50 rounded-xl transition-colors cursor-pointer"
>
<Edit2 className="w-4 h-4" />
</button>
<button
onClick={() => setDeleteTargetId(faq.id)}
className="p-2 text-red-500 hover:bg-red-50 rounded-lg transition-colors"
className="p-2 text-red-600 hover:bg-red-50 rounded-xl transition-colors cursor-pointer"
>
<Trash2 className="w-4 h-4" />
</button>
@ -215,31 +234,49 @@ export default function FAQManager() {
)}
</div>
{/* Form Modal */}
{/* Modal */}
{isModalOpen && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-gray-900/60 backdrop-blur-sm">
<div className="bg-white rounded-2xl w-full max-w-2xl shadow-2xl overflow-hidden">
<div className="px-6 py-4 border-b border-gray-100 flex justify-between items-center bg-gray-50/50">
<h3 className="text-lg font-bold text-gray-900">
{editingFaq ? 'ویرایش سوال متداول' : 'افزودن سوال جدید'}
</h3>
<button onClick={() => setIsModalOpen(false)} className="p-2 hover:bg-gray-100 rounded-lg">
<X className="w-5 h-5 text-gray-500" />
</button>
<Modal
isOpen={isModalOpen}
onClose={() => setIsModalOpen(false)}
title={editingFaq ? 'ویرایش سوال متداول' : 'افزودن سوال جدید'}
icon={HelpCircle}
maxWidth="2xl"
footer={
<div className="flex justify-end gap-2.5 w-full">
<Button
variant="secondary"
size="sm"
type="button"
onClick={() => setIsModalOpen(false)}
>
انصراف
</Button>
<Button
variant="primary"
size="sm"
type="submit"
form="faqForm"
isLoading={isSaving}
>
ذخیره سوال
</Button>
</div>
<form onSubmit={handleSave} className="p-6 space-y-4">
}
>
<div className="space-y-6 text-right font-vazir">
<form id="faqForm" onSubmit={handleSave} className="space-y-4">
<div>
<label className="block text-sm font-bold text-gray-700 mb-1">
سوال <span className="text-red-500">*</span>
</label>
<input
type="text"
required
type="text"
value={formData.question}
onChange={(e) => setFormData({ ...formData, question: e.target.value })}
placeholder="مثال: آیا محصولات کنینا برای سگ‌های حساس مناسب است؟"
className="w-full px-4 py-2.5 rounded-xl border border-gray-200 focus:border-orange-400 outline-none text-sm"
className="w-full px-4 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 outline-none text-sm"
/>
</div>
@ -253,7 +290,7 @@ export default function FAQManager() {
value={formData.answer}
onChange={(e) => setFormData({ ...formData, answer: e.target.value })}
placeholder="پاسخ کامل، واضح و مفید..."
className="w-full px-4 py-2.5 rounded-xl border border-gray-200 focus:border-orange-400 outline-none text-sm resize-none"
className="w-full px-4 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 outline-none text-sm resize-none"
/>
</div>
@ -264,44 +301,28 @@ export default function FAQManager() {
type="number"
value={formData.order}
onChange={(e) => setFormData({ ...formData, order: Number(e.target.value) })}
className="w-full px-4 py-2.5 rounded-xl border border-gray-200 focus:border-orange-400 outline-none text-sm"
className="w-full px-4 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 outline-none text-sm"
/>
</div>
<div className="flex items-end">
<label className="flex items-center justify-between p-3 bg-orange-50 rounded-xl border border-orange-200 cursor-pointer w-full">
<span className="text-xs font-bold text-orange-700"> فعال و قابل نمایش</span>
<label className="flex items-center justify-between p-3 bg-purple-50 rounded-xl border border-purple-200 cursor-pointer w-full">
<span className="text-xs font-bold text-purple-700"> فعال و قابل نمایش</span>
<input
type="checkbox"
checked={formData.isActive}
onChange={(e) => setFormData({ ...formData, isActive: e.target.checked })}
className="rounded text-orange-500 focus:ring-orange-400"
className="rounded text-purple-600 focus:ring-purple-500"
/>
</label>
</div>
</div>
<div className="pt-4 flex justify-end gap-3 border-t border-gray-100">
<button
type="button"
onClick={() => setIsModalOpen(false)}
className="px-5 py-2.5 rounded-xl border border-gray-200 text-gray-600 font-bold hover:bg-gray-50 transition-colors"
>
انصراف
</button>
<button
type="submit"
disabled={isSaving}
className="px-6 py-2.5 rounded-xl bg-orange-500 text-white font-bold hover:bg-orange-600 transition-colors shadow-md shadow-orange-200 flex items-center gap-2"
>
{isSaving && <Spinner size="sm" />}
ذخیره
</button>
</div>
</form>
</div>
</div>
</Modal>
)}
{deleteTargetId && (
<ConfirmModal
isOpen={true}

View File

@ -7,6 +7,10 @@ import { toast } from 'react-hot-toast';
import api from '../services/api';
import Spinner from '../components/ui/Spinner';
import ConfirmModal from '../components/ui/ConfirmModal';
import Button from '../components/ui/Button';
import Modal from '../components/ui/Modal';
interface MenuItem {
id: string;
@ -174,15 +178,17 @@ export default function MenuManager() {
مدیریت آیتمهای هدر و فوتر سایت تغییرات بلادرنگ اعمال میشوند
</p>
</div>
<button
<Button
variant="primary"
size="sm"
startIcon={Plus}
onClick={() => openModal()}
className="bg-blue-600 hover:bg-blue-700 text-white px-5 py-2.5 rounded-xl flex items-center gap-2 font-bold transition-all shadow-md shadow-blue-200"
>
<Plus className="w-5 h-5" />
آیتم جدید
</button>
</Button>
</div>
{/* Tabs */}
<div className="flex gap-2 flex-wrap">
{MENU_TABS.map((tab) => (
@ -292,27 +298,45 @@ export default function MenuManager() {
</div>
)}
{/* Form Modal */}
{/* Modal */}
{isModalOpen && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-gray-900/60 backdrop-blur-sm">
<div className="bg-white rounded-2xl w-full max-w-xl shadow-2xl overflow-hidden">
<div className="px-6 py-4 border-b border-gray-100 flex justify-between items-center bg-gray-50/50">
<h3 className="text-lg font-bold text-gray-900">
{editingItem ? 'ویرایش آیتم منو' : 'افزودن آیتم جدید'}
</h3>
<button onClick={() => setIsModalOpen(false)} className="p-2 hover:bg-gray-100 rounded-lg">
<X className="w-5 h-5 text-gray-500" />
</button>
<Modal
isOpen={isModalOpen}
onClose={() => setIsModalOpen(false)}
title={editingItem ? 'ویرایش آیتم منو' : 'افزودن آیتم جدید'}
icon={Menu}
maxWidth="xl"
footer={
<div className="flex justify-end gap-2.5 w-full">
<Button
variant="secondary"
size="sm"
type="button"
onClick={() => setIsModalOpen(false)}
>
انصراف
</Button>
<Button
variant="primary"
size="sm"
type="submit"
form="menuForm"
isLoading={isSaving}
>
ذخیره
</Button>
</div>
<form onSubmit={handleSave} className="p-6 space-y-4">
}
>
<div className="space-y-6 text-right font-vazir">
<form id="menuForm" onSubmit={handleSave} className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-bold text-gray-700 mb-1">موقعیت در منو</label>
<select
value={formData.menuType}
onChange={(e) => setFormData({ ...formData, menuType: e.target.value as MenuType, parentId: null })}
className="w-full px-3 py-2.5 rounded-xl border border-gray-200 focus:border-blue-400 outline-none text-sm bg-white"
className="w-full px-3 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 outline-none text-sm bg-white"
>
{MENU_TABS.map((tab) => (
<option key={tab.key} value={tab.key}>{tab.label}</option>
@ -324,7 +348,7 @@ export default function MenuManager() {
<select
value={formData.parentId || ''}
onChange={(e) => setFormData({ ...formData, parentId: e.target.value || null })}
className="w-full px-3 py-2.5 rounded-xl border border-gray-200 focus:border-blue-400 outline-none text-sm bg-white"
className="w-full px-3 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 outline-none text-sm bg-white"
>
<option value=""> آیتم اصلی </option>
{parentItems.map((p) => (
@ -342,7 +366,7 @@ export default function MenuManager() {
value={formData.label}
onChange={(e) => setFormData({ ...formData, label: e.target.value })}
placeholder="مثال: فروشگاه تخصصی"
className="w-full px-4 py-2.5 rounded-xl border border-gray-200 focus:border-blue-400 outline-none text-sm"
className="w-full px-4 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 outline-none text-sm"
/>
</div>
@ -355,7 +379,7 @@ export default function MenuManager() {
onChange={(e) => setFormData({ ...formData, href: e.target.value })}
placeholder="مثال: /shop یا https://..."
dir="ltr"
className="w-full px-4 py-2.5 rounded-xl border border-gray-200 focus:border-blue-400 outline-none text-sm font-mono"
className="w-full px-4 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 outline-none text-sm font-mono"
/>
</div>
@ -367,7 +391,7 @@ export default function MenuManager() {
value={formData.icon}
onChange={(e) => setFormData({ ...formData, icon: e.target.value })}
placeholder="🐕 یا نام آیکون"
className="w-full px-4 py-2.5 rounded-xl border border-gray-200 focus:border-blue-400 outline-none text-sm"
className="w-full px-4 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 outline-none text-sm"
/>
</div>
<div>
@ -377,7 +401,7 @@ export default function MenuManager() {
value={formData.badge}
onChange={(e) => setFormData({ ...formData, badge: e.target.value })}
placeholder="مثال: جدید"
className="w-full px-4 py-2.5 rounded-xl border border-gray-200 focus:border-blue-400 outline-none text-sm"
className="w-full px-4 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 outline-none text-sm"
/>
</div>
</div>
@ -389,7 +413,7 @@ export default function MenuManager() {
type="number"
value={formData.order}
onChange={(e) => setFormData({ ...formData, order: Number(e.target.value) })}
className="w-full px-4 py-2.5 rounded-xl border border-gray-200 focus:border-blue-400 outline-none text-sm"
className="w-full px-4 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 outline-none text-sm"
/>
</div>
<label className="flex items-center justify-between p-3 bg-green-50 rounded-xl border border-green-200 cursor-pointer col-span-1 mt-auto">
@ -411,29 +435,12 @@ export default function MenuManager() {
/>
</label>
</div>
<div className="pt-4 flex justify-end gap-3 border-t border-gray-100">
<button
type="button"
onClick={() => setIsModalOpen(false)}
className="px-5 py-2.5 rounded-xl border border-gray-200 text-gray-600 font-bold hover:bg-gray-50"
>
انصراف
</button>
<button
type="submit"
disabled={isSaving}
className="px-6 py-2.5 rounded-xl bg-blue-600 text-white font-bold hover:bg-blue-700 shadow-md shadow-blue-200 flex items-center gap-2"
>
{isSaving && <Spinner size="sm" />}
ذخیره
</button>
</div>
</form>
</div>
</div>
</Modal>
)}
{deleteTargetId && (
<ConfirmModal
isOpen={true}

View File

@ -15,6 +15,9 @@ import {
import { toast } from 'react-hot-toast';
import api from '../services/api';
import Spinner from '../components/ui/Spinner';
import Button from '../components/ui/Button';
export default function PaymentGatewaysPage() {
const [activeTab, setActiveTab] = useState<'gateways' | 'wallets' | 'ebank' | 'submerchants'>('gateways');
@ -490,18 +493,20 @@ export default function PaymentGatewaysPage() {
</div>
<div className="flex justify-end">
<button
<Button
type="submit"
disabled={isSaving}
className="bg-indigo-600 hover:bg-indigo-700 text-white font-bold py-3 px-8 rounded-xl transition-colors flex items-center gap-2 shadow-md shadow-indigo-200 cursor-pointer text-sm"
variant="primary"
size="md"
startIcon={Save}
isLoading={isSaving}
>
{isSaving ? <Spinner size="sm" /> : <Save className="w-5 h-5" />}
<span>ذخیره کلیه تنظیمات درگاه زیبال</span>
</button>
ذخیره کلیه تنظیمات درگاه زیبال
</Button>
</div>
</form>
)}
{activeTab === 'wallets' && (
<div className="space-y-6">
{/* Wallets Overview */}

View File

@ -8,6 +8,9 @@ import Pagination from '../components/ui/Pagination';
import MediaSelector from '../components/ui/MediaSelector';
import ConfirmModal from '../components/ui/ConfirmModal';
import PriceInput from '../components/ui/PriceInput';
import Button from '../components/ui/Button';
export interface Category {
id: string;
@ -435,15 +438,17 @@ export default function Products() {
</h2>
<p className="text-gray-500 font-medium mt-1">افزودن و ویرایش پیشرفته محصولات با تنظیمات سئو</p>
</div>
<button
<Button
variant="primary"
size="sm"
startIcon={Plus}
onClick={() => openModal()}
className="bg-purple-600 hover:bg-purple-700 text-white px-5 py-2.5 rounded-xl flex items-center gap-2 font-bold transition-all shadow-md shadow-purple-200"
>
<Plus className="w-5 h-5" />
محصول جدید
</button>
</Button>
</div>
{/* Filters Bar */}
<div className="bg-white p-4 rounded-2xl shadow-sm border border-gray-200 flex flex-col md:flex-row gap-4">
<div className="relative flex-1">
@ -1650,18 +1655,29 @@ export default function Products() {
</form>
</div>
<div className="p-4 border-t border-gray-100 bg-gray-50 flex justify-end gap-3 rounded-b-2xl">
<button type="button" onClick={() => setIsModalOpen(false)} className="px-5 py-2.5 rounded-xl font-bold text-gray-600 hover:bg-gray-200 transition-colors">
<div className="p-4 border-t border-gray-100 bg-gray-50 flex justify-end gap-2.5 rounded-b-2xl">
<Button
variant="secondary"
size="sm"
type="button"
onClick={() => setIsModalOpen(false)}
>
انصراف
</button>
<button type="submit" form="productForm" className="bg-purple-600 hover:bg-purple-700 text-white px-8 py-2.5 rounded-xl font-bold transition-all shadow-md shadow-purple-200">
</Button>
<Button
variant="primary"
size="sm"
type="submit"
form="productForm"
>
ذخیره محصول
</button>
</Button>
</div>
</div>
</div>
)}
{/* Media Selector Modal */}
<MediaSelector
isOpen={isMediaSelectorOpen}

View File

@ -1,7 +1,11 @@
import { useState, useEffect } from 'react';
import { TrendingUp, DollarSign, ShoppingBag, Users, HeartHandshake, Download } from 'lucide-react';
import api from '../services/api';
import Button from '../components/ui/Button';
import Spinner from '../components/ui/Spinner';
import {
AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip as RechartsTooltip, ResponsiveContainer,
BarChart, Bar, PieChart, Pie, Cell, Legend
@ -97,7 +101,10 @@ export default function Reports() {
</h2>
<p className="text-gray-500 font-medium mt-1">آمار فروش، رفتار کاربران و عملکرد تخفیفها</p>
</div>
<button
<Button
variant="outline"
size="sm"
startIcon={Download}
onClick={() => {
if (!reportData) return;
const rows = [
@ -119,13 +126,12 @@ export default function Reports() {
link.click();
document.body.removeChild(link);
}}
className="bg-white border-2 border-purple-200 text-purple-700 hover:bg-purple-50 px-5 py-2.5 rounded-xl flex items-center gap-2 font-bold transition-all shadow-sm"
>
<Download className="w-5 h-5" />
خروجی اکسل (Excel)
</button>
</Button>
</div>
{/* Overview Cards */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
<div className="bg-gradient-to-br from-purple-500 to-indigo-600 p-6 rounded-2xl text-white shadow-lg shadow-purple-200">

View File

@ -18,6 +18,10 @@ import {
import { toast } from 'react-hot-toast';
import api from '../services/api';
import Spinner from '../components/ui/Spinner';
import Button from '../components/ui/Button';
import Modal from '../components/ui/Modal';
interface ProductReview {
id: string;
@ -434,21 +438,34 @@ export default function Reviews() {
{/* Reply Modal */}
{replyingReview && (
<div className="fixed inset-0 z-50 bg-black/40 backdrop-blur-sm flex items-center justify-center p-4">
<div className="bg-white rounded-2xl p-6 w-full max-w-lg shadow-2xl space-y-4 font-vazir animate-in fade-in zoom-in-95 duration-150" dir="rtl">
<div className="flex items-center justify-between border-b border-gray-100 pb-3">
<h3 className="text-base font-black text-gray-900 flex items-center gap-2">
<Reply className="w-5 h-5 text-purple-600" />
پاسخ به دیدگاه {replyingReview.userName}
</h3>
<button
<Modal
isOpen={!!replyingReview}
onClose={() => setReplyingReview(null)}
title={`پاسخ به دیدگاه ${replyingReview.userName}`}
icon={Reply}
maxWidth="lg"
footer={
<div className="flex items-center justify-end gap-2.5 w-full">
<Button
variant="secondary"
size="sm"
type="button"
onClick={() => setReplyingReview(null)}
className="p-1 text-gray-400 hover:text-gray-600 rounded-lg hover:bg-gray-100"
>
</button>
انصراف
</Button>
<Button
variant="primary"
size="sm"
isLoading={isSavingReply}
onClick={handleSaveReply}
>
ثبت و انتشار پاسخ
</Button>
</div>
}
>
<div className="space-y-4 font-vazir text-right">
<div className="p-3 bg-gray-50 rounded-xl border border-gray-100 text-xs text-gray-600 space-y-1">
<span className="font-bold text-gray-700 block">متن دیدگاه خریدار:</span>
<p className="leading-relaxed whitespace-pre-wrap">{replyingReview.comment}</p>
@ -466,28 +483,10 @@ export default function Reviews() {
className="w-full p-3 border border-gray-200 rounded-xl text-sm font-medium outline-none focus:ring-2 focus:ring-purple-500 resize-none leading-relaxed"
/>
</div>
<div className="flex items-center justify-end gap-3 pt-2">
<button
type="button"
onClick={() => setReplyingReview(null)}
className="px-4 py-2.5 text-xs font-bold text-gray-600 hover:bg-gray-100 rounded-xl transition-all"
>
انصراف
</button>
<button
type="button"
disabled={isSavingReply}
onClick={handleSaveReply}
className="px-5 py-2.5 text-xs font-bold bg-purple-600 hover:bg-purple-700 text-white rounded-xl shadow-lg shadow-purple-600/20 transition-all flex items-center gap-1.5"
>
{isSavingReply && <Loader2 className="w-3.5 h-3.5 animate-spin" />}
<span>ثبت و انتشار پاسخ</span>
</button>
</div>
</div>
</div>
</Modal>
)}
</div>
);
}

View File

@ -20,6 +20,8 @@ import {
} from 'lucide-react';
import api from '../services/api';
import { toast } from 'react-hot-toast';
import Button from '../components/ui/Button';
import Modal from '../components/ui/Modal';
interface TicketMessage {
id: string;
@ -340,47 +342,63 @@ export default function Tickets() {
{/* Ticket Details & Admin Reply Modal */}
{selectedTicket && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm">
<div className="bg-white w-full max-w-3xl rounded-3xl p-6 sm:p-8 shadow-2xl space-y-6 max-h-[90vh] flex flex-col" dir="rtl">
{/* Modal Header */}
<div className="flex items-start justify-between pb-4 border-b border-gray-100">
<div>
<div className="flex items-center gap-2">
<h3 className="text-lg font-black text-gray-900">{selectedTicket.subject}</h3>
<span className="px-2.5 py-0.5 bg-canina-blue/10 text-canina-blue font-mono font-bold text-xs rounded-lg">
{selectedTicket.ticketNumber}
</span>
</div>
<div className="flex items-center gap-3 text-xs text-gray-500 font-medium mt-1">
<span>کاربر: {selectedTicket.user?.firstName} {selectedTicket.user?.lastName} ({selectedTicket.user?.mobile})</span>
{selectedTicket.pet && (
<span className="text-purple-600 font-bold"> 🐾 {selectedTicket.pet.name} ({selectedTicket.pet.breed || selectedTicket.pet.type})</span>
)}
</div>
<Modal
isOpen={!!selectedTicket}
onClose={() => setSelectedTicket(null)}
title={`تیکت ${selectedTicket.ticketNumber}: ${selectedTicket.subject}`}
icon={MessageSquare}
maxWidth="3xl"
footer={
<div className="flex items-center justify-end gap-2.5 w-full">
<Button
variant="secondary"
size="sm"
type="button"
onClick={() => setSelectedTicket(null)}
>
بستن
</Button>
<Button
variant="primary"
size="sm"
type="submit"
form="ticketReplyForm"
startIcon={Send}
isLoading={sendingReply}
disabled={!replyMessage.trim()}
>
ارسال پاسخ به کاربر
</Button>
</div>
}
>
<div className="space-y-5 text-right font-vazir">
{/* Header meta */}
<div className="p-3 bg-gray-50 rounded-2xl border border-gray-100 flex flex-wrap items-center justify-between gap-3">
<div className="text-xs text-gray-600 font-medium">
<span>کاربر: {selectedTicket.user?.firstName} {selectedTicket.user?.lastName} ({selectedTicket.user?.mobile})</span>
{selectedTicket.pet && (
<span className="text-purple-600 font-bold block sm:inline sm:mr-2">🐾 {selectedTicket.pet.name} ({selectedTicket.pet.breed || selectedTicket.pet.type})</span>
)}
</div>
<div className="flex items-center gap-2">
<span className="text-xs font-bold text-gray-500">تغییر وضعیت:</span>
<select
value={selectedTicket.status}
onChange={(e) => handleUpdateStatus(e.target.value)}
className="px-2.5 py-1.5 border border-gray-200 rounded-xl text-xs font-bold bg-gray-50 focus:border-canina-blue outline-none"
className="px-2.5 py-1.5 border border-gray-200 rounded-xl text-xs font-bold bg-white focus:ring-2 focus:ring-purple-500 outline-none"
>
<option value="OPEN">در انتظار پاسخ</option>
<option value="IN_PROGRESS">در حال بررسی</option>
<option value="ANSWERED">پاسخ داده شد</option>
<option value="CLOSED">بسته شده</option>
</select>
<button
type="button"
onClick={() => setSelectedTicket(null)}
className="w-8 h-8 rounded-full bg-gray-100 hover:bg-gray-200 text-gray-600 flex items-center justify-center font-bold text-sm cursor-pointer"
>
</button>
</div>
</div>
{/* Messages Chat Scroll */}
<div className="flex-1 overflow-y-auto space-y-4 p-2 min-h-[250px]">
<div className="overflow-y-auto space-y-3.5 p-1 max-h-[340px]">
{(selectedTicket.messages || []).map((msg) => {
const isAdminOrVet = msg.senderRole === 'ADMIN' || msg.senderRole === 'VET_DOCTOR';
return (
@ -388,7 +406,7 @@ export default function Tickets() {
key={msg.id}
className={`p-4 rounded-2xl max-w-[85%] text-xs font-medium leading-relaxed ${
isAdminOrVet
? 'bg-emerald-50 border border-emerald-200 text-emerald-950 mr-auto'
? 'bg-purple-50 border border-purple-200 text-purple-950 mr-auto'
: 'bg-blue-50 border border-blue-200 text-blue-950 ml-auto'
}`}
>
@ -396,8 +414,8 @@ export default function Tickets() {
<span className="font-black text-[11px] flex items-center gap-1.5">
{isAdminOrVet ? (
<>
<Stethoscope className="w-3.5 h-3.5 text-emerald-600" />
<span className="text-emerald-700">{msg.senderName} (تیم پزشکی / ادمین)</span>
<Stethoscope className="w-3.5 h-3.5 text-purple-600" />
<span className="text-purple-700">{msg.senderName} (تیم پزشکی / ادمین)</span>
</>
) : (
<>
@ -417,7 +435,7 @@ export default function Tickets() {
</div>
{/* Admin Response Composer */}
<form onSubmit={handleSendReply} className="pt-3 border-t border-gray-100 space-y-3">
<form id="ticketReplyForm" onSubmit={handleSendReply} className="pt-3 border-t border-gray-100 space-y-3">
<div className="flex items-center justify-between text-xs">
<label className="font-bold text-gray-700">ارسال پاسخ دامپزشک / پشتیبانی:</label>
<div className="flex items-center gap-2">
@ -439,29 +457,13 @@ export default function Tickets() {
placeholder="متن پاسخ تخصصی به کاربر را وارد کنید..."
value={replyMessage}
onChange={(e) => setReplyMessage(e.target.value)}
className="w-full px-4 py-3 bg-gray-50 border border-gray-200 rounded-xl text-xs font-medium focus:border-canina-blue outline-none"
className="w-full px-4 py-3 bg-gray-50 border border-gray-200 rounded-xl text-xs font-medium focus:border-purple-500 focus:ring-2 focus:ring-purple-200 outline-none resize-none leading-relaxed"
/>
<div className="flex justify-end gap-3">
<button
type="button"
onClick={() => setSelectedTicket(null)}
className="px-4 py-2 rounded-xl text-xs font-bold text-gray-600 bg-gray-100 hover:bg-gray-200 cursor-pointer"
>
بستن
</button>
<button
type="submit"
disabled={sendingReply || !replyMessage.trim()}
className="px-6 py-2 bg-canina-blue text-white rounded-xl text-xs font-black hover:bg-canina-dark disabled:opacity-50 transition-all flex items-center gap-2 cursor-pointer shadow-md shadow-canina-blue/20"
>
<Send className="w-3.5 h-3.5" />
<span>{sendingReply ? 'در حال ارسال...' : 'ارسال پاسخ به کاربر'}</span>
</button>
</div>
</form>
</div>
</div>
</Modal>
)}
</div>
);
}

View File

@ -265,13 +265,15 @@ export default function Users() {
</div>
<div className="flex flex-col sm:flex-row items-center gap-3 w-full sm:w-auto">
<button
<Button
variant="primary"
size="sm"
startIcon={Plus}
onClick={() => setShowCreateModal(true)}
className="w-full sm:w-auto flex items-center justify-center gap-2 bg-purple-600 hover:bg-purple-700 text-white px-4 py-2.5 rounded-xl font-bold text-xs shadow-md shadow-purple-200 transition-all cursor-pointer"
>
<Plus className="w-4 h-4" />
افزودن کاربر جدید
</button>
</Button>
<select
className="bg-white border border-gray-200 text-gray-700 px-4 py-2.5 rounded-xl flex items-center gap-2 font-bold hover:bg-gray-50 transition-all outline-none w-full sm:w-auto text-xs"

View File

@ -4,6 +4,9 @@ import { toast } from 'react-hot-toast';
import api, { BASE_DOMAIN } from '../services/api';
import ConfirmModal from '../components/ui/ConfirmModal';
import MediaSelector from '../components/ui/MediaSelector';
import Button from '../components/ui/Button';
import Modal from '../components/ui/Modal';
interface Video {
id: string;
@ -179,15 +182,17 @@ export default function Videos() {
آپلود و انتخاب مستقیم ویدئو/کاور از گالری رسانه یا درج کد آیفرم آپارات/یوتیوب برای مشاوره ویدئویی دامپزشکان کنینا
</p>
</div>
<button
<Button
variant="primary"
size="sm"
startIcon={Plus}
onClick={() => handleOpenModal()}
className="bg-purple-600 hover:bg-purple-700 text-white px-5 py-3 rounded-xl font-black text-sm flex items-center gap-2 shadow-lg shadow-purple-600/20 transition-all hover:scale-105 shrink-0 cursor-pointer"
>
<Plus className="w-5 h-5" />
<span>افزودن ویدئوی جدید</span>
</button>
افزودن ویدئوی جدید
</Button>
</div>
<div className="bg-white p-4 rounded-2xl border border-gray-100 shadow-xs flex items-center gap-4">
<div className="relative flex-1">
<Search className="w-5 h-5 text-gray-400 absolute right-4 top-1/2 -translate-y-1/2" />
@ -310,22 +315,35 @@ export default function Videos() {
)}
{isModalOpen && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-xs font-vazir">
<div className="bg-white rounded-3xl max-w-2xl w-full p-6 sm:p-8 shadow-2xl border border-gray-100 max-h-[90vh] overflow-y-auto">
<div className="flex items-center justify-between pb-4 border-b border-gray-100 mb-6">
<h3 className="text-lg font-black text-gray-900 flex items-center gap-2">
<VideoIcon className="w-5 h-5 text-purple-600" />
{editingVideo ? 'ویرایش ویدئوی آموزشی' : 'افزودن ویدئوی جدید به آکادمی'}
</h3>
<button
<Modal
isOpen={isModalOpen}
onClose={() => setIsModalOpen(false)}
title={editingVideo ? 'ویرایش ویدئوی آموزشی' : 'افزودن ویدئوی جدید به آکادمی'}
icon={VideoIcon}
maxWidth="2xl"
footer={
<div className="flex justify-end gap-2.5 w-full">
<Button
variant="secondary"
size="sm"
type="button"
onClick={() => setIsModalOpen(false)}
className="p-2 text-gray-400 hover:text-gray-600 hover:bg-gray-50 rounded-xl transition-colors cursor-pointer"
>
<X className="w-5 h-5" />
</button>
انصراف
</Button>
<Button
variant="primary"
size="sm"
type="submit"
form="videoForm"
>
ذخیره اطلاعات
</Button>
</div>
<form onSubmit={handleSave} className="space-y-5">
}
>
<div className="space-y-5 text-right font-vazir">
<form id="videoForm" onSubmit={handleSave} className="space-y-5">
<div className="space-y-1.5">
<label className="text-xs font-black text-gray-700 block">عنوان ویدئو *</label>
<input
@ -404,14 +422,15 @@ export default function Videos() {
placeholder="آدرس تصویر یا انتخاب از گالری..."
className="flex-1 bg-gray-50 border border-gray-200 rounded-xl py-3 px-4 text-xs font-mono focus:outline-none focus:ring-2 focus:ring-purple-500/20"
/>
<button
<Button
type="button"
variant="outline"
size="sm"
startIcon={ImageIcon}
onClick={() => setIsThumbnailMediaOpen(true)}
className="px-4 py-3 bg-purple-50 text-purple-700 hover:bg-purple-100 rounded-xl text-xs font-black flex items-center gap-1.5 border border-purple-200 shrink-0 cursor-pointer"
>
<ImageIcon className="w-4 h-4" />
<span>انتخاب از گالری</span>
</button>
انتخاب کاور
</Button>
</div>
</div>
</div>
@ -437,14 +456,15 @@ export default function Videos() {
placeholder="https://... یا کد آیفرم آپارات/یوتیوب"
className="flex-1 bg-gray-50 border border-gray-200 rounded-xl py-3 px-4 text-xs font-mono focus:outline-none focus:ring-2 focus:ring-purple-500/20"
/>
<button
<Button
type="button"
variant="outline"
size="sm"
startIcon={Film}
onClick={() => setIsVideoMediaOpen(true)}
className="px-4 py-3 bg-purple-50 text-purple-700 hover:bg-purple-100 rounded-xl text-xs font-black flex items-center gap-1.5 border border-purple-200 shrink-0 cursor-pointer"
>
<Film className="w-4 h-4" />
<span>انتخاب ویدئو</span>
</button>
انتخاب ویدئو
</Button>
</div>
</div>
</div>
@ -471,27 +491,12 @@ export default function Videos() {
نمایش به عنوان ویدئوی ویژه در بخش «مشاوره ویدئویی» صفحه اصلی سایت
</label>
</div>
<div className="pt-4 flex justify-end gap-3 border-t border-gray-100">
<button
type="button"
onClick={() => setIsModalOpen(false)}
className="px-5 py-2.5 rounded-xl border border-gray-200 text-gray-600 font-bold text-sm hover:bg-gray-50 cursor-pointer"
>
انصراف
</button>
<button
type="submit"
className="px-6 py-2.5 rounded-xl bg-purple-600 text-white font-bold text-sm hover:bg-purple-700 shadow-md cursor-pointer"
>
ذخیره اطلاعات
</button>
</div>
</form>
</div>
</div>
</Modal>
)}
{/* Cover Thumbnail Media Selector Modal */}
<MediaSelector
isOpen={isThumbnailMediaOpen}

View File

@ -5,6 +5,9 @@ import api from '../services/api';
import Spinner from '../components/ui/Spinner';
import Pagination from '../components/ui/Pagination';
import ConfirmModal from '../components/ui/ConfirmModal';
import Button from '../components/ui/Button';
import Modal from '../components/ui/Modal';
export interface WikiTerm {
key: string;
@ -138,13 +141,14 @@ export default function Wiki() {
</h2>
<p className="text-gray-500 font-medium mt-1">مدیریت اصطلاحات علمی، مواد تشکیلدهنده و خواص درمانی</p>
</div>
<button
<Button
variant="primary"
size="sm"
startIcon={Plus}
onClick={() => openModal()}
className="bg-purple-600 hover:bg-purple-700 text-white px-5 py-2.5 rounded-xl flex items-center gap-2 font-bold transition-all shadow-md shadow-purple-200"
>
<Plus className="w-5 h-5" />
مفهوم جدید
</button>
</Button>
</div>
<div className="bg-white p-4 rounded-2xl shadow-sm border border-gray-200 flex flex-col sm:flex-row gap-4">
@ -184,8 +188,8 @@ export default function Wiki() {
<td className="py-4 px-6 text-gray-500 text-sm max-w-xs truncate">{t.definition}</td>
<td className="py-4 px-6">
<div className="flex items-center gap-2">
<button onClick={() => openModal(t)} className="p-2 text-blue-600 hover:bg-blue-50 rounded-lg transition-colors"><Edit2 className="w-4 h-4" /></button>
<button onClick={() => setDeleteTargetKey(t.key)} className="p-2 text-red-600 hover:bg-red-50 rounded-lg transition-colors"><Trash2 className="w-4 h-4" /></button>
<button onClick={() => openModal(t)} className="p-2 text-blue-600 hover:bg-blue-50 rounded-lg transition-colors cursor-pointer"><Edit2 className="w-4 h-4" /></button>
<button onClick={() => setDeleteTargetKey(t.key)} className="p-2 text-red-600 hover:bg-red-50 rounded-lg transition-colors cursor-pointer"><Trash2 className="w-4 h-4" /></button>
</div>
</td>
</tr>
@ -202,99 +206,109 @@ export default function Wiki() {
</div>
{isModalOpen && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-gray-900/60 backdrop-blur-sm">
<div className="bg-white rounded-2xl w-full max-w-2xl max-h-[95vh] flex flex-col shadow-2xl animate-in zoom-in duration-200">
<div className="px-6 py-4 border-b border-gray-100 flex justify-between items-center bg-gray-50/50">
<h3 className="text-xl font-bold text-gray-900">
{editingTerm ? 'ویرایش دانشنامه' : 'افزودن مفهوم جدید'}
</h3>
<button onClick={() => setIsModalOpen(false)} className="text-gray-400 hover:text-red-500 transition-colors">
<XCircle className="w-6 h-6" />
</button>
<Modal
isOpen={isModalOpen}
onClose={() => setIsModalOpen(false)}
title={editingTerm ? 'ویرایش دانشنامه' : 'افزودن مفهوم جدید'}
icon={BookOpen}
maxWidth="2xl"
footer={
<div className="flex justify-end gap-2.5 w-full">
<Button
variant="secondary"
size="sm"
type="button"
onClick={() => setIsModalOpen(false)}
>
انصراف
</Button>
<Button
variant="primary"
size="sm"
type="submit"
form="wikiForm"
>
ذخیره
</Button>
</div>
<div className="p-6 overflow-y-auto flex-1">
<form id="wikiForm" onSubmit={handleSave} className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<label className="text-sm font-bold text-gray-700">شناسه یکتا (Key) *</label>
<input required type="text" value={formData.key} disabled={!!editingTerm} dir="ltr" onChange={(e) => setFormData({ ...formData, key: e.target.value })} className="w-full px-4 py-3 rounded-xl border border-gray-200 focus:border-purple-500 outline-none font-mono text-sm disabled:bg-gray-100" />
</div>
<div className="space-y-2">
<label className="text-sm font-bold text-gray-700">نام اصطلاح (Term) *</label>
<input required type="text" value={formData.term} onChange={(e) => setFormData({ ...formData, term: e.target.value })} className="w-full px-4 py-3 rounded-xl border border-gray-200 focus:border-purple-500 outline-none" />
</div>
</div>
}
>
<div className="space-y-6 text-right font-vazir">
<form id="wikiForm" onSubmit={handleSave} className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<label className="text-sm font-bold text-gray-700">توضیح علمی *</label>
<textarea required value={formData.definition} onChange={(e) => setFormData({ ...formData, definition: e.target.value })} rows={5} className="w-full px-4 py-3 rounded-xl border border-gray-200 focus:border-purple-500 outline-none"></textarea>
<label className="text-sm font-bold text-gray-700">شناسه یکتا (Key) *</label>
<input required type="text" value={formData.key} disabled={!!editingTerm} dir="ltr" onChange={(e) => setFormData({ ...formData, key: e.target.value })} className="w-full px-4 py-3 rounded-xl border border-gray-200 focus:border-purple-500 outline-none font-mono text-sm disabled:bg-gray-100" />
</div>
{/* Related Products Selector (TASK-2.23) */}
<div className="space-y-2 border border-gray-200 p-4 rounded-xl bg-purple-50/40">
<label className="text-xs font-bold text-gray-800 block">محصولات مرتبط با این مقاله علمی (TASK-2.23)</label>
<select
onChange={(e) => {
const prodId = e.target.value;
if (prodId && !formData.relatedProducts.includes(prodId)) {
setFormData({ ...formData, relatedProducts: [...formData.relatedProducts, prodId] });
}
}}
className="w-full px-3 py-2 border rounded-xl text-xs bg-white"
>
<option value="">+ انتخاب محصول جهت افزودن به لیست مرتبط</option>
{productsList.map(p => (
<option key={p.id} value={p.id}>{p.nameFa} ({p.artNo})</option>
))}
</select>
<div className="flex flex-wrap gap-2 mt-2">
{formData.relatedProducts.map(pId => {
const prod = productsList.find(p => p.id === pId);
return (
<span key={pId} className="inline-flex items-center gap-1.5 px-3 py-1 bg-purple-100 text-purple-700 text-xs font-bold rounded-full">
<span>{prod ? prod.nameFa : pId}</span>
<button
type="button"
onClick={() => setFormData({ ...formData, relatedProducts: formData.relatedProducts.filter(id => id !== pId) })}
className="hover:text-red-600 font-black text-sm"
>
×
</button>
</span>
);
})}
</div>
<div className="space-y-2">
<label className="text-sm font-bold text-gray-700">نام اصطلاح (Term) *</label>
<input required type="text" value={formData.term} onChange={(e) => setFormData({ ...formData, term: e.target.value })} className="w-full px-4 py-3 rounded-xl border border-gray-200 focus:border-purple-500 outline-none" />
</div>
</div>
<div className="space-y-4 border border-gray-200 p-4 rounded-xl bg-gray-50/50">
<h4 className="font-bold text-purple-700 text-sm mb-2">تنظیمات سئو (SEO)</h4>
<div className="space-y-2">
<label className="text-xs font-bold text-gray-700">عنوان متا</label>
<input type="text" value={formData.metaTitle} onChange={(e) => setFormData({ ...formData, metaTitle: e.target.value })} className="w-full px-3 py-2 rounded-lg border border-gray-200 focus:border-purple-500 outline-none text-sm" />
</div>
<div className="space-y-2">
<label className="text-xs font-bold text-gray-700">کلمات کلیدی</label>
<input type="text" value={formData.keywords} onChange={(e) => setFormData({ ...formData, keywords: e.target.value })} className="w-full px-3 py-2 rounded-lg border border-gray-200 focus:border-purple-500 outline-none text-sm" />
</div>
<div className="space-y-2">
<label className="text-xs font-bold text-gray-700">توضیحات متا</label>
<textarea rows={2} value={formData.metaDescription} onChange={(e) => setFormData({ ...formData, metaDescription: e.target.value })} className="w-full px-3 py-2 rounded-lg border border-gray-200 focus:border-purple-500 outline-none text-sm"></textarea>
</div>
<div className="space-y-2">
<label className="text-sm font-bold text-gray-700">توضیح علمی *</label>
<textarea required value={formData.definition} onChange={(e) => setFormData({ ...formData, definition: e.target.value })} rows={5} className="w-full px-4 py-3 rounded-xl border border-gray-200 focus:border-purple-500 outline-none"></textarea>
</div>
{/* Related Products Selector */}
<div className="space-y-2 border border-gray-200 p-4 rounded-xl bg-purple-50/40">
<label className="text-xs font-bold text-gray-800 block">محصولات مرتبط با این مقاله علمی</label>
<select
onChange={(e) => {
const prodId = e.target.value;
if (prodId && !formData.relatedProducts.includes(prodId)) {
setFormData({ ...formData, relatedProducts: [...formData.relatedProducts, prodId] });
}
}}
className="w-full px-3 py-2 border rounded-xl text-xs bg-white"
>
<option value="">+ انتخاب محصول جهت افزودن به لیست مرتبط</option>
{productsList.map(p => (
<option key={p.id} value={p.id}>{p.nameFa} ({p.artNo})</option>
))}
</select>
<div className="flex flex-wrap gap-2 mt-2">
{formData.relatedProducts.map(pId => {
const prod = productsList.find(p => p.id === pId);
return (
<span key={pId} className="inline-flex items-center gap-1.5 px-3 py-1 bg-purple-100 text-purple-700 text-xs font-bold rounded-full">
<span>{prod ? prod.nameFa : pId}</span>
<button
type="button"
onClick={() => setFormData({ ...formData, relatedProducts: formData.relatedProducts.filter(id => id !== pId) })}
className="hover:text-red-600 font-black text-sm cursor-pointer"
>
×
</button>
</span>
);
})}
</div>
</div>
</form>
</div>
<div className="p-4 border-t border-gray-100 bg-gray-50 flex justify-end gap-3 rounded-b-2xl">
<button type="button" onClick={() => setIsModalOpen(false)} className="px-5 py-2.5 rounded-xl font-bold text-gray-600 hover:bg-gray-200 transition-colors">انصراف</button>
<button type="submit" form="wikiForm" className="bg-purple-600 hover:bg-purple-700 text-white px-8 py-2.5 rounded-xl font-bold transition-all shadow-md shadow-purple-200">ذخیره</button>
</div>
<div className="space-y-4 border border-gray-200 p-4 rounded-xl bg-gray-50/50">
<h4 className="font-bold text-purple-700 text-sm mb-2">تنظیمات سئو (SEO)</h4>
<div className="space-y-2">
<label className="text-xs font-bold text-gray-700">عنوان متا</label>
<input type="text" value={formData.metaTitle} onChange={(e) => setFormData({ ...formData, metaTitle: e.target.value })} className="w-full px-3 py-2 rounded-lg border border-gray-200 focus:border-purple-500 outline-none text-sm" />
</div>
<div className="space-y-2">
<label className="text-xs font-bold text-gray-700">کلمات کلیدی</label>
<input type="text" value={formData.keywords} onChange={(e) => setFormData({ ...formData, keywords: e.target.value })} className="w-full px-3 py-2 rounded-lg border border-gray-200 focus:border-purple-500 outline-none text-sm" />
</div>
<div className="space-y-2">
<label className="text-xs font-bold text-gray-700">توضیحات متا</label>
<textarea rows={2} value={formData.metaDescription} onChange={(e) => setFormData({ ...formData, metaDescription: e.target.value })} className="w-full px-3 py-2 rounded-lg border border-gray-200 focus:border-purple-500 outline-none text-sm"></textarea>
</div>
</div>
</form>
</div>
</div>
</Modal>
)}
<ConfirmModal
isOpen={!!deleteTargetKey}
title="حذف اصطلاح علمی"

View File

@ -26,6 +26,9 @@ import {
import { toast } from 'react-hot-toast';
import api from '../services/api';
import Spinner from '../components/ui/Spinner';
import Button from '../components/ui/Button';
import Modal from '../components/ui/Modal';
export default function ZibalPortalPage() {
const [activeMainTab, setActiveMainTab] = useState<'refunds' | 'checkouts' | 'queue' | 'gateway_transactions'>('refunds');
@ -378,25 +381,27 @@ export default function ZibalPortalPage() {
<div className="bg-white rounded-2xl shadow-sm border border-gray-200 p-6 space-y-6">
{/* Top Bar with Filter & Search Controls */}
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4 border-b border-gray-100 pb-5">
<div className="flex items-center gap-3">
<button
<div className="flex items-center gap-2.5">
<Button
variant="primary"
size="sm"
startIcon={Download}
onClick={() => downloadCSV(refunds, 'zibal-refunds')}
className="bg-indigo-600 hover:bg-indigo-700 text-white text-xs font-bold px-4 py-2.5 rounded-xl flex items-center gap-2 shadow-sm transition-colors cursor-pointer"
>
<Download className="w-4 h-4" />
<span>دانلود گزارش</span>
</button>
دانلود گزارش
</Button>
<button
<Button
variant="outline"
size="sm"
onClick={fetchRefunds}
disabled={loadingRefunds}
className="p-2.5 border border-gray-200 text-gray-600 hover:bg-gray-50 rounded-xl transition-colors cursor-pointer"
isLoading={loadingRefunds}
startIcon={RefreshCw}
title="بروزرسانی"
>
<RefreshCw className={`w-4 h-4 ${loadingRefunds ? 'animate-spin' : ''}`} />
</button>
/>
</div>
{/* Filter Inputs Bar */}
<div className="flex flex-wrap items-center gap-3">
{/* Date Filter */}
@ -634,25 +639,27 @@ export default function ZibalPortalPage() {
{activeMainTab === 'checkouts' && (
<div className="bg-white rounded-2xl shadow-sm border border-gray-200 p-6 space-y-6">
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4 border-b border-gray-100 pb-5">
<div className="flex items-center gap-3">
<button
<div className="flex items-center gap-2.5">
<Button
variant="primary"
size="sm"
startIcon={Download}
onClick={() => downloadCSV(checkouts, 'zibal-checkouts')}
className="bg-indigo-600 hover:bg-indigo-700 text-white text-xs font-bold px-4 py-2.5 rounded-xl flex items-center gap-2 shadow-sm transition-colors cursor-pointer"
>
<Download className="w-4 h-4" />
<span>دانلود گزارش</span>
</button>
دانلود گزارش
</Button>
<button
<Button
variant="outline"
size="sm"
onClick={fetchCheckouts}
disabled={loadingCheckouts}
className="p-2.5 border border-gray-200 text-gray-600 hover:bg-gray-50 rounded-xl transition-colors cursor-pointer"
isLoading={loadingCheckouts}
startIcon={RefreshCw}
title="بروزرسانی"
>
<RefreshCw className={`w-4 h-4 ${loadingCheckouts ? 'animate-spin' : ''}`} />
</button>
/>
</div>
{/* Filter Inputs Bar */}
<div className="flex flex-wrap items-center gap-3">
<div className="flex items-center gap-1.5 bg-gray-50 border border-gray-200 px-3 py-1.5 rounded-xl text-xs font-bold">
@ -851,25 +858,27 @@ export default function ZibalPortalPage() {
{activeMainTab === 'gateway_transactions' && (
<div className="bg-white rounded-2xl shadow-sm border border-gray-200 p-6 space-y-6">
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4 border-b border-gray-100 pb-5">
<div className="flex items-center gap-3">
<button
<div className="flex items-center gap-2.5">
<Button
variant="primary"
size="sm"
startIcon={Download}
onClick={() => downloadCSV(gatewayTxList, 'zibal-ipg-transactions')}
className="bg-indigo-600 hover:bg-indigo-700 text-white text-xs font-bold px-4 py-2.5 rounded-xl flex items-center gap-2 shadow-sm transition-colors cursor-pointer"
>
<Download className="w-4 h-4" />
<span>دانلود گزارش درگاه</span>
</button>
دانلود گزارش درگاه
</Button>
<button
<Button
variant="outline"
size="sm"
onClick={fetchGatewayTx}
disabled={loadingGatewayTx}
className="p-2.5 border border-gray-200 text-gray-600 hover:bg-gray-50 rounded-xl transition-colors cursor-pointer"
isLoading={loadingGatewayTx}
startIcon={RefreshCw}
title="بروزرسانی"
>
<RefreshCw className={`w-4 h-4 ${loadingGatewayTx ? 'animate-spin' : ''}`} />
</button>
/>
</div>
{/* Filter Inputs */}
<div className="flex flex-wrap items-center gap-3">
<div className="flex items-center gap-1.5 bg-gray-50 border border-gray-200 px-3 py-1.5 rounded-xl text-xs font-bold">
@ -1013,25 +1022,40 @@ export default function ZibalPortalPage() {
)}
{/* ======================================================== */}
{/* MODAL: SUBMIT NEW REFUND / REVERSE */}
{/* MODAL: NEW REFUND SUBMIT */}
{/* ======================================================== */}
{newRefundModal && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/50 backdrop-blur-sm">
<div className="bg-white rounded-3xl w-full max-w-lg border border-gray-200 shadow-2xl p-6 sm:p-8 space-y-6">
<div className="flex items-center justify-between border-b border-gray-100 pb-4">
<h3 className="text-base font-black text-gray-900 flex items-center gap-2">
<RotateCcw className="w-5 h-5 text-rose-600" />
ثبت درخواست استرداد وجه (Refund / Reverse)
</h3>
<button
<Modal
isOpen={newRefundModal}
onClose={() => setNewRefundModal(false)}
title="ثبت و ارسال استرداد وجه جدید (Refund / Reverse)"
icon={RotateCcw}
maxWidth="lg"
footer={
<div className="flex justify-end gap-2.5 w-full">
<Button
variant="secondary"
size="sm"
type="button"
onClick={() => setNewRefundModal(false)}
className="text-gray-400 hover:text-gray-700 font-bold text-lg cursor-pointer"
>
</button>
انصراف
</Button>
<Button
variant="danger"
size="sm"
type="submit"
form="zibalRefundForm"
startIcon={Send}
isLoading={isSubmittingRefund}
>
ارسال به زیبال
</Button>
</div>
<form onSubmit={handleCreateRefundSubmit} className="space-y-4 text-xs">
}
>
<div className="space-y-4 text-xs font-vazir text-right">
<form id="zibalRefundForm" onSubmit={handleCreateRefundSubmit} className="space-y-4 text-xs">
<div>
<label className="block font-bold text-gray-700 mb-1">شماره تراکنش شاپرک (Track ID) *</label>
<input
@ -1092,47 +1116,38 @@ export default function ZibalPortalPage() {
اولویت با Reverse باشد (برگشت آنی قبل از تسویه شاپرک)
</label>
</div>
<div className="flex justify-end gap-3 pt-3">
<button
type="button"
onClick={() => setNewRefundModal(false)}
className="px-5 py-2.5 rounded-xl font-bold bg-gray-100 hover:bg-gray-200 text-gray-600 cursor-pointer"
>
انصراف
</button>
<button
type="submit"
disabled={isSubmittingRefund}
className="bg-rose-600 hover:bg-rose-700 text-white font-bold px-6 py-2.5 rounded-xl transition-colors flex items-center gap-2 cursor-pointer shadow-md shadow-rose-200"
>
{isSubmittingRefund ? <Spinner size="sm" /> : <Send className="w-4 h-4" />}
<span>ارسال به زیبال</span>
</button>
</div>
</form>
</div>
</div>
</Modal>
)}
{/* ======================================================== */}
{/* MODAL: DETAIL / INQUIRY VIEWER */}
{/* ======================================================== */}
{detailModalItem && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/50 backdrop-blur-sm">
<div className="bg-white rounded-3xl w-full max-w-lg border border-gray-200 shadow-2xl p-6 space-y-4">
<div className="flex items-center justify-between border-b border-gray-100 pb-3">
<h3 className="font-black text-sm text-gray-900">{detailModalItem.title}</h3>
<button onClick={() => setDetailModalItem(null)} className="text-gray-400 font-bold text-lg">
</button>
<Modal
isOpen={!!detailModalItem}
onClose={() => setDetailModalItem(null)}
title={detailModalItem.title}
maxWidth="lg"
footer={
<div className="flex justify-end w-full">
<Button
variant="secondary"
size="sm"
onClick={() => setDetailModalItem(null)}
>
بستن
</Button>
</div>
<pre className="bg-slate-900 text-emerald-400 p-4 rounded-xl text-[11px] font-mono overflow-x-auto dir-ltr">
{JSON.stringify(detailModalItem.data, null, 2)}
</pre>
</div>
</div>
}
>
<pre className="bg-slate-900 text-emerald-400 p-4 rounded-xl text-[11px] font-mono overflow-x-auto dir-ltr">
{JSON.stringify(detailModalItem.data, null, 2)}
</pre>
</Modal>
)}
</div>
);
}