917 lines
54 KiB
TypeScript
917 lines
54 KiB
TypeScript
import { useState, useEffect, useCallback } from 'react';
|
||
import { Search, Filter, Box, Plus, Edit2, Trash2, Image as ImageIcon, X, HelpCircle } from 'lucide-react';
|
||
import { toast } from 'react-hot-toast';
|
||
import api, { BASE_DOMAIN } from '../services/api';
|
||
import Spinner from '../components/ui/Spinner';
|
||
import Pagination from '../components/ui/Pagination';
|
||
import MediaSelector from '../components/ui/MediaSelector';
|
||
import ConfirmModal from '../components/ui/ConfirmModal';
|
||
|
||
export interface Category {
|
||
id: string;
|
||
nameFa: string;
|
||
nameEn?: string;
|
||
}
|
||
|
||
export interface Product {
|
||
id: string;
|
||
artNo: string;
|
||
nameFa: string;
|
||
nameEn?: string;
|
||
scientificTagline?: string;
|
||
description?: string;
|
||
shortDescription?: string;
|
||
categoryId?: string;
|
||
priceValue?: number;
|
||
wholesalePrice?: number;
|
||
priceDisplay?: string;
|
||
unit?: string;
|
||
packageSize?: number;
|
||
dosageLogic?: string;
|
||
suitableFor?: string;
|
||
imageUrl?: string;
|
||
metaTitle?: string;
|
||
metaDescription?: string;
|
||
keywords?: string;
|
||
canonicalUrl?: string;
|
||
slug?: string;
|
||
images?: string[];
|
||
podcastUrl?: string;
|
||
videoUrl?: string;
|
||
pdfUrl?: string;
|
||
symptoms?: Array<{ symptom: string } | string>;
|
||
}
|
||
|
||
export default function Products() {
|
||
const [products, setProducts] = useState<Product[]>([]);
|
||
const [categories, setCategories] = useState<Category[]>([]);
|
||
const [isLoading, setIsLoading] = useState(true);
|
||
|
||
// URL and String Helper Functions
|
||
const getProductImageUrl = (url: string) => {
|
||
if (!url) return '';
|
||
if (url.startsWith('http')) {
|
||
const path = url.replace(/^https?:\/\/[^/]+/, '');
|
||
return `${BASE_DOMAIN}${path}`;
|
||
}
|
||
return url.startsWith('/') ? `${BASE_DOMAIN}${url}` : `${BASE_DOMAIN}/${url}`;
|
||
};
|
||
|
||
const getStoreProductUrl = (slug: string) => {
|
||
const isDev = window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1';
|
||
if (isDev) {
|
||
return `http://localhost:4000/shop/${slug}`;
|
||
}
|
||
// Production fallback based on actual URL
|
||
const storeOrigin = window.location.origin.replace('admin', 'www');
|
||
return `${storeOrigin}/shop/${slug}`;
|
||
};
|
||
|
||
const slugify = (text: string) => {
|
||
return text
|
||
.toLowerCase()
|
||
.trim()
|
||
.replace(/\s+/g, '-')
|
||
.replace(/[^a-z0-9-]/g, '')
|
||
.replace(/-+/g, '-')
|
||
.replace(/^-|-$/g, '');
|
||
};
|
||
|
||
// Filters
|
||
const [search, setSearch] = useState('');
|
||
const [categoryFilter, setCategoryFilter] = useState('');
|
||
const [page, setPage] = useState(1);
|
||
const [totalPages, setTotalPages] = useState(1);
|
||
const [imageErrors, setImageErrors] = useState<Record<string, boolean>>({});
|
||
const [mediaImageError, setMediaImageError] = useState(false);
|
||
const limit = 10;
|
||
|
||
// Modal State
|
||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||
const [isMediaSelectorOpen, setIsMediaSelectorOpen] = useState(false);
|
||
const [editingProduct, setEditingProduct] = useState<Product | null>(null);
|
||
const [deleteTargetId, setDeleteTargetId] = useState<string | null>(null);
|
||
const [activeTab, setActiveTab] = useState('general');
|
||
|
||
const [formData, setFormData] = useState({
|
||
artNo: '',
|
||
nameFa: '',
|
||
nameEn: '',
|
||
scientificTagline: '',
|
||
description: '',
|
||
shortDescription: '',
|
||
categoryId: '',
|
||
priceValue: 0,
|
||
wholesalePrice: '' as number | string,
|
||
priceDisplay: '',
|
||
unit: '',
|
||
packageSize: 0,
|
||
dosageLogic: '',
|
||
suitableFor: 'سگ و گربه',
|
||
imageUrl: '',
|
||
metaTitle: '',
|
||
metaDescription: '',
|
||
keywords: '',
|
||
canonicalUrl: '',
|
||
slug: '',
|
||
images: [] as string[],
|
||
podcastUrl: '',
|
||
videoUrl: '',
|
||
pdfUrl: '',
|
||
symptoms: [] as string[]
|
||
});
|
||
|
||
const fetchData = useCallback(async () => {
|
||
try {
|
||
// Fetch products first
|
||
const prodRes = await api.get('/admin/products', { params: { page, limit, search, categoryId: categoryFilter } });
|
||
if (prodRes.data?.data) {
|
||
setProducts(prodRes.data.data);
|
||
setTotalPages(prodRes.data.meta?.lastPage || 1);
|
||
}
|
||
|
||
// Fetch categories separately so it doesn't break products
|
||
try {
|
||
const catRes = await api.get('/admin/categories');
|
||
if (catRes.data?.data) {
|
||
setCategories(catRes.data.data);
|
||
} else if (Array.isArray(catRes.data)) {
|
||
setCategories(catRes.data);
|
||
}
|
||
} catch (err) {
|
||
console.warn('Could not fetch categories', err);
|
||
}
|
||
} catch (err: unknown) {
|
||
console.error(err);
|
||
toast.error('خطا در دریافت لیست محصولات');
|
||
} finally {
|
||
setIsLoading(false);
|
||
}
|
||
}, [page, search, categoryFilter]);
|
||
|
||
useEffect(() => {
|
||
const timer = setTimeout(() => {
|
||
fetchData();
|
||
}, 500);
|
||
return () => clearTimeout(timer);
|
||
}, [fetchData]);
|
||
|
||
const openModal = (product: Product | null = null) => {
|
||
setMediaImageError(false);
|
||
if (product) {
|
||
setEditingProduct(product);
|
||
setFormData({
|
||
artNo: product.artNo,
|
||
nameFa: product.nameFa || '',
|
||
nameEn: product.nameEn || '',
|
||
scientificTagline: product.scientificTagline || '',
|
||
description: product.description || '',
|
||
shortDescription: product.shortDescription || '',
|
||
categoryId: product.categoryId || '',
|
||
priceValue: Number(product.priceValue),
|
||
wholesalePrice: product.wholesalePrice ? Number(product.wholesalePrice) : '',
|
||
priceDisplay: product.priceDisplay || '',
|
||
unit: product.unit || '',
|
||
packageSize: Number(product.packageSize),
|
||
dosageLogic: product.dosageLogic || '',
|
||
suitableFor: product.suitableFor || 'سگ و گربه',
|
||
imageUrl: product.imageUrl || '',
|
||
metaTitle: product.metaTitle || '',
|
||
metaDescription: product.metaDescription || '',
|
||
keywords: product.keywords || '',
|
||
canonicalUrl: product.canonicalUrl || '',
|
||
slug: product.slug || '',
|
||
images: Array.isArray(product.images) ? product.images : [],
|
||
podcastUrl: product.podcastUrl || '',
|
||
videoUrl: product.videoUrl || '',
|
||
pdfUrl: product.pdfUrl || '',
|
||
symptoms: product.symptoms ? product.symptoms.map((s: { symptom: string } | string) => typeof s === 'string' ? s : s.symptom) : []
|
||
});
|
||
} else {
|
||
setEditingProduct(null);
|
||
setFormData({
|
||
artNo: '', nameFa: '', nameEn: '', scientificTagline: '', description: '', shortDescription: '', categoryId: '',
|
||
priceValue: 0, wholesalePrice: '', priceDisplay: '', unit: '', packageSize: 0, dosageLogic: '', suitableFor: 'سگ و گربه',
|
||
imageUrl: '', metaTitle: '', metaDescription: '', keywords: '', canonicalUrl: '', slug: '',
|
||
images: [], podcastUrl: '', videoUrl: '', pdfUrl: '',
|
||
symptoms: []
|
||
});
|
||
}
|
||
setActiveTab('general');
|
||
setIsModalOpen(true);
|
||
};
|
||
|
||
const handleSave = async (e: React.FormEvent) => {
|
||
e.preventDefault();
|
||
try {
|
||
const derivedSlug = formData.slug.trim() || slugify(formData.nameEn);
|
||
const finalPayload = {
|
||
...formData,
|
||
slug: derivedSlug,
|
||
metaTitle: formData.metaTitle.trim() || `${formData.nameFa || 'نام محصول'} | خرید و قیمت مکمل کانینا`,
|
||
metaDescription: formData.metaDescription.trim() || formData.shortDescription || 'خرید آنلاین مکمل اصل کانینا آلمان با بالاترین کیفیت بالینی...',
|
||
canonicalUrl: formData.canonicalUrl.trim() || `/shop/${derivedSlug}`
|
||
};
|
||
|
||
if (editingProduct) {
|
||
await api.put(`/admin/products/${editingProduct.id}`, finalPayload);
|
||
toast.success('محصول با موفقیت ویرایش شد');
|
||
} else {
|
||
await api.post('/admin/products', finalPayload);
|
||
toast.success('محصول با موفقیت ایجاد شد');
|
||
}
|
||
setIsModalOpen(false);
|
||
setImageErrors({});
|
||
fetchData();
|
||
} catch (error) {
|
||
console.error('Save failed', error);
|
||
toast.error('خطا در ذخیره محصول');
|
||
}
|
||
};
|
||
|
||
const confirmDelete = async () => {
|
||
if (!deleteTargetId) return;
|
||
try {
|
||
await api.delete(`/admin/products/${deleteTargetId}`);
|
||
toast.success('محصول با موفقیت حذف شد');
|
||
setImageErrors({});
|
||
fetchData();
|
||
} catch (error) {
|
||
console.error('Delete failed', error);
|
||
toast.error('خطا در حذف محصول');
|
||
} finally {
|
||
setDeleteTargetId(null);
|
||
}
|
||
};
|
||
|
||
return (
|
||
<div className="space-y-6">
|
||
<div className="flex flex-col sm:flex-row justify-between gap-4">
|
||
<div>
|
||
<h2 className="text-2xl font-black text-gray-900 flex items-center gap-2">
|
||
<Box className="w-6 h-6 text-purple-600" />
|
||
مدیریت محصولات
|
||
</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>
|
||
</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" />
|
||
<input
|
||
type="text"
|
||
placeholder="جستجو در نام و کد محصول..."
|
||
value={search}
|
||
onChange={(e) => { setSearch(e.target.value); setPage(1); }}
|
||
className="w-full pl-4 pr-12 py-3 rounded-xl border border-gray-200 focus:border-purple-500 focus:ring-2 focus:ring-purple-200 outline-none transition-all"
|
||
/>
|
||
</div>
|
||
<div className="relative w-full sm:w-64">
|
||
<Filter className="w-5 h-5 absolute right-4 top-1/2 -translate-y-1/2 text-gray-400" />
|
||
<select
|
||
value={categoryFilter}
|
||
onChange={(e) => { setCategoryFilter(e.target.value); setPage(1); }}
|
||
className="w-full pl-4 pr-12 py-3 rounded-xl border border-gray-200 focus:border-purple-500 focus:ring-2 focus:ring-purple-200 outline-none transition-all appearance-none"
|
||
>
|
||
<option value="">همه دستهبندیها</option>
|
||
{categories.map(c => <option key={c.id} value={c.id}>{c.name}</option>)}
|
||
</select>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="bg-white rounded-2xl shadow-sm border border-gray-200 overflow-hidden">
|
||
<div className="overflow-x-auto">
|
||
<table className="w-full text-right">
|
||
<thead className="bg-gray-50 border-b border-gray-100">
|
||
<tr>
|
||
<th className="py-4 px-6 text-sm font-bold text-gray-500">تصویر</th>
|
||
<th className="py-4 px-6 text-sm font-bold text-gray-500">کد (Art No)</th>
|
||
<th className="py-4 px-6 text-sm font-bold text-gray-500">نام محصول</th>
|
||
<th className="py-4 px-6 text-sm font-bold text-gray-500">دستهبندی</th>
|
||
<th className="py-4 px-6 text-sm font-bold text-gray-500">موجودی / قیمت</th>
|
||
<th className="py-4 px-6 text-sm font-bold text-gray-500 w-24">عملیات</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody className="divide-y divide-gray-100">
|
||
{isLoading ? (
|
||
<tr>
|
||
<td colSpan={6} className="py-12 text-center">
|
||
<Spinner size="lg" className="mx-auto text-purple-600" />
|
||
</td>
|
||
</tr>
|
||
) : products.length === 0 ? (
|
||
<tr>
|
||
<td colSpan={6} className="py-12 text-center text-gray-500 font-medium">محصولی یافت نشد</td>
|
||
</tr>
|
||
) : (
|
||
products.map((product) => (
|
||
<tr key={product.id} className="hover:bg-gray-50/50 transition-colors">
|
||
<td className="py-3 px-6">
|
||
{product.imageUrl && !imageErrors[product.id] ? (
|
||
<div className="w-12 h-12 rounded-xl bg-gray-50 border border-gray-200 p-1 flex items-center justify-center overflow-hidden">
|
||
<img
|
||
src={getProductImageUrl(product.imageUrl)}
|
||
alt=""
|
||
onError={() => setImageErrors(prev => ({ ...prev, [product.id]: true }))}
|
||
className="max-w-full max-h-full object-contain object-center"
|
||
/>
|
||
</div>
|
||
) : (
|
||
<div className="w-12 h-12 rounded-xl bg-gray-100 flex items-center justify-center text-gray-400">
|
||
<ImageIcon className="w-5 h-5" />
|
||
</div>
|
||
)}
|
||
</td>
|
||
<td className="py-4 px-6 text-gray-500 font-mono text-sm">{product.artNo}</td>
|
||
<td className="py-4 px-6">
|
||
<div className="flex flex-col gap-0.5">
|
||
<span className="font-bold text-gray-900" dir="rtl">{product.nameFa || product.name || '-'}</span>
|
||
{product.nameEn && <span className="text-xs text-gray-400 font-mono" dir="ltr">{product.nameEn}</span>}
|
||
</div>
|
||
</td>
|
||
<td className="py-4 px-6 text-gray-600 text-sm">{product.category?.name || 'بدون دسته'}</td>
|
||
<td className="py-4 px-6">
|
||
<div className="flex flex-col">
|
||
<span className="font-bold text-gray-900">{Number(product.priceValue).toLocaleString()} تومان</span>
|
||
<span className="text-xs text-gray-500">{product.packageSize} موجودی</span>
|
||
</div>
|
||
</td>
|
||
<td className="py-4 px-6">
|
||
<div className="flex items-center gap-2">
|
||
<button onClick={() => openModal(product)} className="p-2 text-blue-600 hover:bg-blue-50 rounded-lg transition-colors">
|
||
<Edit2 className="w-4 h-4" />
|
||
</button>
|
||
<button onClick={() => setDeleteTargetId(product.id)} className="p-2 text-red-600 hover:bg-red-50 rounded-lg transition-colors">
|
||
<Trash2 className="w-4 h-4" />
|
||
</button>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
))
|
||
)}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
{!isLoading && totalPages > 1 && (
|
||
<div className="p-4 border-t border-gray-100 flex justify-center bg-gray-50/50">
|
||
<Pagination currentPage={page} totalPages={totalPages} onPageChange={setPage} />
|
||
</div>
|
||
)}
|
||
</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-[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">
|
||
<div className="flex items-center gap-3">
|
||
<h3 className="text-xl font-bold text-gray-900">
|
||
{editingProduct ? 'ویرایش پیشرفته محصول' : 'افزودن محصول جدید'}
|
||
</h3>
|
||
{editingProduct && formData.slug && (
|
||
<a
|
||
href={getStoreProductUrl(formData.slug)}
|
||
target="_blank"
|
||
rel="noreferrer"
|
||
className="inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-xs font-bold bg-purple-50 border border-purple-100 text-purple-600 hover:bg-purple-100 transition-all cursor-pointer"
|
||
>
|
||
مشاهده آنلاین در سایت 🔗
|
||
</a>
|
||
)}
|
||
</div>
|
||
<button onClick={() => setIsModalOpen(false)} className="text-gray-400 hover:text-red-500 transition-colors">
|
||
<X className="w-6 h-6" />
|
||
</button>
|
||
</div>
|
||
|
||
<div className="flex border-b border-gray-100 px-6 pt-2 gap-4">
|
||
{[
|
||
{ id: 'general', label: 'اطلاعات پایه' },
|
||
{ id: 'pricing', label: 'موجودی و قیمت' },
|
||
{ id: 'seo', label: 'سئو (SEO)' },
|
||
{ id: 'media', label: 'تصاویر' }
|
||
].map(tab => (
|
||
<button
|
||
key={tab.id}
|
||
type="button"
|
||
onClick={() => setActiveTab(tab.id)}
|
||
className={`pb-3 px-2 text-sm font-bold border-b-2 transition-colors ${activeTab === tab.id ? 'border-purple-600 text-purple-700' : 'border-transparent text-gray-500 hover:text-gray-700'}`}
|
||
>
|
||
{tab.label}
|
||
</button>
|
||
))}
|
||
</div>
|
||
|
||
<div className="p-6 overflow-y-auto h-[550px] min-h-[550px] max-h-[550px] bg-gray-50/30">
|
||
<form id="productForm" onSubmit={handleSave} className="space-y-6">
|
||
|
||
{/* General Tab */}
|
||
<div className={activeTab === 'general' ? 'block space-y-4' : 'hidden'}>
|
||
<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 flex items-center gap-1.5">
|
||
<span>کد محصول (Art No) *</span>
|
||
<div className="group relative inline-block">
|
||
<HelpCircle className="w-3.5 h-3.5 text-gray-400 hover:text-purple-600 cursor-help" />
|
||
<div className="absolute top-full right-1/2 translate-x-1/2 mt-2 hidden group-hover:block w-52 p-2.5 bg-gray-900 text-white text-[10px] rounded-lg shadow-xl text-center z-50 font-vazir leading-relaxed">
|
||
کد کاتالوگ انحصاری کارخانه Canina آلمان (مانند 123490).
|
||
</div>
|
||
</div>
|
||
</label>
|
||
<input required type="text" value={formData.artNo} onChange={(e) => setFormData({...formData, artNo: e.target.value})} className="w-full px-4 py-3 rounded-xl border border-gray-200 focus:border-purple-500 outline-none font-mono" dir="ltr" />
|
||
</div>
|
||
<div className="space-y-2">
|
||
<label className="text-sm font-bold text-gray-700 flex items-center gap-1.5">
|
||
<span>نام فارسی محصول *</span>
|
||
<div className="group relative inline-block">
|
||
<HelpCircle className="w-3.5 h-3.5 text-gray-400 hover:text-purple-600 cursor-help" />
|
||
<div className="absolute top-full right-1/2 translate-x-1/2 mt-2 hidden group-hover:block w-52 p-2.5 bg-gray-900 text-white text-[10px] rounded-lg shadow-xl text-center z-50 font-vazir leading-relaxed">
|
||
عنوان فارسی محصول که در صفحات و کاتالوگ فروشگاه نمایش داده میشود.
|
||
</div>
|
||
</div>
|
||
</label>
|
||
<input required type="text" value={formData.nameFa} onChange={(e) => setFormData({...formData, nameFa: e.target.value})} className="w-full px-4 py-3 rounded-xl border border-gray-200 focus:border-purple-500 outline-none" dir="rtl" placeholder="مثال: ژل دندان میکروسیلور" />
|
||
</div>
|
||
<div className="space-y-2">
|
||
<label className="text-sm font-bold text-gray-700 flex items-center gap-1.5">
|
||
<span>نام انگلیسی / آلمانی (nameEn) *</span>
|
||
<div className="group relative inline-block">
|
||
<HelpCircle className="w-3.5 h-3.5 text-gray-400 hover:text-purple-600 cursor-help" />
|
||
<div className="absolute top-full right-1/2 translate-x-1/2 mt-2 hidden group-hover:block w-52 p-2.5 bg-gray-900 text-white text-[10px] rounded-lg shadow-xl text-center z-50 font-vazir leading-relaxed">
|
||
نام برند و وزن کالا به انگلیسی جهت استفاده در آدرس، فاکتورها و سئو.
|
||
</div>
|
||
</div>
|
||
</label>
|
||
<input required type="text" value={formData.nameEn} onChange={(e) => setFormData({...formData, nameEn: e.target.value})} className="w-full px-4 py-3 rounded-xl border border-gray-200 focus:border-purple-500 outline-none font-mono" dir="ltr" placeholder="e.g. Canina Mikrosilber Zahngel - 50ml" />
|
||
</div>
|
||
<div className="space-y-2 md:col-span-2">
|
||
<label className="text-sm font-bold text-gray-700 flex items-center gap-1.5">
|
||
<span>شعار علمی (Tagline)</span>
|
||
<div className="group relative inline-block">
|
||
<HelpCircle className="w-3.5 h-3.5 text-gray-400 hover:text-purple-600 cursor-help" />
|
||
<div className="absolute top-full right-1/2 translate-x-1/2 mt-2 hidden group-hover:block w-52 p-2.5 bg-gray-900 text-white text-[10px] rounded-lg shadow-xl text-center z-50 font-vazir leading-relaxed">
|
||
شعار کلیدی یا علمی کالا که در بالای صفحه محصول نمایش داده میشود.
|
||
</div>
|
||
</div>
|
||
</label>
|
||
<input type="text" value={formData.scientificTagline} onChange={(e) => setFormData({...formData, scientificTagline: 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 flex items-center gap-1.5">
|
||
<span>دستهبندی *</span>
|
||
<div className="group relative inline-block">
|
||
<HelpCircle className="w-3.5 h-3.5 text-gray-400 hover:text-purple-600 cursor-help" />
|
||
<div className="absolute bottom-full right-1/2 translate-x-1/2 mb-2 hidden group-hover:block w-52 p-2.5 bg-gray-900 text-white text-[10px] rounded-lg shadow-xl text-center z-30 font-vazir leading-relaxed">
|
||
دستهبندی اصلی محصول جهت تقسیمبندی در منوها و سیستم ناوبری سایت.
|
||
</div>
|
||
</div>
|
||
</label>
|
||
<select required value={formData.categoryId} onChange={(e) => setFormData({...formData, categoryId: e.target.value})} className="w-full px-4 py-3 rounded-xl border border-gray-200 focus:border-purple-500 outline-none bg-white">
|
||
<option value="">انتخاب کنید</option>
|
||
{categories.map(c => <option key={c.id} value={c.id}>{c.name}</option>)}
|
||
</select>
|
||
</div>
|
||
<div className="space-y-2">
|
||
<label className="text-sm font-bold text-gray-700 flex items-center gap-1.5">
|
||
<span>مناسب برای</span>
|
||
<div className="group relative inline-block">
|
||
<HelpCircle className="w-3.5 h-3.5 text-gray-400 hover:text-purple-600 cursor-help" />
|
||
<div className="absolute bottom-full right-1/2 translate-x-1/2 mb-2 hidden group-hover:block w-52 p-2.5 bg-gray-900 text-white text-[10px] rounded-lg shadow-xl text-center z-30 font-vazir leading-relaxed">
|
||
تعیین این که کالا برای سگ، گربه یا هر دو گونه مناسب است.
|
||
</div>
|
||
</div>
|
||
</label>
|
||
<select value={formData.suitableFor} onChange={(e) => setFormData({...formData, suitableFor: e.target.value})} className="w-full px-4 py-3 rounded-xl border border-gray-200 focus:border-purple-500 outline-none bg-white">
|
||
<option value="سگ">سگ</option>
|
||
<option value="گربه">گربه</option>
|
||
<option value="سگ و گربه">سگ و گربه</option>
|
||
</select>
|
||
</div>
|
||
<div className="space-y-2 md:col-span-2">
|
||
<label className="text-sm font-bold text-gray-700 flex items-center gap-1.5">
|
||
<span>توضیح کوتاه</span>
|
||
<div className="group relative inline-block">
|
||
<HelpCircle className="w-3.5 h-3.5 text-gray-400 hover:text-purple-600 cursor-help" />
|
||
<div className="absolute bottom-full right-1/2 translate-x-1/2 mb-2 hidden group-hover:block w-52 p-2.5 bg-gray-900 text-white text-[10px] rounded-lg shadow-xl text-center z-30 font-vazir leading-relaxed">
|
||
خلاصه ویژگیهای کلیدی کالا که در کارتهای فروشگاه و زیر عنوان محصول نمایش داده میشود.
|
||
</div>
|
||
</div>
|
||
</label>
|
||
<textarea rows={2} value={formData.shortDescription} onChange={(e) => setFormData({...formData, shortDescription: e.target.value})} className="w-full px-4 py-3 rounded-xl border border-gray-200 focus:border-purple-500 outline-none"></textarea>
|
||
</div>
|
||
<div className="space-y-2 md:col-span-2">
|
||
<label className="text-sm font-bold text-gray-700 flex items-center gap-1.5">
|
||
<span>توضیحات کامل</span>
|
||
<div className="group relative inline-block">
|
||
<HelpCircle className="w-3.5 h-3.5 text-gray-400 hover:text-purple-600 cursor-help" />
|
||
<div className="absolute bottom-full right-1/2 translate-x-1/2 mb-2 hidden group-hover:block w-52 p-2.5 bg-gray-900 text-white text-[10px] rounded-lg shadow-xl text-center z-30 font-vazir leading-relaxed">
|
||
توضیحات کامل علمی، نحوه عملکرد مکمل و جزئیات ترکیبات محصول.
|
||
</div>
|
||
</div>
|
||
</label>
|
||
<textarea rows={5} value={formData.description} onChange={(e) => setFormData({...formData, description: e.target.value})} className="w-full px-4 py-3 rounded-xl border border-gray-200 focus:border-purple-500 outline-none"></textarea>
|
||
</div>
|
||
<div className="space-y-2 md:col-span-2">
|
||
<label className="text-sm font-bold text-gray-700 flex items-center gap-1.5">
|
||
<span>منطق دوز مصرفی (Dosage Logic)</span>
|
||
<div className="group relative inline-block">
|
||
<HelpCircle className="w-3.5 h-3.5 text-gray-400 hover:text-purple-600 cursor-help" />
|
||
<div className="absolute bottom-full right-1/2 translate-x-1/2 mb-2 hidden group-hover:block w-52 p-2.5 bg-gray-900 text-white text-[10px] rounded-lg shadow-xl text-center z-30 font-vazir leading-relaxed">
|
||
فرمول نحوه مصرف روزانه کالا بر اساس وزن پت (مثال: ۲ میلیلیتر به ازای هر ۵ کیلوگرم وزن).
|
||
</div>
|
||
</div>
|
||
</label>
|
||
<textarea rows={2} value={formData.dosageLogic} onChange={(e) => setFormData({...formData, dosageLogic: e.target.value})} className="w-full px-4 py-3 rounded-xl border border-gray-200 focus:border-purple-500 outline-none"></textarea>
|
||
</div>
|
||
<div className="space-y-2 md:col-span-2">
|
||
<label className="text-sm font-bold text-gray-700 flex items-center gap-1.5">
|
||
<span>علائم درمانی مرتبط (Symptom Tags)</span>
|
||
<div className="group relative inline-block">
|
||
<HelpCircle className="w-3.5 h-3.5 text-gray-400 hover:text-purple-600 cursor-help" />
|
||
<div className="absolute bottom-full right-1/2 translate-x-1/2 mb-2 hidden group-hover:block w-52 p-2.5 bg-gray-900 text-white text-[10px] rounded-lg shadow-xl text-center z-30 font-vazir leading-relaxed">
|
||
علائم متصل به این دارو جهت فیلترینگ و سرچ پویای کاربران (مثال: اسهال، خارش، ریزش مو).
|
||
</div>
|
||
</div>
|
||
</label>
|
||
<div className="flex flex-wrap gap-2 p-3 bg-white border border-gray-200 rounded-xl min-h-[50px] items-center focus-within:border-purple-500 focus-within:ring-2 focus-within:ring-purple-200 transition-all">
|
||
{formData.symptoms && formData.symptoms.map((symptom, idx) => (
|
||
<span key={idx} className="inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-xs font-bold bg-purple-50 border border-purple-200 text-purple-700">
|
||
{symptom}
|
||
<button
|
||
type="button"
|
||
onClick={() => {
|
||
const newSymptoms = formData.symptoms.filter((_, i) => i !== idx);
|
||
setFormData({ ...formData, symptoms: newSymptoms });
|
||
}}
|
||
className="text-purple-400 hover:text-purple-600 focus:outline-none transition-colors"
|
||
>
|
||
<X className="w-3.5 h-3.5" />
|
||
</button>
|
||
</span>
|
||
))}
|
||
<input
|
||
type="text"
|
||
placeholder="تایپ و اینتر کنید..."
|
||
className="flex-1 bg-transparent border-none outline-none text-sm min-w-[150px] text-right font-vazir"
|
||
onKeyDown={(e) => {
|
||
if (e.key === 'Enter') {
|
||
e.preventDefault();
|
||
const input = e.currentTarget;
|
||
const value = input.value.trim();
|
||
if (value && !formData.symptoms.includes(value)) {
|
||
setFormData({
|
||
...formData,
|
||
symptoms: [...(formData.symptoms || []), value]
|
||
});
|
||
input.value = '';
|
||
}
|
||
}
|
||
}}
|
||
/>
|
||
</div>
|
||
<p className="text-[11px] text-gray-400 font-medium">پس از نوشتن هر علامت درمانی (مثلاً «درد مفاصل»)، کلید Enter را فشار دهید.</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Pricing Tab */}
|
||
<div className={activeTab === 'pricing' ? 'block space-y-4' : 'hidden'}>
|
||
<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="number"
|
||
min="0"
|
||
value={formData.priceValue}
|
||
onChange={(e) => {
|
||
const val = Number(e.target.value);
|
||
setFormData({
|
||
...formData,
|
||
priceValue: val,
|
||
priceDisplay: `${val.toLocaleString('fa-IR')} تومان`,
|
||
});
|
||
}}
|
||
className="w-full px-4 py-3 rounded-xl border border-gray-200 focus:border-purple-500 outline-none"
|
||
dir="ltr"
|
||
/>
|
||
</div>
|
||
<div className="space-y-2">
|
||
<label className="text-sm font-bold text-purple-700 flex items-center gap-1">
|
||
<span>قیمت اختصاصی B2B (تومان)</span>
|
||
<span className="text-[10px] font-normal text-gray-400">(اختیاری)</span>
|
||
</label>
|
||
<input type="number" min="0" placeholder="در صورت عدم ورود، درصد کلی اعمال میشود" value={formData.wholesalePrice} onChange={(e) => setFormData({...formData, wholesalePrice: e.target.value ? Number(e.target.value) : ''})} className="w-full px-4 py-3 rounded-xl border border-purple-200 focus:border-purple-600 outline-none font-bold bg-purple-50/20" dir="ltr" />
|
||
</div>
|
||
<div className="space-y-2">
|
||
<label className="text-sm font-bold text-gray-700">موجودی *</label>
|
||
<input required type="number" min="0" value={formData.packageSize} onChange={(e) => setFormData({...formData, packageSize: Number(e.target.value)})} className="w-full px-4 py-3 rounded-xl border border-gray-200 focus:border-purple-500 outline-none" dir="ltr" />
|
||
</div>
|
||
<div className="space-y-2">
|
||
<label className="text-sm font-bold text-gray-700">واحد (Unit) *</label>
|
||
<input required type="text" placeholder="عدد، گرم، میلیلیتر" value={formData.unit} onChange={(e) => setFormData({...formData, unit: 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>
|
||
|
||
|
||
{/* SEO Tab */}
|
||
<div className={activeTab === 'seo' ? 'block' : 'hidden'}>
|
||
{(() => {
|
||
const derivedSlug = formData.slug || slugify(formData.nameEn);
|
||
const derivedMetaTitle = formData.metaTitle || `${formData.nameFa || 'نام محصول'} | خرید و قیمت مکمل کانینا`;
|
||
const derivedMetaDescription = formData.metaDescription || (formData.shortDescription || 'خرید آنلاین مکمل اصل کانینا آلمان با بالاترین کیفیت بالینی و ضمانت اصالت فیزیکی...');
|
||
const derivedCanonicalUrl = formData.canonicalUrl || `/shop/${derivedSlug}`;
|
||
|
||
return (
|
||
<div className="grid grid-cols-1 lg:grid-cols-12 gap-6 items-start">
|
||
{/* Right Column: SEO Inputs */}
|
||
<div className="lg:col-span-7 space-y-4">
|
||
<div className="space-y-2">
|
||
<label className="text-sm font-bold text-gray-700 flex items-center gap-1.5">
|
||
<span>نامک (Slug) محصول</span>
|
||
<div className="group relative inline-block">
|
||
<HelpCircle className="w-3.5 h-3.5 text-gray-400 hover:text-purple-600 cursor-help" />
|
||
<div className="absolute top-full right-1/2 translate-x-1/2 mt-2 hidden group-hover:block w-52 p-2.5 bg-gray-900 text-white text-[10px] rounded-lg shadow-xl text-center z-50 font-vazir leading-relaxed">
|
||
آدرس مستقیم محصول در سایت. در صورت خالی بودن، به طور خودکار از روی نام انگلیسی ساخته میشود.
|
||
</div>
|
||
</div>
|
||
</label>
|
||
<input
|
||
type="text"
|
||
value={formData.slug}
|
||
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"
|
||
dir="ltr"
|
||
placeholder={derivedSlug}
|
||
/>
|
||
</div>
|
||
|
||
<div className="space-y-2">
|
||
<label className="text-sm font-bold text-gray-700 flex items-center gap-1.5">
|
||
<span>عنوان متا (Meta Title)</span>
|
||
<div className="group relative inline-block">
|
||
<HelpCircle className="w-3.5 h-3.5 text-gray-400 hover:text-purple-600 cursor-help" />
|
||
<div className="absolute bottom-full right-1/2 translate-x-1/2 mb-2 hidden group-hover:block w-52 p-2.5 bg-gray-900 text-white text-[10px] rounded-lg shadow-xl text-center z-30 font-vazir leading-relaxed">
|
||
عنوان اصلی صفحه در گوگل (حداکثر ۶۰ کاراکتر). در صورت خالی بودن، از ساختار پیشفرض استفاده میشود.
|
||
</div>
|
||
</div>
|
||
</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"
|
||
placeholder={derivedMetaTitle}
|
||
/>
|
||
</div>
|
||
|
||
<div className="space-y-2">
|
||
<label className="text-sm font-bold text-gray-700 flex items-center gap-1.5">
|
||
<span>توضیحات متا (Meta Description)</span>
|
||
<div className="group relative inline-block">
|
||
<HelpCircle className="w-3.5 h-3.5 text-gray-400 hover:text-purple-600 cursor-help" />
|
||
<div className="absolute bottom-full right-1/2 translate-x-1/2 mb-2 hidden group-hover:block w-52 p-2.5 bg-gray-900 text-white text-[10px] rounded-lg shadow-xl text-center z-30 font-vazir leading-relaxed">
|
||
خلاصه توضیحات صفحه در نتایج جستجو (حداکثر ۱۵۵ کاراکتر). در صورت خالی بودن، از توضیحات کوتاه محصول استفاده میشود.
|
||
</div>
|
||
</div>
|
||
</label>
|
||
<textarea
|
||
rows={3}
|
||
value={formData.metaDescription}
|
||
onChange={(e) => setFormData({...formData, metaDescription: e.target.value})}
|
||
className="w-full px-4 py-3 rounded-xl border border-gray-200 focus:border-purple-500 outline-none"
|
||
placeholder={derivedMetaDescription}
|
||
></textarea>
|
||
</div>
|
||
|
||
<div className="space-y-2">
|
||
<label className="text-sm font-bold text-gray-700 flex items-center gap-1.5">
|
||
<span>کلمات کلیدی سئو (Keywords)</span>
|
||
<div className="group relative inline-block">
|
||
<HelpCircle className="w-3.5 h-3.5 text-gray-400 hover:text-purple-600 cursor-help" />
|
||
<div className="absolute bottom-full right-1/2 translate-x-1/2 mb-2 hidden group-hover:block w-52 p-2.5 bg-gray-900 text-white text-[10px] rounded-lg shadow-xl text-center z-30 font-vazir leading-relaxed">
|
||
کلمات کلیدی مرتبط با محصول که با کاما (,) از هم جدا شدهاند.
|
||
</div>
|
||
</div>
|
||
</label>
|
||
<input
|
||
type="text"
|
||
value={formData.keywords}
|
||
onChange={(e) => setFormData({...formData, keywords: e.target.value})}
|
||
className="w-full px-4 py-3 rounded-xl border border-gray-200 focus:border-purple-500 outline-none"
|
||
placeholder="مثال: مکمل سگ, تقویت مفاصل, کنهیدروکس"
|
||
/>
|
||
</div>
|
||
|
||
<div className="space-y-2">
|
||
<label className="text-sm font-bold text-gray-700 flex items-center gap-1.5">
|
||
<span>آدرس کانونی (Canonical URL)</span>
|
||
<div className="group relative inline-block">
|
||
<HelpCircle className="w-3.5 h-3.5 text-gray-400 hover:text-purple-600 cursor-help" />
|
||
<div className="absolute bottom-full right-1/2 translate-x-1/2 mb-2 hidden group-hover:block w-52 p-2.5 bg-gray-900 text-white text-[10px] rounded-lg shadow-xl text-center z-30 font-vazir leading-relaxed">
|
||
آدرس اصلی محصول جهت جلوگیری از جریمه محتوای تکراری در گوگل.
|
||
</div>
|
||
</div>
|
||
</label>
|
||
<input
|
||
type="text"
|
||
value={formData.canonicalUrl}
|
||
onChange={(e) => setFormData({...formData, canonicalUrl: 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"
|
||
dir="ltr"
|
||
placeholder={derivedCanonicalUrl}
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Left Column: Sticky Google SERP Preview */}
|
||
<div className="lg:col-span-5 lg:sticky lg:top-4 bg-gray-100 border border-gray-200 rounded-2xl p-5 space-y-3">
|
||
<span className="text-[10px] font-black text-gray-400 uppercase tracking-widest block">پیشنمایش نتایج در گوگل (Google SERP Preview)</span>
|
||
|
||
<div className="bg-white rounded-xl p-4 border border-gray-100 shadow-sm text-right font-sans" dir="rtl">
|
||
{/* Domain / Breadcrumb */}
|
||
<div className="flex items-center gap-1 text-xs text-[#202124] mb-1">
|
||
<span className="font-medium text-gray-800">caninoiran.ir</span>
|
||
<span className="text-gray-400">›</span>
|
||
<span className="text-gray-500">shop</span>
|
||
<span className="text-gray-400">›</span>
|
||
<span className="text-gray-500 max-w-[150px] truncate">{derivedSlug}</span>
|
||
</div>
|
||
|
||
{/* Title */}
|
||
<h4 className="text-[19px] text-[#1a0dab] hover:underline cursor-pointer leading-tight font-medium font-sans mb-1 select-none">
|
||
{derivedMetaTitle}
|
||
</h4>
|
||
|
||
{/* Description */}
|
||
<p className="text-sm text-[#4d5156] leading-relaxed font-sans select-none break-words">
|
||
{derivedMetaDescription}
|
||
</p>
|
||
</div>
|
||
<p className="text-[10px] text-gray-400 font-medium">تغییرات فیلدهای سمت راست، بلافاصله در پیشنمایش بالا منعکس میشود.</p>
|
||
</div>
|
||
</div>
|
||
);
|
||
})()}
|
||
</div>
|
||
|
||
{/* Media Tab */}
|
||
<div className={activeTab === 'media' ? 'block space-y-6' : 'hidden'}>
|
||
{/* Direct Image URL Input Box */}
|
||
<div className="bg-white p-5 rounded-2xl border border-gray-200 shadow-sm space-y-3">
|
||
<label className="text-sm font-bold text-gray-700 flex items-center justify-between">
|
||
<span className="flex items-center gap-2">
|
||
<ImageIcon className="w-4 h-4 text-purple-600" />
|
||
لینک مستقیم آدرس تصویر (External / Direct URL)
|
||
</span>
|
||
<span className="text-[11px] text-gray-400 font-normal">امکان چسباندن (Ctrl+V) مستقیم آدرس اینترنتی تصویر</span>
|
||
</label>
|
||
<div className="flex gap-2">
|
||
<input
|
||
type="text"
|
||
value={formData.imageUrl}
|
||
onChange={(e) => {
|
||
setFormData({...formData, imageUrl: e.target.value});
|
||
setMediaImageError(false);
|
||
}}
|
||
placeholder="https://example.com/images/product.png یا /uploads/photo.jpg"
|
||
className="flex-1 px-4 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 outline-none font-mono text-sm"
|
||
dir="ltr"
|
||
/>
|
||
<button
|
||
type="button"
|
||
onClick={() => setIsMediaSelectorOpen(true)}
|
||
className="bg-purple-100 text-purple-700 hover:bg-purple-200 px-4 py-2.5 rounded-xl text-xs font-bold transition-colors whitespace-nowrap"
|
||
>
|
||
گالری سرور
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Preview Container */}
|
||
<div className="flex flex-col items-center justify-center p-8 border-2 border-dashed border-gray-200 rounded-2xl bg-white">
|
||
{formData.imageUrl && !mediaImageError ? (
|
||
<div className="w-full flex flex-col items-center">
|
||
<div className="relative group">
|
||
<img
|
||
src={getProductImageUrl(formData.imageUrl)}
|
||
alt="پیشنمایش تصویر محصول"
|
||
onError={() => setMediaImageError(true)}
|
||
className="max-w-xs h-48 object-contain rounded-xl shadow-md border border-gray-100"
|
||
/>
|
||
<div className="absolute inset-0 bg-black/50 opacity-0 group-hover:opacity-100 transition-opacity rounded-xl flex items-center justify-center gap-3">
|
||
<button type="button" onClick={() => setIsMediaSelectorOpen(true)} className="bg-white text-gray-800 px-4 py-2 rounded-lg font-bold text-sm">انتخاب از گالری</button>
|
||
<button type="button" onClick={() => setFormData({...formData, imageUrl: ''})} className="bg-red-500 text-white px-4 py-2 rounded-lg font-bold text-sm hover:bg-red-600">حذف عکس</button>
|
||
</div>
|
||
</div>
|
||
<div className="mt-4 text-center" dir="ltr">
|
||
<p className="text-xs text-gray-500 font-mono bg-gray-100 px-4 py-2 rounded-lg inline-block max-w-md truncate">
|
||
{formData.imageUrl}
|
||
</p>
|
||
</div>
|
||
<p className="text-[11px] text-gray-400 mt-2">تصویر با موفقیت بارگذاری و تایید شد.</p>
|
||
</div>
|
||
) : (
|
||
<>
|
||
<div className="w-16 h-16 bg-gray-100 rounded-full flex items-center justify-center text-gray-400 mb-4">
|
||
<ImageIcon className="w-8 h-8 text-purple-400" />
|
||
</div>
|
||
<p className="text-gray-600 font-bold mb-2">آدرس یا فایلی برای تصویر انتخاب نشده است</p>
|
||
<p className="text-xs text-gray-400 mb-4 text-center leading-relaxed">
|
||
میتوانید لینک اینترنتی تصویر را در کادر بالا وارد کنید، عکس جدید آپلود نمایید<br />
|
||
یا از کلیدهای <kbd className="px-1.5 py-0.5 bg-gray-100 rounded text-gray-700 font-mono">Ctrl+V</kbd> برای چسباندن مستقیم فایل یا لینک استفاده کنید.
|
||
</p>
|
||
<button type="button" onClick={() => setIsMediaSelectorOpen(true)} className="bg-purple-600 text-white hover:bg-purple-700 px-6 py-2.5 rounded-xl font-bold transition-colors shadow-sm text-sm">
|
||
انتخاب یا آپلود در گالری
|
||
</button>
|
||
</>
|
||
)}
|
||
</div>
|
||
|
||
{/* Multimedia Attachments (TASK-2.3 & TASK-2.4) */}
|
||
<div className="bg-white p-5 rounded-2xl border border-gray-200 shadow-sm space-y-4">
|
||
<h4 className="font-bold text-gray-800 text-sm border-b pb-2">پیوستهای مالتیمدیا و مستندات علمی</h4>
|
||
|
||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||
<div className="space-y-1.5">
|
||
<label className="text-xs font-bold text-gray-700 block">لینک فصلی پادکست (Podcast URL)</label>
|
||
<input
|
||
type="text"
|
||
value={formData.podcastUrl}
|
||
onChange={(e) => setFormData({ ...formData, podcastUrl: e.target.value })}
|
||
placeholder="https://... MP3 or Soundcloud"
|
||
className="w-full px-3 py-2 border rounded-xl text-xs font-mono"
|
||
dir="ltr"
|
||
/>
|
||
</div>
|
||
<div className="space-y-1.5">
|
||
<label className="text-xs font-bold text-gray-700 block">لینک یا کدفریم ویدیو (Video URL)</label>
|
||
<input
|
||
type="text"
|
||
value={formData.videoUrl}
|
||
onChange={(e) => setFormData({ ...formData, videoUrl: e.target.value })}
|
||
placeholder="https://... MP4 or Aparat/YouTube iframe"
|
||
className="w-full px-3 py-2 border rounded-xl text-xs font-mono"
|
||
dir="ltr"
|
||
/>
|
||
</div>
|
||
<div className="space-y-1.5">
|
||
<label className="text-xs font-bold text-gray-700 block">فایل کاتالوگ / PDF علمی (PDF URL)</label>
|
||
<input
|
||
type="text"
|
||
value={formData.pdfUrl}
|
||
onChange={(e) => setFormData({ ...formData, pdfUrl: e.target.value })}
|
||
placeholder="https://... PDF catalog"
|
||
className="w-full px-3 py-2 border rounded-xl text-xs font-mono"
|
||
dir="ltr"
|
||
/>
|
||
</div>
|
||
</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="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>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Media Selector Modal */}
|
||
<MediaSelector
|
||
isOpen={isMediaSelectorOpen}
|
||
onClose={() => setIsMediaSelectorOpen(false)}
|
||
onSelect={(url) => {
|
||
setFormData({...formData, imageUrl: url});
|
||
setMediaImageError(false);
|
||
}}
|
||
selectedUrl={formData.imageUrl.startsWith('http') ? formData.imageUrl : formData.imageUrl ? `${BASE_DOMAIN}${formData.imageUrl}` : undefined}
|
||
/>
|
||
|
||
<ConfirmModal
|
||
isOpen={!!deleteTargetId}
|
||
title="حذف محصول"
|
||
message="آیا از حذف این محصول مطمئن هستید؟ این عملیات قابل بازگشت نیست."
|
||
onConfirm={confirmDelete}
|
||
onCancel={() => setDeleteTargetId(null)}
|
||
/>
|
||
</div>
|
||
);
|
||
}
|