- 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
167 lines
8.4 KiB
TypeScript
167 lines
8.4 KiB
TypeScript
"use client";
|
||
import { useState, useEffect, useMemo } from "react";
|
||
import { Product } from "../lib/data/products";
|
||
import { usePetStore } from "../lib/store/usePetStore";
|
||
import { productService } from "../lib/services/productService";
|
||
import { motion, AnimatePresence } from "motion/react";
|
||
import { Eye, ShoppingCart, ShieldCheck, Heart, AlertCircle, Sparkles } from "lucide-react";
|
||
import SafeImage from "./SafeImage";
|
||
import { ProductCardSkeleton } from "./Skeleton";
|
||
import Link from 'next/link';
|
||
|
||
function ProductCard({ product, onClick }: { product: Product; key?: string | number; onClick: (p: Product) => void }) {
|
||
const { getActivePet } = usePetStore();
|
||
const activePet = getActivePet();
|
||
|
||
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}` };
|
||
return { type: 'neutral', text: `مناسب برای ${activePet.name}` };
|
||
}, [product, activePet]);
|
||
|
||
return (
|
||
<motion.div
|
||
initial={{ opacity: 0, y: 30 }}
|
||
whileInView={{ opacity: 1, y: 0 }}
|
||
viewport={{ once: true }}
|
||
transition={{ duration: 0.5 }}
|
||
onClick={() => onClick(product)}
|
||
className="group bg-white rounded-3xl border border-medical-gray-200 overflow-hidden hover:shadow-2xl hover:shadow-canina-blue/10 transition-all duration-500 flex flex-col h-full cursor-pointer relative"
|
||
>
|
||
{compatibility && (
|
||
<div className={`absolute top-16 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>
|
||
)}
|
||
{/* Product Image Area */}
|
||
<div className="relative aspect-square bg-medical-gray-50 p-10 flex items-center justify-center overflow-hidden">
|
||
<SafeImage
|
||
src={product.image}
|
||
alt={product.name}
|
||
className="w-full h-full group-hover:scale-105 transition-transform duration-500 drop-shadow-2xl"
|
||
imgClassName="object-contain"
|
||
/>
|
||
<div className="absolute top-4 right-4 px-3 py-1 bg-white/80 backdrop-blur-md rounded-full border border-medical-gray-200 text-[10px] font-bold text-medical-gray-500 uppercase tracking-widest">
|
||
{product.category}
|
||
</div>
|
||
|
||
{/* Hover Actions */}
|
||
<Link href={`/shop/${product.slug || product.id}`} className="absolute inset-0 z-10" />
|
||
<div className="absolute inset-0 bg-canina-blue/60 opacity-0 group-hover:opacity-100 backdrop-blur-sm transition-all duration-300 flex items-center justify-center gap-4">
|
||
<div className="w-12 h-12 rounded-full bg-white text-canina-blue flex items-center justify-center hover:scale-110 transition-transform shadow-lg">
|
||
<Eye className="w-5 h-5" />
|
||
</div>
|
||
<div className="w-12 h-12 rounded-full bg-canina-blue text-white flex items-center justify-center hover:scale-110 transition-transform shadow-lg border border-white/20">
|
||
<ShoppingCart className="w-5 h-5" />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Content Area */}
|
||
<div className="p-8 flex flex-col flex-1">
|
||
<div className="flex items-start justify-between mb-4 gap-2">
|
||
<h3 className="text-xl font-black text-medical-gray-900 group-hover:text-canina-blue transition-colors leading-tight">
|
||
{product.name}
|
||
</h3>
|
||
<ShieldCheck className="w-6 h-6 text-canina-blue flex-shrink-0" />
|
||
</div>
|
||
|
||
<p className="text-sm text-medical-gray-500 leading-relaxed mb-6 flex-1">
|
||
{product.description}
|
||
</p>
|
||
|
||
<div className="space-y-4 pt-4 border-t border-medical-gray-100">
|
||
<div className="flex items-center gap-3">
|
||
<div className="w-1 h-1 rounded-full bg-canina-blue" />
|
||
<span className="text-xs font-semibold text-medical-gray-700 italic">{product.benefits}</span>
|
||
</div>
|
||
|
||
<div className="flex items-center justify-between">
|
||
<div className="text-xl font-black text-medical-gray-900 font-vazir">{product.price}</div>
|
||
<button className="text-xs font-black text-canina-blue uppercase tracking-widest border-b-2 border-canina-blue pb-1 hover:text-medical-gray-900 hover:border-medical-gray-900 transition-all">
|
||
مشاهده جزییات
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</motion.div>
|
||
);
|
||
}
|
||
|
||
import { useRouter } from 'next/navigation';
|
||
import { toast } from 'sonner';
|
||
|
||
export default function FeaturedProducts() {
|
||
const router = useRouter();
|
||
const [products, setProducts] = useState<Product[]>([]);
|
||
const [isLoading, setIsLoading] = useState(true);
|
||
|
||
useEffect(() => {
|
||
const fetchFeatured = async () => {
|
||
try {
|
||
const data = await productService.getFeaturedProducts();
|
||
setProducts(data);
|
||
} catch (error) {
|
||
console.error("Failed to fetch featured products:", error);
|
||
toast.error("خطا در دریافت لیست محصولات از سرور. لطفاً صفحه را رفرش کنید.");
|
||
} finally {
|
||
setIsLoading(false);
|
||
}
|
||
};
|
||
fetchFeatured();
|
||
}, []);
|
||
|
||
return (
|
||
<section className="py-24 bg-medical-gray-50">
|
||
<div className="max-w-7xl mx-auto px-4">
|
||
<div className="flex flex-col lg:flex-row items-end justify-between mb-16 gap-6">
|
||
<div className="lg:w-2/3">
|
||
<div className="text-canina-blue text-xs font-bold uppercase tracking-[0.2em] mb-4 font-vazir">بیشترین انتخاب توسط دامپزشکان</div>
|
||
<h2 className="text-3xl lg:text-5xl font-black text-medical-gray-900 leading-[1.2]">
|
||
محصولات برگزیده و راهکارهای <br />
|
||
<span className="italic text-canina-blue">درمان تخصصی</span>
|
||
</h2>
|
||
</div>
|
||
<div className="lg:w-1/3 text-left lg:text-right">
|
||
<p className="text-medical-gray-500 text-sm leading-relaxed mb-4">
|
||
محصولات دارویی کانینا با استفاده از دانش پیشرفته بیوتکنولوژی و مواد اولیه ارگانیک نایاب فرآوری شدهاند.
|
||
</p>
|
||
<button
|
||
onClick={() => router.push('/shop')}
|
||
className="inline-flex items-center gap-2 text-canina-blue font-bold text-sm hover:translate-x-1 transition-transform rtl:hover:-translate-x-1"
|
||
>
|
||
مشاهده تمامی محصولات
|
||
<span className="text-lg">←</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-10">
|
||
{isLoading ? (
|
||
Array.from({ length: 3 }).map((_, i) => <ProductCardSkeleton key={i} />)
|
||
) : products.length > 0 ? (
|
||
products.map((p) => (
|
||
<ProductCard key={p.id} onClick={(p) => router.push(`/shop/${p.slug || p.id}`)} product={p} />
|
||
))
|
||
) : (
|
||
<div className="col-span-full py-16 px-8 flex flex-col items-center justify-center text-center bg-white rounded-[2.5rem] border border-medical-gray-200 shadow-xl max-w-lg mx-auto">
|
||
<AlertCircle className="w-12 h-12 text-canina-teal mb-4" />
|
||
<h3 className="text-xl font-black text-medical-gray-900 mb-2 font-vazir">هیچ محصولی یافت نشد</h3>
|
||
<p className="text-sm text-medical-gray-500 font-medium font-vazir leading-relaxed">
|
||
در حال حاضر هیچ محصول برجستهای برای نمایش وجود ندارد. لطفاً در پنل ادمین محصولات برگزیده را تنظیم کنید.
|
||
</p>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</section>
|
||
);
|
||
}
|