canina/frontend/application/components/FeaturedProducts.tsx
parsa aghaei 01a538ca5a
Some checks failed
Deploy Canina / deploy (push) Successful in 2m21s
E2E Playwright Tests / Run Full E2E & Security Suites (push) Failing after 0s
feat(home): mobile 1.5 card horizontal slider with scrollbar for featured products
2026-09-06 11:25:21 +03:30

242 lines
13 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";
import { isSuitableForDog, isSuitableForCat, isSuitableForBoth, isSpeciesCompatible, matchesMedicalSymptom } from "../lib/petCompatibility";
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 forDog = isSuitableForDog(product.suitableFor);
const forCat = isSuitableForCat(product.suitableFor);
const forBoth = isSuitableForBoth(product.suitableFor) || (forDog && forCat);
const compatibility = useMemo(() => {
if (!activePet) return null;
const petType = activePet.type || 'سگ';
const sameSpecies = isSpeciesCompatible(product.suitableFor, petType);
const matchedSymptom = matchesMedicalSymptom(activePet.medicalConditions, product.symptoms, product.benefits);
if (!sameSpecies) return { type: "alert", text: `مخصوص ${product.suitableFor || 'پت'}` };
if (matchedSymptom) return { type: "success", text: `توصیه شده برای ${matchedSymptom} ${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 w-full"
>
{/* Category Badge */}
<div className="absolute top-2.5 right-2.5 sm:top-4 sm:right-4 z-30 px-2 py-0.5 sm:px-2.5 sm:py-1 bg-white/90 backdrop-blur-md rounded-full border border-medical-gray-200/80 text-[10px] 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.4rem] sm:top-[3.25rem] right-2.5 sm:right-4 z-10 text-[9px] sm:text-3xs font-black uppercase tracking-widest px-2 py-0.5 sm:px-2.5 sm: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" ? (
forBoth ? (
<span className="flex items-center gap-0.5">
<Dog className="w-2.5 h-2.5" />
<Cat className="w-2.5 h-2.5" />
</span>
) : forCat ? (
<Cat className="w-2.5 h-2.5" />
) : forDog ? (
<Dog className="w-2.5 h-2.5" />
) : (
<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-2 sm:p-3">
<SafeImage
src={product.image}
alt={nameFa}
priority={priority}
sizes="(max-width: 640px) 240px, (max-width: 1024px) 240px, 280px)"
className="w-full h-full group-hover:scale-105 transition-transform duration-500"
imgClassName="object-contain w-full h-full max-h-[130px] sm:max-h-[200px] mx-auto"
/>
{/* Species Badges (Dog / Cat) */}
<div className="absolute bottom-2 left-2 flex gap-1 z-10">
{forDog && (
<div className="w-5 h-5 sm:w-6 sm: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 h-3 sm:w-3.5 sm:h-3.5 text-canina-blue" />
</div>
)}
{forCat && (
<div className="w-5 h-5 sm:w-6 sm: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 h-3 sm:w-3.5 sm: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="p-3 sm:px-5 sm:py-4 flex flex-col flex-1 gap-1.5 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-[10px] 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-[11px] sm:text-xs text-medical-gray-500 leading-relaxed 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 mt-auto">
<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-[10px] sm:text-xs-plus font-black text-amber-600 bg-amber-50 px-2 py-0.5 sm:px-2.5 sm:py-1 rounded-full border border-amber-200">
ثبت پیشخرید
</span>
) : (
<span className="text-[10px] 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="flex md:grid md:grid-cols-2 lg:grid-cols-4 gap-4 md:gap-6 overflow-x-auto md:overflow-visible pb-4 md:pb-0 micro-hscroll -mx-4 px-4 md:mx-0 md:px-0 select-none touch-pan-x snap-x snap-mandatory md:snap-none">
{isLoading ? (
Array.from({ length: 4 }).map((_, i) => (
<div key={i} className="w-[68vw] sm:w-[50vw] md:w-auto flex-shrink-0 snap-start">
<ProductCardSkeleton />
</div>
))
) : products.length > 0 ? (
products.map((p, i) => (
<div key={p.id} className="w-[68vw] sm:w-[50vw] md:w-auto flex-shrink-0 snap-start flex">
<ProductCard product={p} priority={i < 2} />
</div>
))
) : (
<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 w-full">
<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>
);
}