- Add filename label and upload date under each thumbnail in MediaSelector - Highlight currently selected image with purple border and badge - Add selectedUrl prop to MediaSelector for visual indication - Show image filename in product form Media tab - Add helper text for clipboard paste and gallery selection
443 lines
25 KiB
TypeScript
443 lines
25 KiB
TypeScript
import { useState, useEffect } from 'react';
|
||
import { Search, Filter, Box, Plus, Edit2, Trash2, Image as ImageIcon, X } from 'lucide-react';
|
||
import { toast } from 'react-hot-toast';
|
||
import api from '../services/api';
|
||
import Spinner from '../components/ui/Spinner';
|
||
import Pagination from '../components/ui/Pagination';
|
||
import MediaSelector from '../components/ui/MediaSelector';
|
||
|
||
export default function Products() {
|
||
const [products, setProducts] = useState<any[]>([]);
|
||
const [categories, setCategories] = useState<any[]>([]);
|
||
const [isLoading, setIsLoading] = useState(true);
|
||
|
||
// Filters
|
||
const [search, setSearch] = useState('');
|
||
const [categoryFilter, setCategoryFilter] = useState('');
|
||
const [page, setPage] = useState(1);
|
||
const [totalPages, setTotalPages] = useState(1);
|
||
const limit = 10;
|
||
|
||
// Modal State
|
||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||
const [isMediaSelectorOpen, setIsMediaSelectorOpen] = useState(false);
|
||
const [editingProduct, setEditingProduct] = useState<any>(null);
|
||
const [activeTab, setActiveTab] = useState('general');
|
||
|
||
const [formData, setFormData] = useState({
|
||
artNo: '',
|
||
name: '',
|
||
scientificTagline: '',
|
||
description: '',
|
||
shortDescription: '',
|
||
categoryId: '',
|
||
priceValue: 0,
|
||
priceDisplay: '',
|
||
unit: '',
|
||
packageSize: 0,
|
||
dosageLogic: '',
|
||
suitableFor: 'سگ و گربه',
|
||
imageUrl: '',
|
||
metaTitle: '',
|
||
metaDescription: '',
|
||
keywords: '',
|
||
canonicalUrl: '',
|
||
slug: ''
|
||
});
|
||
|
||
const fetchData = async () => {
|
||
try {
|
||
setIsLoading(true);
|
||
// 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: any) {
|
||
console.error(err);
|
||
toast.error('خطا در دریافت لیست محصولات: ' + (err.response?.data?.message || err.message));
|
||
} finally {
|
||
setIsLoading(false);
|
||
}
|
||
};
|
||
|
||
useEffect(() => {
|
||
const timer = setTimeout(() => {
|
||
fetchData();
|
||
}, 500);
|
||
return () => clearTimeout(timer);
|
||
}, [search, categoryFilter, page]);
|
||
|
||
const openModal = (product: any = null) => {
|
||
if (product) {
|
||
setEditingProduct(product);
|
||
setFormData({
|
||
artNo: product.artNo,
|
||
name: product.name,
|
||
scientificTagline: product.scientificTagline || '',
|
||
description: product.description || '',
|
||
shortDescription: product.shortDescription || '',
|
||
categoryId: product.categoryId || '',
|
||
priceValue: Number(product.priceValue),
|
||
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 || ''
|
||
});
|
||
} else {
|
||
setEditingProduct(null);
|
||
setFormData({
|
||
artNo: '', name: '', scientificTagline: '', description: '', shortDescription: '', categoryId: '',
|
||
priceValue: 0, priceDisplay: '', unit: '', packageSize: 0, dosageLogic: '', suitableFor: 'سگ و گربه',
|
||
imageUrl: '', metaTitle: '', metaDescription: '', keywords: '', canonicalUrl: '', slug: ''
|
||
});
|
||
}
|
||
setActiveTab('general');
|
||
setIsModalOpen(true);
|
||
};
|
||
|
||
const handleSave = async (e: React.FormEvent) => {
|
||
e.preventDefault();
|
||
try {
|
||
if (editingProduct) {
|
||
await api.put(`/admin/products/${editingProduct.id}`, formData);
|
||
} else {
|
||
await api.post('/admin/products', formData);
|
||
}
|
||
setIsModalOpen(false);
|
||
fetchData();
|
||
} catch (error) {
|
||
console.error('Save failed', error);
|
||
alert('خطا در ذخیره محصول');
|
||
}
|
||
};
|
||
|
||
const handleDelete = async (id: string) => {
|
||
if (!window.confirm('آیا از حذف این محصول مطمئن هستید؟')) return;
|
||
try {
|
||
await api.delete(`/admin/products/${id}`);
|
||
fetchData();
|
||
} catch (error) {
|
||
console.error('Delete failed', error);
|
||
}
|
||
};
|
||
|
||
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 ? (
|
||
<img src={product.imageUrl.startsWith('http') ? product.imageUrl : `http://localhost:4001${product.imageUrl}`} alt={product.name} className="w-12 h-12 rounded-xl object-cover border border-gray-200" />
|
||
) : (
|
||
<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 font-bold text-gray-900">{product.name}</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={() => handleDelete(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">
|
||
<h3 className="text-xl font-bold text-gray-900">
|
||
{editingProduct ? 'ویرایش پیشرفته محصول' : 'افزودن محصول جدید'}
|
||
</h3>
|
||
<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 flex-1 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">کد محصول (Art No) *</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">نام محصول *</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" />
|
||
</div>
|
||
<div className="space-y-2 md:col-span-2">
|
||
<label className="text-sm font-bold text-gray-700">شعار علمی (Tagline)</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">دستهبندی *</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">مناسب برای</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">توضیح کوتاه</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">توضیحات کامل</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">منطق دوز مصرفی (Dosage Logic)</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>
|
||
</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) => setFormData({...formData, priceValue: 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">نمایش قیمت (متنی) *</label>
|
||
<input required type="text" placeholder="مثال: ۸۵۰,۰۰۰ تومان" value={formData.priceDisplay} onChange={(e) => setFormData({...formData, priceDisplay: 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">موجودی *</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 space-y-4' : 'hidden'}>
|
||
<div className="grid grid-cols-1 gap-4">
|
||
<div className="space-y-2">
|
||
<label className="text-sm font-bold text-gray-700">نامک (Slug) محصول</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="مثال: canhydrox-gag" />
|
||
</div>
|
||
<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" />
|
||
</div>
|
||
<div className="space-y-2">
|
||
<label className="text-sm font-bold text-gray-700">توضیحات متا (Meta Description)</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"></textarea>
|
||
</div>
|
||
<div className="space-y-2">
|
||
<label className="text-sm font-bold text-gray-700">کلمات کلیدی (با کاما جدا کنید)</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" />
|
||
</div>
|
||
<div className="space-y-2">
|
||
<label className="text-sm font-bold text-gray-700">آدرس کانونی (Canonical URL)</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" />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Media Tab */}
|
||
<div className={activeTab === 'media' ? 'block space-y-4' : 'hidden'}>
|
||
<div className="flex flex-col items-center justify-center p-8 border-2 border-dashed border-gray-200 rounded-2xl bg-white">
|
||
{formData.imageUrl ? (
|
||
<div className="w-full flex flex-col items-center">
|
||
<div className="relative group">
|
||
<img src={formData.imageUrl.startsWith('http') ? formData.imageUrl : `http://localhost:4001${formData.imageUrl}`} alt="product" className="max-w-xs h-48 object-cover rounded-xl shadow-md" />
|
||
<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>
|
||
</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">
|
||
{formData.imageUrl.split('/').pop()}
|
||
</p>
|
||
</div>
|
||
<p className="text-[11px] text-gray-400 mt-2">برای تغییر عکس، روی تصویر کلیک کنید یا دکمه زیر را بزنید</p>
|
||
<button type="button" onClick={() => setIsMediaSelectorOpen(true)} className="mt-3 bg-purple-100 text-purple-700 hover:bg-purple-200 px-6 py-2 rounded-xl font-bold transition-colors text-sm">
|
||
انتخاب از گالری رسانه
|
||
</button>
|
||
</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" />
|
||
</div>
|
||
<p className="text-gray-500 font-medium mb-4">هیچ تصویری انتخاب نشده است</p>
|
||
<p className="text-xs text-gray-400 mb-4">از گالری رسانه تصویر انتخاب کنید یا با Ctrl+V از کلیپبورد بچسبانید</p>
|
||
<button type="button" onClick={() => setIsMediaSelectorOpen(true)} className="bg-purple-100 text-purple-700 hover:bg-purple-200 px-6 py-2 rounded-xl font-bold transition-colors">
|
||
انتخاب از گالری رسانه
|
||
</button>
|
||
</>
|
||
)}
|
||
</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})}
|
||
selectedUrl={formData.imageUrl.startsWith('http') ? formData.imageUrl : formData.imageUrl ? `http://localhost:4001${formData.imageUrl}` : undefined}
|
||
/>
|
||
</div>
|
||
);
|
||
}
|