canina/frontend/application/components/ArchivePage.tsx

569 lines
26 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 { toPersian } from "../lib/utils";
import { Product, PetType } from "../lib/data/products";
import { usePetStore } from "../lib/store/usePetStore";
import { productService } from "../lib/services/productService";
import Link from "next/link";
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 ICON_MAP: Record<string, React.ReactNode> = {
joints: <HeartPulse className="w-4 h-4" />,
immune: <ShieldCheck className="w-4 h-4" />,
energy: <Activity className="w-4 h-4" />,
"special-care": <Stethoscope className="w-4 h-4" />,
general: <Heart className="w-4 h-4" />,
nutrition: <Sparkles 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 ArchiveProductCard: React.FC<{ product: Product }> = ({ product }) => {
const { addItem } = useCartStore();
const [isAdding, setIsAdding] = useState(false);
const activePet = usePetStore(state => state.pets.find(p => p.id === state.activePetId) || null);
const productUrl = `/shop/${product.slug || product.id}`;
const handleAddToCart = (e: React.MouseEvent) => {
e.preventDefault();
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 matchedSymptom = (activePet.medicalConditions || []).find(mc =>
(product.symptoms || []).some(s => mc.includes(s) || s.includes(mc))
);
if (!sameSpecies) return { type: 'alert', text: `مخصوص ${product.suitableFor}` };
if (matchedSymptom) return { type: 'success', text: `توصیه شده برای ${matchedSymptom} ${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 }}
className="h-full"
>
<Link
href={productUrl}
className="group bg-white rounded-[2.5rem] border border-medical-gray-200 overflow-hidden hover:shadow-2xl transition-all 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>
<div className="flex flex-col gap-1 mb-3">
<h3 dir="rtl" className="text-base font-black text-medical-gray-900 group-hover:text-canina-blue transition-colors leading-tight text-right font-vazir">
{product.nameFa || product.name}
</h3>
{product.nameEn && (
<span dir="ltr" className="text-[11px] font-bold text-slate-400 font-sans text-left block">
{product.nameEn}
</span>
)}
</div>
<div className="flex flex-wrap gap-1">
{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 gap-2">
<span className="font-black text-medical-gray-900 font-vazir text-sm sm:text-base">{product.price}</span>
{(() => {
const cartItem = useCartStore.getState().items.find(i => i.product.id === product.id);
if (cartItem && cartItem.quantity > 0) {
return (
<div
onClick={(e) => { e.preventDefault(); e.stopPropagation(); }}
className="flex items-center gap-1 bg-canina-blue/10 border border-canina-blue/20 rounded-xl p-1"
>
<button
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
useCartStore.getState().updateQuantity(product.id, cartItem.quantity + 1);
}}
className="w-7 h-7 rounded-lg bg-canina-blue text-white flex items-center justify-center font-bold text-xs"
>
+
</button>
<span className="w-5 text-center font-black text-canina-blue text-xs font-vazir">
{toPersian(cartItem.quantity)}
</span>
<button
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
useCartStore.getState().updateQuantity(product.id, cartItem.quantity - 1);
}}
className="w-7 h-7 rounded-lg bg-white text-medical-gray-700 flex items-center justify-center font-bold text-xs shadow-sm"
>
-
</button>
</div>
);
}
return (
<button
onClick={handleAddToCart}
disabled={isAdding}
className="px-3 py-2 rounded-xl bg-canina-blue text-white text-xs font-black flex items-center gap-1.5 hover:bg-canina-dark transition-all disabled:opacity-50 shadow-md shadow-canina-blue/20"
>
{isAdding ? <Loader2 className="w-3.5 h-3.5 animate-spin" /> : <Plus className="w-3.5 h-3.5" />}
<span>خرید</span>
</button>
);
})()}
</div>
</div>
</Link>
</motion.div>
);
};
import { useRouter } from 'next/navigation';
export default function ArchivePage({
initialCategory = "all",
initialSearch = "",
initialPetType = "all",
initialSymptoms = ""
}: {
initialCategory?: string,
initialSearch?: string,
initialPetType?: string,
initialSymptoms?: string
}) {
const router = useRouter();
const [selectedCategory, setSelectedCategory] = useState(initialCategory);
const [selectedPet, setSelectedPet] = useState<PetType | "all">(initialPetType as any);
const [activeSymptoms, setActiveSymptoms] = useState<string[]>(initialSymptoms ? initialSymptoms.split(',') : []);
const [searchQuery, setSearchQuery] = useState(initialSearch);
const [isUpdating, setIsUpdating] = useState(false);
const [isMobileFilterOpen, setIsMobileFilterOpen] = useState(false);
const [filteredProducts, setFilteredProducts] = useState<Product[]>([]);
const [page, setPage] = useState(1);
const [meta, setMeta] = useState({ total: 0, lastPage: 1 });
const [categories, setCategories] = useState<{ id: string; label: string; icon: React.ReactNode }[]>([]);
const [symptoms, setSymptoms] = useState<string[]>([]);
useEffect(() => {
const loadFilters = async () => {
try {
const filters = await productService.getActiveFilters();
const mapped = [
{ id: "all", label: "همه محصولات", icon: <Activity className="w-4 h-4" /> },
...filters.categories.map(c => ({
id: c.slug,
label: c.name,
icon: ICON_MAP[c.slug] || <Activity className="w-4 h-4" />
}))
];
setCategories(mapped);
setSymptoms(filters.symptoms);
} catch (e) {
console.error("Failed to load active filters:", e);
}
};
loadFilters();
}, []);
const CATEGORY_LABELS = useMemo(() => {
const labels: Record<string, string> = { all: "همه محصولات" };
categories.forEach(c => {
labels[c.id] = c.label;
});
return labels;
}, [categories]);
// Sync state with prop change (e.g. from Header menu)
useEffect(() => {
// Normalize Input (Trim and lowercase)
const normSearch = (initialSearch || "").trim().toLowerCase();
const normCat = initialCategory.trim().toLowerCase();
const mapKey = normSearch || normCat;
const mapping = CATEGORY_MAP[mapKey];
if (selectedCategory === initialCategory && searchQuery === initialSearch) {
return;
}
setIsUpdating(true);
// 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, symptoms]);
// Update URL Query Parameters
useEffect(() => {
const params = new URLSearchParams();
if (selectedCategory && selectedCategory !== "all") params.set('category', selectedCategory);
if (selectedPet && selectedPet !== "all") params.set('petType', selectedPet);
if (searchQuery) params.set('search', searchQuery);
if (activeSymptoms.length > 0) params.set('symptoms', activeSymptoms.join(','));
const queryString = params.toString();
const newUrl = `/shop${queryString ? `?${queryString}` : ''}`;
router.push(newUrl, { scroll: false });
}, [selectedCategory, selectedPet, searchQuery, activeSymptoms]);
const fetchProducts = async () => {
setIsUpdating(true);
try {
const res = await productService.getProducts({
category: selectedCategory,
petType: selectedPet,
query: searchQuery,
limit: 999,
});
let result = res.data;
// Filter with OR logic (union of symptoms) if symptoms are active
if (activeSymptoms.length > 0) {
result = result.filter(p => p.symptoms && p.symptoms.some(s => activeSymptoms.includes(s)));
}
setFilteredProducts(result);
setMeta(res.meta);
} catch (error) {
console.error("Failed to fetch products:", error);
toast.error("خطا در دریافت لیست محصولات از سرور. لطفاً صفحه را رفرش کنید.");
} finally {
setIsUpdating(false);
}
};
useEffect(() => {
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 Header & Filter Trigger */}
<div className="lg:hidden mb-6 space-y-4">
<div className="flex items-center justify-between">
<button
onClick={() => router.back()}
className="flex items-center gap-1 text-sm font-bold text-medical-gray-600"
>
<ChevronRight className="w-4 h-4 font-vazir" />
<span>بازگشت</span>
</button>
<div className="flex items-center gap-2">
<button
onClick={() => setIsMobileFilterOpen(!isMobileFilterOpen)}
className="flex items-center gap-2 bg-canina-blue text-white px-4 py-2 rounded-xl text-xs font-black shadow-md shadow-canina-blue/20"
>
<Filter className="w-4 h-4" />
<span>فیلترها {activeSymptoms.length > 0 || selectedPet !== "all" || selectedCategory !== "all" ? "•" : ""}</span>
</button>
</div>
</div>
<div className="flex items-center gap-1 text-[10px] font-black text-medical-gray-400 uppercase tracking-widest overflow-x-auto whitespace-nowrap pb-1">
<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">فروشگاه تخصصی ({meta.total || filteredProducts.length} محصول)</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="cursor-pointer hover:text-canina-blue" onClick={() => { setSelectedCategory("all"); }}>کاتالوگ</span>
{selectedCategory !== "all" && (
<>
<ChevronLeft className="w-2.5 h-2.5" />
<span className="text-canina-blue">{CATEGORY_LABELS[selectedCategory] || selectedCategory}</span>
</>
)}
</div>
<div className="flex flex-col lg:flex-row gap-8">
{/* Sidebar Filters - Collapsible on Mobile */}
<aside className={`lg:w-72 flex-shrink-0 space-y-6 ${isMobileFilterOpen ? 'block' : 'hidden lg:block'}`}>
<div className="bg-white rounded-[2rem] p-6 border border-medical-gray-200 shadow-sm">
<div className="flex items-center justify-between mb-6">
<div className="flex items-center gap-2">
<Filter className="w-5 h-5 text-canina-blue" />
<h3 className="font-black text-medical-gray-900">فیلترهای تخصصی</h3>
</div>
<button
onClick={() => setIsMobileFilterOpen(false)}
className="lg:hidden text-xs text-medical-gray-400 font-bold"
>
بستن
</button>
</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); setIsMobileFilterOpen(false); }}
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>
{/* 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} />
))}
</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>
);
}