canina/frontend/application/components/FeaturedProducts.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

167 lines
8.4 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 { 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>
);
}