fix(database): fix baseSlug format in products seed to populate product symptoms successfully + format cleanup in ArchivePage

This commit is contained in:
parsa aghaei 2026-07-15 17:46:27 +03:30
parent 6dafe86344
commit 717a61a225
2 changed files with 223 additions and 225 deletions

View File

@ -139,10 +139,6 @@ async function main() {
const cleanIngredients = item.ingredients || ''; const cleanIngredients = item.ingredients || '';
const cleanDosage = item.usage_and_dosage || ''; const cleanDosage = item.usage_and_dosage || '';
const suitableFor = determineSuitableFor(baseName, cleanDesc); const suitableFor = determineSuitableFor(baseName, cleanDesc);
const baseSlug = slugify(baseName);
const categorySlug = item.category_slug || 'supplements';
const category = await findOrCreateCategory(categorySlug);
// Split baseName into nameFa and nameEn // Split baseName into nameFa and nameEn
let baseNameFa = baseName; let baseNameFa = baseName;
let baseNameEn = baseName; let baseNameEn = baseName;
@ -152,6 +148,10 @@ async function main() {
baseNameFa = matchName[2].trim(); baseNameFa = matchName[2].trim();
} }
const baseSlug = slugify(baseNameEn);
const categorySlug = item.category_slug || 'supplements';
const category = await findOrCreateCategory(categorySlug);
for (const variant of item.variants) { for (const variant of item.variants) {
const artNo = variant.art_no; const artNo = variant.art_no;
const barcode = variant.barcode; const barcode = variant.barcode;

View File

@ -107,10 +107,9 @@ const ArchiveProductCard: React.FC<{ product: Product, onClick: (p: Product) =>
)} )}
{compatibility && ( {compatibility && (
<div className={`absolute top-14 right-4 z-10 text-[8px] font-black uppercase tracking-widest px-3 py-1 rounded-full shadow-md flex items-center gap-1 ${ <div className={`absolute top-14 right-4 z-10 text-[8px] font-black uppercase tracking-widest px-3 py-1 rounded-full shadow-md flex items-center gap-1 ${compatibility.type === 'alert' ? 'bg-amber-100 text-amber-700' :
compatibility.type === 'alert' ? 'bg-amber-100 text-amber-700' : compatibility.type === 'success' ? 'bg-green-100 text-green-700' : 'bg-medical-gray-100 text-medical-gray-600'
compatibility.type === 'success' ? 'bg-green-100 text-green-700' : 'bg-medical-gray-100 text-medical-gray-600' }`}>
}`}>
{compatibility.type === 'alert' ? <AlertCircle className="w-2.5 h-2.5" /> : compatibility.type === 'success' ? <Heart className="w-2.5 h-2.5" /> : <Sparkles className="w-2.5 h-2.5" />} {compatibility.type === 'alert' ? <AlertCircle className="w-2.5 h-2.5" /> : compatibility.type === 'success' ? <Heart className="w-2.5 h-2.5" /> : <Sparkles className="w-2.5 h-2.5" />}
{compatibility.text} {compatibility.text}
</div> </div>
@ -119,16 +118,16 @@ const ArchiveProductCard: React.FC<{ product: Product, onClick: (p: Product) =>
<div className="aspect-square bg-medical-gray-50 flex items-center justify-center p-8 overflow-hidden relative"> <div className="aspect-square bg-medical-gray-50 flex items-center justify-center p-8 overflow-hidden relative">
<SafeImage src={product.image} alt={product.name} className="w-full h-full group-hover:scale-110 transition-transform duration-700" imgClassName="object-contain" /> <SafeImage src={product.image} alt={product.name} className="w-full h-full group-hover:scale-110 transition-transform duration-700" imgClassName="object-contain" />
<div className="absolute bottom-4 left-4 flex gap-2"> <div className="absolute bottom-4 left-4 flex gap-2">
{(product.suitableFor === "سگ" || product.suitableFor === "هر دو") && ( {(product.suitableFor === "سگ" || product.suitableFor === "هر دو") && (
<div className="w-7 h-7 bg-white rounded-lg flex items-center justify-center text-medical-gray-400 border border-medical-gray-100 shadow-sm"> <div className="w-7 h-7 bg-white rounded-lg flex items-center justify-center text-medical-gray-400 border border-medical-gray-100 shadow-sm">
<Dog className="w-4 h-4" /> <Dog className="w-4 h-4" />
</div> </div>
)} )}
{(product.suitableFor === "گربه" || product.suitableFor === "هر دو") && ( {(product.suitableFor === "گربه" || product.suitableFor === "هر دو") && (
<div className="w-7 h-7 bg-white rounded-lg flex items-center justify-center text-medical-gray-400 border border-medical-gray-100 shadow-sm"> <div className="w-7 h-7 bg-white rounded-lg flex items-center justify-center text-medical-gray-400 border border-medical-gray-100 shadow-sm">
<Cat className="w-4 h-4" /> <Cat className="w-4 h-4" />
</div> </div>
)} )}
</div> </div>
</div> </div>
@ -145,23 +144,23 @@ const ArchiveProductCard: React.FC<{ product: Product, onClick: (p: Product) =>
)} )}
</div> </div>
<div className="flex flex-wrap gap-1 mb-6"> <div className="flex flex-wrap gap-1">
{product.symptoms.slice(0, 2).map((s, i) => ( {product.symptoms.slice(0, 2).map((s, i) => (
<span key={i} className="text-[9px] font-bold bg-medical-gray-100 text-medical-gray-500 px-2 py-0.5 rounded-md"> <span key={i} className="text-[9px] font-bold bg-medical-gray-100 text-medical-gray-500 px-2 py-0.5 rounded-md">
{s} {s}
</span> </span>
))} ))}
</div> </div>
<div className="mt-auto flex items-center justify-between pt-4 border-t border-medical-gray-50"> <div className="mt-auto flex items-center justify-between pt-4 border-t border-medical-gray-50">
<span className="font-black text-medical-gray-900 font-vazir">{product.price}</span> <span className="font-black text-medical-gray-900 font-vazir">{product.price}</span>
<button <button
onClick={handleAddToCart} onClick={handleAddToCart}
disabled={isAdding} disabled={isAdding}
className="w-10 h-10 rounded-xl bg-medical-gray-50 text-canina-blue flex items-center justify-center group-hover:bg-canina-blue group-hover:text-white transition-all disabled:opacity-50" className="w-10 h-10 rounded-xl bg-medical-gray-50 text-canina-blue flex items-center justify-center group-hover:bg-canina-blue group-hover:text-white transition-all disabled:opacity-50"
> >
{isAdding ? <Loader2 className="w-4 h-4 animate-spin" /> : <Plus className="w-4 h-4" />} {isAdding ? <Loader2 className="w-4 h-4 animate-spin" /> : <Plus className="w-4 h-4" />}
</button> </button>
</div> </div>
</div> </div>
</motion.div> </motion.div>
@ -279,34 +278,34 @@ export default function ArchivePage({
}, [selectedCategory, selectedPet, searchQuery, activeSymptoms]); }, [selectedCategory, selectedPet, searchQuery, activeSymptoms]);
const fetchProducts = async (resetPage = true) => { const fetchProducts = async (resetPage = true) => {
setIsUpdating(true); setIsUpdating(true);
try { try {
const res = await productService.getProducts({ const res = await productService.getProducts({
category: selectedCategory, category: selectedCategory,
petType: selectedPet, petType: selectedPet,
query: searchQuery, query: searchQuery,
// Send the first active symptom to the backend for server-side filtering // Send the first active symptom to the backend for server-side filtering
symptom: activeSymptoms.length === 1 ? activeSymptoms[0] : undefined, symptom: activeSymptoms.length === 1 ? activeSymptoms[0] : undefined,
page: resetPage ? 1 : page, page: resetPage ? 1 : page,
limit: 20, limit: 20,
}); });
let result = res.data; let result = res.data;
// If multiple symptoms selected, do client-side intersection (rare case) // If multiple symptoms selected, do client-side intersection (rare case)
if (activeSymptoms.length > 1) { if (activeSymptoms.length > 1) {
result = result.filter(p => activeSymptoms.every(s => p.symptoms.includes(s))); result = result.filter(p => activeSymptoms.every(s => p.symptoms.includes(s)));
}
setFilteredProducts(result);
setMeta(res.meta);
if (resetPage) setPage(1);
} catch (error) {
console.error("Failed to fetch products:", error);
toast.error("خطا در دریافت لیست محصولات از سرور. لطفاً صفحه را رفرش کنید.");
} finally {
setIsUpdating(false);
} }
};
setFilteredProducts(result);
setMeta(res.meta);
if (resetPage) setPage(1);
} catch (error) {
console.error("Failed to fetch products:", error);
toast.error("خطا در دریافت لیست محصولات از سرور. لطفاً صفحه را رفرش کنید.");
} finally {
setIsUpdating(false);
}
};
useEffect(() => { useEffect(() => {
fetchProducts(true); fetchProducts(true);
@ -361,174 +360,173 @@ export default function ArchivePage({
</div> </div>
{/* Desktop Breadcrumbs */} {/* Desktop Breadcrumbs */}
<div className="flex items-center gap-2 text-[10px] font-black text-medical-gray-400 uppercase tracking-widest mb-10"> <div className="flex items-center gap-2 text-[10px] font-black text-medical-gray-400 uppercase tracking-widest mb-10">
<span className="cursor-pointer hover:text-canina-blue" onClick={() => router.push('/')}>خانه</span> <span className="cursor-pointer hover:text-canina-blue" onClick={() => router.push('/')}>خانه</span>
<ChevronLeft className="w-2.5 h-2.5" /> <ChevronLeft className="w-2.5 h-2.5" />
<span className="cursor-pointer hover:text-canina-blue" onClick={() => { setSelectedCategory("all"); }}>کاتالوگ</span> <span className="cursor-pointer hover:text-canina-blue" onClick={() => { setSelectedCategory("all"); }}>کاتالوگ</span>
{selectedCategory !== "all" && ( {selectedCategory !== "all" && (
<> <>
<ChevronLeft className="w-2.5 h-2.5" /> <ChevronLeft className="w-2.5 h-2.5" />
<span className="text-canina-blue">{CATEGORY_LABELS[selectedCategory] || selectedCategory}</span> <span className="text-canina-blue">{CATEGORY_LABELS[selectedCategory] || selectedCategory}</span>
</> </>
)} )}
</div> </div>
<div className="flex flex-col lg:flex-row gap-10"> <div className="flex flex-col lg:flex-row gap-10">
{/* Sidebar Filters */} {/* Sidebar Filters */}
<aside className="lg:w-72 flex-shrink-0 space-y-8"> <aside className="lg:w-72 flex-shrink-0 space-y-8">
<div className="bg-white rounded-[2rem] p-6 border border-medical-gray-200 shadow-sm"> <div className="bg-white rounded-[2rem] p-6 border border-medical-gray-200 shadow-sm">
<div className="flex items-center gap-2 mb-6"> <div className="flex items-center gap-2 mb-6">
<Filter className="w-5 h-5 text-canina-blue" /> <Filter className="w-5 h-5 text-canina-blue" />
<h3 className="font-black text-medical-gray-900">فیلترهای تخصصی</h3> <h3 className="font-black text-medical-gray-900">فیلترهای تخصصی</h3>
</div>
<div className="space-y-6">
{/* Pet Type */}
<div>
<span className="text-[10px] font-black text-medical-gray-400 uppercase tracking-widest block mb-3">نوع پت</span>
<div className="grid grid-cols-3 gap-2">
{[
{ id: "all", label: "هر دو", icon: <Activity className="w-3 h-3" /> },
{ id: "سگ", label: "سگ", icon: <Dog className="w-3 h-3" /> },
{ id: "گربه", label: "گربه", icon: <Cat className="w-3 h-3" /> },
].map(pet => (
<button
key={pet.id}
onClick={() => setSelectedPet(pet.id as any)}
className={`py-2 rounded-xl text-[10px] font-black border transition-all flex flex-col items-center gap-1 ${selectedPet === pet.id ? 'bg-canina-blue border-canina-blue text-white shadow-lg shadow-canina-blue/20' : 'bg-white border-medical-gray-200 text-medical-gray-400'}`}
>
{pet.icon}
{pet.label}
</button>
))}
</div>
</div> </div>
<div className="space-y-6"> {/* Solutions */}
{/* Pet Type */} <div>
<div> <span className="text-[10px] font-black text-medical-gray-400 uppercase tracking-widest block mb-3">راهکار درمانی</span>
<span className="text-[10px] font-black text-medical-gray-400 uppercase tracking-widest block mb-3">نوع پت</span> <div className="flex flex-col gap-2">
<div className="grid grid-cols-3 gap-2"> {categories.map(cat => (
{[ <button
{ id: "all", label: "هر دو", icon: <Activity className="w-3 h-3" /> }, key={cat.id}
{ id: "سگ", label: "سگ", icon: <Dog className="w-3 h-3" /> }, onClick={() => setSelectedCategory(cat.id)}
{ id: "گربه", label: "گربه", icon: <Cat className="w-3 h-3" /> }, className={`flex items-center gap-3 px-4 py-3 rounded-2xl text-xs font-bold transition-all border ${selectedCategory === cat.id ? 'bg-canina-blue border-canina-blue text-white' : 'bg-white border-medical-gray-100 text-medical-gray-600 hover:border-canina-blue/30'}`}
].map(pet => ( >
<button {cat.icon}
key={pet.id} {cat.label}
onClick={() => setSelectedPet(pet.id as any)} </button>
className={`py-2 rounded-xl text-[10px] font-black border transition-all flex flex-col items-center gap-1 ${selectedPet === pet.id ? 'bg-canina-blue border-canina-blue text-white shadow-lg shadow-canina-blue/20' : 'bg-white border-medical-gray-200 text-medical-gray-400'}`} ))}
> </div>
{pet.icon}
{pet.label}
</button>
))}
</div>
</div>
{/* Solutions */}
<div>
<span className="text-[10px] font-black text-medical-gray-400 uppercase tracking-widest block mb-3">راهکار درمانی</span>
<div className="flex flex-col gap-2">
{categories.map(cat => (
<button
key={cat.id}
onClick={() => setSelectedCategory(cat.id)}
className={`flex items-center gap-3 px-4 py-3 rounded-2xl text-xs font-bold transition-all border ${selectedCategory === cat.id ? 'bg-canina-blue border-canina-blue text-white' : 'bg-white border-medical-gray-100 text-medical-gray-600 hover:border-canina-blue/30'}`}
>
{cat.icon}
{cat.label}
</button>
))}
</div>
</div>
{/* Symptoms Pill Tags */}
<div>
<div className="flex items-center justify-between mb-3">
<span className="text-[10px] font-black text-medical-gray-400 uppercase tracking-widest">جستجو بر اساس علائم</span>
{activeSymptoms.length > 0 && (
<button
onClick={() => setActiveSymptoms([])}
className="text-[10px] font-bold text-canina-blue hover:underline"
>
پاک کردن
</button>
)}
</div>
<div className="flex flex-wrap gap-1.5 max-h-52 overflow-y-auto scrollbar-thin scrollbar-thumb-medical-gray-200 scrollbar-track-transparent pr-0.5">
{symptoms.length === 0 ? (
<span className="text-[10px] text-medical-gray-400">در حال بارگذاری...</span>
) : symptoms.map(s => (
<button
key={s}
onClick={() => toggleSymptom(s)}
className={`px-2.5 py-1 rounded-full text-[10px] font-bold transition-all border whitespace-nowrap ${
activeSymptoms.includes(s)
? 'bg-canina-blue border-canina-blue text-white shadow-sm shadow-canina-blue/30'
: 'bg-medical-gray-50 border-medical-gray-200 text-medical-gray-500 hover:border-canina-blue/40 hover:text-canina-blue'
}`}
>
{s}
</button>
))}
</div>
</div>
</div> </div>
{/* Symptoms Pill Tags */}
<div>
<div className="flex items-center justify-between mb-3">
<span className="text-[10px] font-black text-medical-gray-400 uppercase tracking-widest">جستجو بر اساس علائم</span>
{activeSymptoms.length > 0 && (
<button
onClick={() => setActiveSymptoms([])}
className="text-[10px] font-bold text-canina-blue hover:underline"
>
پاک کردن
</button>
)}
</div>
<div className="flex flex-wrap gap-1.5 max-h-52 overflow-y-auto scrollbar-thin scrollbar-thumb-medical-gray-200 scrollbar-track-transparent pr-0.5">
{symptoms.length === 0 ? (
<span className="text-[10px] text-medical-gray-400">در حال بارگذاری...</span>
) : symptoms.map(s => (
<button
key={s}
onClick={() => toggleSymptom(s)}
className={`px-2.5 py-1 rounded-full text-[10px] font-bold transition-all border whitespace-nowrap ${activeSymptoms.includes(s)
? 'bg-canina-blue border-canina-blue text-white shadow-sm shadow-canina-blue/30'
: 'bg-medical-gray-50 border-medical-gray-200 text-medical-gray-500 hover:border-canina-blue/40 hover:text-canina-blue'
}`}
>
{s}
</button>
))}
</div>
</div>
</div>
</div> </div>
{/* Promo Card */} {/* Promo Card */}
<div className="bg-medical-gray-900 rounded-[2rem] p-6 text-white relative overflow-hidden group"> <div className="bg-medical-gray-900 rounded-[2rem] p-6 text-white relative overflow-hidden group">
<div className="absolute -top-10 -left-10 w-32 h-32 bg-canina-blue rounded-full blur-3xl opacity-20 group-hover:opacity-40 transition-opacity" /> <div className="absolute -top-10 -left-10 w-32 h-32 bg-canina-blue rounded-full blur-3xl opacity-20 group-hover:opacity-40 transition-opacity" />
<h4 className="text-lg font-black italic mb-4 relative z-10 underline decoration-canina-blue underline-offset-8">مشاوره رایگان</h4> <h4 className="text-lg font-black italic mb-4 relative z-10 underline decoration-canina-blue underline-offset-8">مشاوره رایگان</h4>
<p className="text-xs text-white/60 leading-relaxed mb-6 relative z-10">اگر نمیدانید کدام ترکیب برای پت شما مناسب است، همین حالا با دامپزشکان ما تماس بگیرید.</p> <p className="text-xs text-white/60 leading-relaxed mb-6 relative z-10">اگر نمیدانید کدام ترکیب برای پت شما مناسب است، همین حالا با دامپزشکان ما تماس بگیرید.</p>
<a href="tel:0218888" className="w-full bg-white text-medical-gray-900 py-3 rounded-xl text-xs font-black hover:scale-105 transition-transform relative z-10 flex items-center justify-center">شماره تماس: ۰۲۱-۸۸۸۸</a> <a href="tel:0218888" className="w-full bg-white text-medical-gray-900 py-3 rounded-xl text-xs font-black hover:scale-105 transition-transform relative z-10 flex items-center justify-center">شماره تماس: ۰۲۱-۸۸۸۸</a>
</div> </div>
</aside> </aside>
{/* Main Content */} {/* Main Content */}
<main className="flex-1 relative"> <main className="flex-1 relative">
<div className="flex flex-col md:flex-row items-center justify-between mb-8 gap-4"> <div className="flex flex-col md:flex-row items-center justify-between mb-8 gap-4">
<h2 className="text-3xl font-black text-medical-gray-900">کاتولوگ دارویی <span className="text-canina-blue">Canina</span></h2> <h2 className="text-3xl font-black text-medical-gray-900">کاتولوگ دارویی <span className="text-canina-blue">Canina</span></h2>
<div className="relative w-full md:w-80"> <div className="relative w-full md:w-80">
<Search className="absolute right-4 top-1/2 -translate-y-1/2 w-4 h-4 text-medical-gray-400" /> <Search className="absolute right-4 top-1/2 -translate-y-1/2 w-4 h-4 text-medical-gray-400" />
<input <input
type="text" type="text"
placeholder="جستجوی محصول..." placeholder="جستجوی محصول..."
value={searchQuery} value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)} onChange={(e) => setSearchQuery(e.target.value)}
className="w-full bg-white border border-medical-gray-200 rounded-full py-3 pr-11 pl-4 text-sm focus:outline-none focus:ring-2 focus:ring-canina-blue/20" className="w-full bg-white border border-medical-gray-200 rounded-full py-3 pr-11 pl-4 text-sm focus:outline-none focus:ring-2 focus:ring-canina-blue/20"
/> />
</div> </div>
</div> </div>
<AnimatePresence mode="wait"> <AnimatePresence mode="wait">
{isUpdating ? ( {isUpdating ? (
<div key="skeleton" className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-6"> <div key="skeleton" className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-6">
{Array.from({ length: 8 }).map((_, i) => ( {Array.from({ length: 8 }).map((_, i) => (
<ProductCardSkeleton key={i} /> <ProductCardSkeleton key={i} />
))} ))}
</div> </div>
) : filteredProducts.length > 0 ? ( ) : filteredProducts.length > 0 ? (
<> <>
<div key="grid" className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-6"> <div key="grid" className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-6">
<AnimatePresence mode="popLayout"> <AnimatePresence mode="popLayout">
{filteredProducts.map((p: Product) => ( {filteredProducts.map((p: Product) => (
<ArchiveProductCard key={p.id} product={p} onClick={(p) => router.push(`/shop/${p.slug || p.id}`)} /> <ArchiveProductCard key={p.id} product={p} onClick={(p) => router.push(`/shop/${p.slug || p.id}`)} />
))} ))}
</AnimatePresence> </AnimatePresence>
</div>
{page < meta.lastPage && (
<div className="flex justify-center mt-10">
<button
onClick={loadMore}
className="bg-canina-blue text-white px-8 py-3 rounded-xl font-black text-sm hover:scale-105 transition-transform shadow-lg shadow-canina-blue/20"
>
بارگذاری محصولات بیشتر
</button>
</div> </div>
)} {page < meta.lastPage && (
</> <div className="flex justify-center mt-10">
) : ( <button
<div key="empty" className="bg-white rounded-[3rem] p-20 text-center border-2 border-dashed border-medical-gray-200 flex flex-col items-center"> onClick={loadMore}
<div className="w-20 h-20 bg-medical-gray-50 rounded-full flex items-center justify-center text-medical-gray-300 mb-6">
<Search className="w-10 h-10" />
</div>
<h3 className="text-2xl font-black text-medical-gray-900 mb-2">محصولی یافت نشد!</h3>
<p className="text-medical-gray-400 max-w-sm mb-8 font-bold">با فیلترهای فعلی محصولی مطابق با نیاز شما پیدا نکردیم. لطفاً فیلترها را گستردهتر کنید یا دکمه ریست را بزنید.</p>
<button
onClick={() => {
router.push('/shop');
setSelectedCategory("all");
setSelectedPet("all");
setActiveSymptoms([]);
setSearchQuery("");
}}
className="bg-canina-blue text-white px-8 py-3 rounded-xl font-black text-sm hover:scale-105 transition-transform shadow-lg shadow-canina-blue/20" className="bg-canina-blue text-white px-8 py-3 rounded-xl font-black text-sm hover:scale-105 transition-transform shadow-lg shadow-canina-blue/20"
> >
پاک کردن همه فیلترها و نمایش همه بارگذاری محصولات بیشتر
</button> </button>
</div>
)}
</>
) : (
<div key="empty" className="bg-white rounded-[3rem] p-20 text-center border-2 border-dashed border-medical-gray-200 flex flex-col items-center">
<div className="w-20 h-20 bg-medical-gray-50 rounded-full flex items-center justify-center text-medical-gray-300 mb-6">
<Search className="w-10 h-10" />
</div>
<h3 className="text-2xl font-black text-medical-gray-900 mb-2">محصولی یافت نشد!</h3>
<p className="text-medical-gray-400 max-w-sm mb-8 font-bold">با فیلترهای فعلی محصولی مطابق با نیاز شما پیدا نکردیم. لطفاً فیلترها را گستردهتر کنید یا دکمه ریست را بزنید.</p>
<button
onClick={() => {
router.push('/shop');
setSelectedCategory("all");
setSelectedPet("all");
setActiveSymptoms([]);
setSearchQuery("");
}}
className="bg-canina-blue text-white px-8 py-3 rounded-xl font-black text-sm hover:scale-105 transition-transform shadow-lg shadow-canina-blue/20"
>
پاک کردن همه فیلترها و نمایش همه
</button>
</div> </div>
)} )}
</AnimatePresence> </AnimatePresence>
</main> </main>