canina/frontend/application/components/ArchivePage.tsx
parsa aghaei 7615aa0e51 fix: resolve seed encoding, search param, and wiki data source issues
- Fix seed-products.ts TS error (implicit any) and BOM handling
- Re-run full seed to restore correct Persian encoding in DB
- Fix productService query→search param mismatch
- Fix IngredientWiki hardcoded port 4000→use settingsStore
- Restore seed-products-data.json from git after accidental corruption
2026-07-11 15:40:49 +03:30

422 lines
21 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client";
import React, { useState, useMemo, useEffect } from "react";
import { Product, PetType } from "../lib/data/products";
import { usePetStore } from "../lib/store/usePetStore";
import { productService } from "../lib/services/productService";
import { motion, AnimatePresence } from "motion/react";
import { ProductCardSkeleton } from "./Skeleton";
import { toast } from "sonner";
import {
Search,
Filter,
Dog,
Cat,
ShieldCheck,
ChevronLeft,
ChevronRight,
Activity,
Sparkles,
Stethoscope,
HeartPulse,
Heart,
AlertCircle,
Plus,
Loader2
} from "lucide-react";
import { useCartStore } from "../lib/store/cartStore";
import SafeImage from "./SafeImage";
const CATEGORIES = [
{ id: "all", label: "همه محصولات", icon: <Activity className="w-4 h-4" /> },
{ id: "joints", label: "حرکتی و مفاصل", icon: <HeartPulse className="w-4 h-4" /> },
{ id: "immune", label: "تقویت و ایمنی", icon: <ShieldCheck className="w-4 h-4" /> },
{ id: "energy", label: "ویتامین و انرژی", icon: <Activity className="w-4 h-4" /> },
{ id: "special-care", label: "پلتفرم مراقبت ویژه", icon: <Stethoscope className="w-4 h-4" /> },
];
const CATEGORY_MAP: Record<string, { category?: string; query?: string; symptoms?: string[] }> = {
// Main Categories
"joints": { category: "joints" },
"immune": { category: "immune" },
"energy": { category: "energy" },
"special-care": { category: "special-care" },
// Persian Fallbacks (for robust matching)
"مفاصل و استخوان": { category: "joints" },
"تقویت سیستم ایمنی و گوارش": { category: "immune" },
"ویتامین‌ها و انرژی‌بخش‌ها": { category: "energy" },
"مراقبت‌های ویژه (پوست، دندان و چشم)": { category: "special-care" },
// Solutions
"آرتروز سگ‌های پیر": { category: "joints", symptoms: ["درد مفاصل", "سختی در بلند شدن", "لنگیدن"] },
"رشد استخوانی توله‌سگ": { category: "joints", symptoms: ["رشد سریع توله‌سگ"] },
"تقویت رباط و تاندون": { category: "joints", query: "تاندون" },
"پیشگیری از بیماری": { category: "immune", symptoms: ["ضعف بعد از بیماری"] },
"رفع اسهال و یبوست": { category: "immune", symptoms: ["اسهال", "یبوست", "اسهال مزمن"] },
"پروبیوتیک‌ها": { category: "immune", query: "فیبر" },
"مکمل‌های مولتی‌ویتامین": { category: "energy", query: "ویتامین" },
"افزایش اشتها": { category: "energy", symptoms: ["بی‌اشتهایی"] },
"رشد و بلوغ": { category: "energy", query: "انرژی" },
"سلامت پوست و مفاصل": { category: "special-care", symptoms: ["خشکی پوست", "ریزش مو", "خارش"] },
"رفع جرم دندان": { category: "special-care", symptoms: ["جرم دندان", "بوی بد دهان", "التهاب لثه"] },
"شستشوی چشم": { category: "special-care", query: "چشم" },
};
const SYMPTOMS = [
"لنگیدن", "سختی در بلند شدن", "ریزش مو", "جرم دندان", "بی‌اشتهایی", "اسهال مزمن", "بعد از مصرف آنتی‌بیوتیک"
];
const ArchiveProductCard: React.FC<{ product: Product, onClick: (p: Product) => void }> = ({ product, onClick }) => {
const { addItem } = useCartStore();
const [isAdding, setIsAdding] = useState(false);
const activePet = usePetStore(state => state.pets.find(p => p.id === state.activePetId) || null);
const handleAddToCart = (e: React.MouseEvent) => {
e.stopPropagation();
setIsAdding(true);
addItem(product, 1);
toast.success(`${product.name} به سبد خرید اضافه شد`);
setTimeout(() => setIsAdding(false), 800);
};
const compatibility = useMemo(() => {
if (!activePet) return null;
const sameSpecies = product.suitableFor === activePet.type || product.suitableFor === "هر دو";
const helpsSymptom = (activePet.medicalConditions || []).some(s => product.symptoms.includes(s));
if (!sameSpecies) return { type: 'alert', text: `مخصوص ${product.suitableFor}` };
if (helpsSymptom) return { type: 'success', text: `توصیه شده برای راهکار ${activePet.name}` };
if (sameSpecies) return { type: 'neutral', text: `مناسب برای ${activePet.name}` };
return null;
}, [product, activePet]);
return (
<motion.div
layout
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
onClick={() => onClick(product)}
className="group bg-white rounded-[2.5rem] border border-medical-gray-200 overflow-hidden hover:shadow-2xl transition-all cursor-pointer flex flex-col h-full relative"
>
{product.specialBadge && (
<div className="absolute top-4 right-4 z-10 bg-canina-blue text-white text-[9px] font-black uppercase tracking-widest px-3 py-1 rounded-full shadow-lg">
{product.specialBadge}
</div>
)}
{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 ${
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 === '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}
</div>
)}
<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" />
<div className="absolute bottom-4 left-4 flex gap-2">
{(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">
<Dog className="w-4 h-4" />
</div>
)}
{(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">
<Cat className="w-4 h-4" />
</div>
)}
</div>
</div>
<div className="p-6 flex flex-col flex-1">
<div className="text-[10px] font-bold text-canina-blue uppercase tracking-widest mb-2">{product.category}</div>
<h3 className="text-lg font-black text-medical-gray-900 group-hover:text-canina-blue transition-colors mb-3 leading-tight">{product.name}</h3>
<div className="flex flex-wrap gap-1 mb-6">
{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">
{s}
</span>
))}
</div>
<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>
<button
onClick={handleAddToCart}
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"
>
{isAdding ? <Loader2 className="w-4 h-4 animate-spin" /> : <Plus className="w-4 h-4" />}
</button>
</div>
</div>
</motion.div>
);
}
import { useRouter } from 'next/navigation';
export default function ArchivePage({
initialCategory = "all",
initialSearch = ""
}: {
initialCategory?: string,
initialSearch?: string
}) {
const router = useRouter();
const [selectedCategory, setSelectedCategory] = useState(initialCategory);
const [selectedPet, setSelectedPet] = useState<PetType | "all">("all");
const [activeSymptoms, setActiveSymptoms] = useState<string[]>([]);
const [searchQuery, setSearchQuery] = useState(initialSearch);
const [isUpdating, setIsUpdating] = useState(false);
const [filteredProducts, setFilteredProducts] = useState<Product[]>([]);
// Debug Log for Search Params
useEffect(() => {
console.log("[ArchivePage] URL Params Sync:", { initialCategory, initialSearch });
}, [initialCategory, initialSearch]);
// Sync state with prop change (e.g. from Header menu)
useEffect(() => {
setIsUpdating(true);
// Normalize Input (Trim and lowercase)
const normSearch = (initialSearch || "").trim().toLowerCase();
const normCat = initialCategory.trim().toLowerCase();
const mapKey = normSearch || normCat;
const mapping = CATEGORY_MAP[mapKey];
console.log("[ArchivePage] Processing Mapping for:", mapKey);
// Reset ALL other filters when a new category/solution is selected from menu
setSelectedPet("all");
setActiveSymptoms([]);
setSearchQuery("");
if (mapping) {
if (mapping.category) setSelectedCategory(mapping.category);
if (mapping.query) setSearchQuery(mapping.query);
if (mapping.symptoms) {
setActiveSymptoms(mapping.symptoms);
}
} else {
setSelectedCategory(initialCategory);
setSearchQuery(initialSearch);
if (initialSearch && SYMPTOMS.includes(initialSearch)) {
setActiveSymptoms([initialSearch]);
}
}
const timer = setTimeout(() => {
window.scrollTo({ top: 0, behavior: "smooth" });
}, 400);
return () => clearTimeout(timer);
}, [initialCategory, initialSearch]);
useEffect(() => {
const fetchProducts = async () => {
setIsUpdating(true);
try {
const data = await productService.getProducts({
category: selectedCategory,
petType: selectedPet,
query: searchQuery
});
let result = data;
if (activeSymptoms.length > 0) {
result = result.filter(p => p.symptoms.some(s => activeSymptoms.includes(s)));
}
setFilteredProducts(result);
} catch (error) {
console.error("Failed to fetch products:", error);
toast.error("خطا در دریافت لیست محصولات از سرور. لطفاً صفحه را رفرش کنید.");
} finally {
setIsUpdating(false);
}
};
fetchProducts();
}, [selectedCategory, selectedPet, searchQuery, activeSymptoms]);
const toggleSymptom = (s: string) => {
setActiveSymptoms(prev => prev.includes(s) ? prev.filter(item => item !== s) : [...prev, s]);
};
return (
<div className="min-h-screen bg-medical-gray-50 pb-20 px-4 pt-10 font-vazir" dir="rtl">
<div className="max-w-7xl mx-auto">
{/* Mobile Navigation & Breadcrumbs */}
<div className="lg:hidden mb-10 space-y-4">
<button
onClick={() => router.back()}
className="flex items-center gap-2 text-medical-gray-500 font-bold hover:text-canina-blue transition-colors"
>
<ChevronRight className="w-5 h-5 font-vazir" />
<span>بازگشت</span>
</button>
<div className="flex items-center gap-2 text-[10px] font-black text-medical-gray-400 uppercase tracking-widest overflow-x-auto whitespace-nowrap pb-2">
<span className="cursor-pointer hover:text-canina-blue" onClick={() => router.push('/')}>کانینا</span>
<ChevronLeft className="w-2.5 h-2.5" />
<span className="text-canina-blue">فروشگاه تخصصی</span>
</div>
</div>
{/* Desktop Breadcrumbs */}
<div className="hidden lg: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>
<ChevronLeft className="w-2.5 h-2.5" />
<span className="text-canina-blue">کاتالوگ کامل محصولات</span>
</div>
<div className="flex flex-col lg:flex-row gap-10">
{/* Sidebar Filters */}
<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="flex items-center gap-2 mb-6">
<Filter className="w-5 h-5 text-canina-blue" />
<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>
{/* 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 Checklist */}
<div>
<span className="text-[10px] font-black text-medical-gray-400 uppercase tracking-widest block mb-3">جستجو بر اساس علائم</span>
<div className="flex flex-wrap gap-2">
{SYMPTOMS.map(s => (
<button
key={s}
onClick={() => toggleSymptom(s)}
className={`px-3 py-1.5 rounded-lg text-[10px] font-bold transition-all border ${activeSymptoms.includes(s) ? 'bg-medical-gray-900 border-medical-gray-900 text-white' : 'bg-medical-gray-50 border-medical-gray-100 text-medical-gray-500 hover:border-medical-gray-300'}`}
>
{s}
</button>
))}
</div>
</div>
</div>
</div>
{/* Promo Card */}
<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" />
<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>
<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>
</aside>
{/* Main Content */}
<main className="flex-1 relative">
<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>
<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" />
<input
type="text"
placeholder="جستجوی محصول..."
value={searchQuery}
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"
/>
</div>
</div>
<AnimatePresence mode="wait">
{isUpdating ? (
<div key="skeleton" className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-6">
{Array.from({ length: 8 }).map((_, i) => (
<ProductCardSkeleton key={i} />
))}
</div>
) : filteredProducts.length > 0 ? (
<div key="grid" className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-6">
<AnimatePresence mode="popLayout">
{filteredProducts.map((p: Product) => (
<ArchiveProductCard key={p.id} product={p} onClick={(p) => router.push(`/shop/${p.slug || p.id}`)} />
))}
</AnimatePresence>
</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>
)}
</AnimatePresence>
</main>
</div>
</div>
</div>
);
}