canina/frontend/application/components/FeaturedProducts.tsx
parsa aghaei 3aaea0062b
Some checks failed
Deploy Canina / deploy (push) Successful in 2m14s
E2E Playwright Tests / Run Full E2E & Security Suites (push) Failing after 6s
perf: optimize font payloads, enable responsive image optimization for uploads, and set high priority on top hero/product images
2026-09-02 15:54:12 +03:30

213 lines
12 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.

"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 } from "motion/react";
import { Eye, ShoppingCart, Heart, AlertCircle, Sparkles, Dog, Cat } from "lucide-react";
import SafeImage from "./SafeImage";
import { ProductCardSkeleton } from "./Skeleton";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { useSettingsStore } from "../lib/store/settingsStore";
import { useCartStore } from "../lib/store/cartStore";
import { toast } from "sonner";
function ProductCard({ product, priority = false }: { product: Product; priority?: boolean }) {
const router = useRouter();
const { getText, isInitialized } = useSettingsStore();
const { getActivePet } = usePetStore();
const activePet = getActivePet();
const { addItem } = useCartStore();
const nameFa = product.nameFa || product.name;
const nameEn = product.nameEn || "";
const compatibility = useMemo(() => {
if (!activePet) return null;
const petType = activePet.type || 'سگ';
const sameSpecies = product.suitableFor === petType || product.suitableFor === "هر دو";
const productSymptoms = Array.isArray(product.symptoms) ? product.symptoms : [];
const petConditions = Array.isArray(activePet.medicalConditions) ? activePet.medicalConditions : [];
const helpsSymptom = petConditions.some((s) => productSymptoms.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]);
const isCatalogOnly = isInitialized ? getText('CATALOG_ONLY_MODE', 'false') === 'true' : false;
const showPreorderBtn = isCatalogOnly && (isInitialized ? getText('CATALOG_PREORDER_BTN', 'false') === 'true' : false);
const allowCart = !isCatalogOnly && (isInitialized ? getText('allow_cart_purchase', 'true') === 'true' : true);
const showPrices = !isCatalogOnly || (isInitialized ? getText('show_product_prices', 'true') === 'true' : true);
return (
<motion.div
initial={{ opacity: 0, y: 15 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.3, ease: "easeOut" }}
onClick={() => router.push(`/shop/${product.slug || product.id}`)}
className="group bg-white/90 backdrop-blur-md rounded-2xl sm:rounded-3xl border border-medical-gray-200/80 overflow-hidden hover:shadow-xl hover:shadow-canina-blue/10 hover:border-canina-blue/30 transition-all duration-300 flex flex-col cursor-pointer relative"
>
{/* Category Badge */}
<div className="absolute top-3 right-3 sm:top-4 sm:right-4 z-30 px-2.5 py-1 bg-white/90 backdrop-blur-md rounded-full border border-medical-gray-200/80 text-2xs sm:text-xs font-black text-medical-gray-700 uppercase tracking-wider shadow-xs group-hover:bg-canina-blue group-hover:text-white transition-all">
{product.category}
</div>
{/* Compatibility Tag */}
{compatibility && (
<div className={`absolute top-[2.8rem] sm:top-[3.25rem] right-3 sm:right-4 z-10 text-3xs font-black uppercase tracking-widest px-2.5 py-0.5 rounded-full shadow-xs 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>
)}
{/* Image Container */}
<div className="relative aspect-[16/11] sm:aspect-[4/3] bg-medical-gray-50 flex items-center justify-center overflow-hidden p-1 sm:p-2">
<SafeImage
src={product.image}
alt={nameFa}
priority={priority}
sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 25vw)"
className="w-full h-full group-hover:scale-105 transition-transform duration-500"
imgClassName="object-contain w-full h-full max-h-[160px] sm:max-h-[200px] mx-auto"
/>
{/* Species Badges (Dog / Cat) */}
<div className="absolute bottom-2 left-2 flex gap-1 z-10">
{(product.suitableFor === "سگ" || product.suitableFor === "هر دو") && (
<div className="w-6 h-6 bg-white/90 backdrop-blur-xs rounded-md flex items-center justify-center text-medical-gray-500 border border-medical-gray-200 shadow-2xs" title="مناسب برای سگ">
<Dog className="w-3.5 h-3.5 text-canina-blue" />
</div>
)}
{(product.suitableFor === "گربه" || product.suitableFor === "هر دو") && (
<div className="w-6 h-6 bg-white/90 backdrop-blur-xs rounded-md flex items-center justify-center text-medical-gray-500 border border-medical-gray-200 shadow-2xs" title="مناسب برای گربه">
<Cat className="w-3.5 h-3.5 text-canina-blue" />
</div>
)}
</div>
{/* Hover Actions for desktop */}
<div className="hidden sm:flex absolute inset-0 bg-canina-blue/60 opacity-0 group-hover:opacity-100 backdrop-blur-xs transition-all duration-300 items-center justify-center gap-3 z-20">
<div
onClick={(e) => { e.stopPropagation(); router.push(`/shop/${product.slug || product.id}`); }}
className="w-10 h-10 rounded-full bg-white text-canina-blue flex items-center justify-center hover:scale-110 transition-transform shadow-lg cursor-pointer"
title="مشاهده جزئیات"
>
<Eye className="w-5 h-5" />
</div>
{allowCart && (product.packageSize !== undefined && product.packageSize !== null ? Number(product.packageSize) : 100) > 0 && (
<div
onClick={(e) => {
e.stopPropagation();
addItem(product, 1);
toast.success(`${nameFa} به سبد خرید اضافه شد.`);
}}
className="w-10 h-10 rounded-full bg-canina-blue text-white flex items-center justify-center hover:scale-110 transition-transform shadow-lg border border-white/20 cursor-pointer"
title="افزودن به سبد خرید"
>
<ShoppingCart className="w-5 h-5" />
</div>
)}
</div>
</div>
{/* Content */}
<div className="px-3.5 py-3 sm:px-5 sm:py-4 flex flex-col flex-1 gap-2 sm:gap-3">
{/* Dual-language Name */}
<div className="flex flex-col gap-0.5">
<h3 dir="rtl" className="text-xs sm:text-sm font-black text-medical-gray-900 group-hover:text-canina-blue transition-colors leading-snug text-right line-clamp-1 sm:line-clamp-2 font-vazir">
{nameFa}
</h3>
{nameEn && (
<span dir="ltr" className="text-2xs sm:text-xs-plus font-semibold text-slate-400 font-sans text-left block line-clamp-1">
{nameEn}
</span>
)}
</div>
{/* Short description */}
<p className="text-xs-plus sm:text-xs text-medical-gray-500 leading-relaxed line-clamp-1 sm:line-clamp-2 flex-1">
{product.shortDescription || (product.description ? product.description.replace(/<[^>]*>/g, '') : '')}
</p>
{/* Footer: Price + CTA */}
<div className="flex items-center justify-between pt-2 sm:pt-3 border-t border-medical-gray-100">
<div className="text-xs sm:text-sm md:text-base font-black text-medical-gray-900 font-vazir whitespace-nowrap">
{showPrices ? (typeof product.price === 'string' || typeof product.price === 'number' ? String(product.price) : 'تماس بگیرید') : 'تماس بگیرید'}
</div>
{showPreorderBtn ? (
<span className="text-2xs sm:text-xs-plus font-black text-amber-600 bg-amber-50 px-2.5 py-1 rounded-full border border-amber-200">
ثبت پیشخرید
</span>
) : (
<span className="text-2xs sm:text-xs-plus font-black text-canina-blue uppercase tracking-wider border-b border-canina-blue pb-0.5 group-hover:text-medical-gray-900 group-hover:border-medical-gray-900 transition-all whitespace-nowrap">
{getText("product_view_details", "مشاهده جزئیات")}
</span>
)}
</div>
</div>
</motion.div>
);
}
export default function FeaturedProducts() {
const router = useRouter();
const getText = useSettingsStore((state) => state.getText);
const [products, setProducts] = useState<Product[]>([]);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
productService.getFeaturedProducts()
.then(setProducts)
.catch(() => toast.error("خطا در دریافت لیست محصولات از سرور."))
.finally(() => setIsLoading(false));
}, []);
return (
<section className="py-8 sm:py-16 bg-medical-gray-50">
<div className="max-w-7xl mx-auto px-4">
<div className="flex items-center justify-between w-full mb-6 sm:mb-12 pb-4 sm:pb-6 border-b border-medical-gray-200 gap-2">
<div>
<div className="text-canina-blue text-[10px] sm:text-xs font-bold uppercase tracking-[0.2em] mb-1 sm:mb-2 font-vazir">
{getText("featured_badge", "بیشترین انتخاب توسط دامپزشکان")}
</div>
<h2 className="text-xl sm:text-2xl md:text-3xl lg:text-4xl font-black text-medical-gray-900 font-lalezar tracking-wide">
{getText("featured_title_line1", "محصولات برگزیده و راهکارهای")}{" "}
<span className="text-canina-blue">{getText("featured_title_line2", "درمان تخصصی")}</span>
</h2>
</div>
<button
onClick={() => router.push("/shop")}
className="inline-flex items-center gap-1 sm:gap-2 text-canina-blue font-black text-xs sm:text-sm hover:translate-x-1 transition-all group font-vazir shrink-0"
>
<span>{getText("featured_view_all", "مشاهده تمامی محصولات")}</span>
<span className="text-sm sm:text-lg group-hover:-translate-x-1 transition-transform"></span>
</button>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
{isLoading ? (
Array.from({ length: 4 }).map((_, i) => <ProductCardSkeleton key={i} />)
) : products.length > 0 ? (
products.map((p, i) => <ProductCard key={p.id} product={p} priority={i < 2} />)
) : (
<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">
{getText("featured_no_products_title", "هیچ محصولی یافت نشد")}
</h3>
<p className="text-sm text-medical-gray-500 font-medium font-vazir leading-relaxed">
{getText("featured_no_products_desc", "در حال حاضر هیچ محصول برجسته‌ای برای نمایش وجود ندارد.")}
</p>
</div>
)}
</div>
</div>
</section>
);
}