- 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
422 lines
21 KiB
TypeScript
422 lines
21 KiB
TypeScript
"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>
|
||
);
|
||
}
|