canina/frontend/application/components/Hero.tsx
parsa aghaei a9fbcbb90c
Some checks failed
Deploy Canina / deploy (push) Successful in 2m31s
E2E Playwright Tests / Run Full E2E & Security Suites (push) Failing after 0s
feat(shop): mobile filter slider, list/card view toggle, and breadcrumb fixes
2026-09-06 09:30:02 +03:30

363 lines
20 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

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.

import Image from "next/image";
import { motion, AnimatePresence, animate } from "motion/react";
import { ChevronLeft, ChevronRight, Globe, ShieldCheck, Calendar, ArrowLeft } from "lucide-react";
import { useEffect, useState, useRef } from "react";
import { toPersian } from "../lib/utils";
import { useSettingsStore } from "../lib/store/settingsStore";
import { useRouter } from 'next/navigation';
import { Banner } from "../lib/types";
import { getMediaUrl } from "../lib/media";
function StatCounter({ target }: { target: number }) {
const [displayValue, setDisplayValue] = useState(target);
useEffect(() => {
const controls = animate(0, target, {
duration: 2,
ease: [0.16, 1, 0.3, 1],
onUpdate: (latest) => setDisplayValue(Math.round(latest))
});
return controls.stop;
}, [target]);
return (
<div
className="text-4xl lg:text-6xl font-black text-canina-blue font-vazir origin-right flex items-center min-w-[3ch]"
>
<span>
{toPersian(displayValue)}+
</span>
</div>
);
}
export default function Hero({ banners = [], onShopNavigate }: { banners?: Banner[]; onShopNavigate?: () => void }) {
const router = useRouter();
const getText = useSettingsStore((state) => state?.getText || ((_k: string, fb: string) => fb));
const isInitialized = useSettingsStore((state) => state?.isInitialized ?? false);
const [mounted, setMounted] = useState(false);
const [currentSlide, setCurrentSlide] = useState(0);
const [isPaused, setIsPaused] = useState(false);
useEffect(() => {
setMounted(true);
}, []);
const heroBanners = banners.filter(b => b.isActive !== false && b.imageUrl);
const totalSlides = heroBanners.length;
// Auto-advance slider
useEffect(() => {
if (totalSlides <= 1 || isPaused) return;
const interval = setInterval(() => {
setCurrentSlide(prev => (prev + 1) % totalSlides);
}, 5000);
return () => clearInterval(interval);
}, [totalSlides, isPaused]);
const activeBanner = totalSlides > 0 ? heroBanners[currentSlide] : null;
const defaultTitle = activeBanner?.title || (banners.length > 0 && banners[0].title ? banners[0].title : "تخصص آلمانی در خدمت\nسلامت پت‌های خانگی");
const defaultSubtitle = activeBanner?.subtitle || (banners.length > 0 && banners[0].subtitle ? banners[0].subtitle : 'از سال ۱۹۸۴، شرکت Canina pharma GmbH در برگیش گلادباخ آلمان، با بهره‌گیری از مواد اولیه طبیعی و فرآیندهای پیشرفته، استاندارد طلایی مکمل‌های دامپزشکی را تعریف می‌کند. کنینا نماینده رسمی این برند در ایران است.');
const title = (mounted && isInitialized ? getText('hero_title', '') : '') || defaultTitle;
const subtitle = (mounted && isInitialized ? getText('hero_desc', '') : '') || defaultSubtitle;
const titleParts = title.split('\n');
const rawHeroImg = (mounted && isInitialized ? getText('hero_image_url', '') : '');
const sanitizedHeroImg = rawHeroImg.includes('hero-section-image.png') ? '' : rawHeroImg;
const activeImageUrl = activeBanner?.imageUrl || sanitizedHeroImg || (banners.length > 0 && banners[0].imageUrl ? banners[0].imageUrl : "");
const handleNextSlide = () => {
if (totalSlides > 1) {
setCurrentSlide(prev => (prev + 1) % totalSlides);
}
};
const handlePrevSlide = () => {
if (totalSlides > 1) {
setCurrentSlide(prev => (prev - 1 + totalSlides) % totalSlides);
}
};
const touchStartX = useRef<number | null>(null);
const touchEndX = useRef<number | null>(null);
const minSwipeDistance = 40;
const onTouchStart = (e: React.TouchEvent) => {
touchEndX.current = null;
touchStartX.current = e.targetTouches[0].clientX;
};
const onTouchMove = (e: React.TouchEvent) => {
touchEndX.current = e.targetTouches[0].clientX;
};
const onTouchEnd = () => {
if (!touchStartX.current || !touchEndX.current) return;
const distance = touchStartX.current - touchEndX.current;
const isLeftSwipe = distance > minSwipeDistance;
const isRightSwipe = distance < -minSwipeDistance;
if (isRightSwipe) {
handleNextSlide();
} else if (isLeftSwipe) {
handlePrevSlide();
}
};
return (
<section className="relative overflow-hidden bg-white py-3 sm:py-6 border-b border-medical-gray-100">
{/* Background patterns */}
<div className="absolute top-0 left-0 w-full h-full opacity-[0.03] pointer-events-none select-none overflow-hidden">
<div className="absolute top-0 right-0 w-[800px] h-[800px] bg-canina-blue rounded-full blur-[120px] -mr-96 -mt-96" />
<div className="absolute bottom-0 left-0 w-[600px] h-[600px] bg-medical-gray-500 rounded-full blur-[100px] -ml-40 -mb-40" />
</div>
<div className="max-w-7xl mx-auto px-4 relative flex flex-col lg:flex-row gap-4 sm:gap-12 mb-8 sm:mb-16 lg:mb-20">
{/* Content */}
<div className="lg:w-1/2 text-center lg:text-right">
<div>
<div className="inline-flex items-center gap-2 px-3 py-1 bg-canina-blue/5 border border-canina-blue/10 rounded-full mt-3 mb-3 sm:mb-5">
<span className="w-2 h-2 rounded-full bg-canina-blue animate-pulse" />
<span suppressHydrationWarning className="text-canina-blue text-xs font-bold uppercase tracking-widest leading-none">
{mounted && isInitialized ? getText('hero_badge', "از ۱۹۸۴ در خدمت سلامت حیوانات") : "از ۱۹۸۴ در خدمت سلامت حیوانات"}
</span>
</div>
<h1 suppressHydrationWarning className="text-2xl lg:text-7xl font-black text-medical-gray-900 leading-[1.2] mb-2 sm:mb-6 font-lalezar tracking-wide">
{titleParts[0]} <br />
{titleParts[1] && <span className="text-canina-blue ">{titleParts[1]}</span>}
</h1>
<div
suppressHydrationWarning
className="text-sm lg:text-xl text-medical-gray-600 mb-3 sm:mb-5 max-w-2xl mx-auto lg:mx-0 leading-relaxed font-vazir animate-fade-in [&_p]:text-sm [&_p]:lg:text-xl [&_p]:leading-relaxed [&_p]:my-0 [&_span]:text-inherit"
dangerouslySetInnerHTML={{
__html: subtitle
}}
/>
<div className="whitespace-nowrap flex flex-nowrap justify-center lg:justify-start gap-2 sm:gap-6">
<button
onClick={() => {
if (onShopNavigate) {
onShopNavigate();
} else {
router.push('/shop');
}
}}
className="bg-canina-blue text-white px-3 sm:px-10 py-2 sm:py-5 rounded-full font-bold text-lg hover:bg-canina-blue/90 hover:shadow-2xl focus-visible:ring-4 focus-visible:ring-canina-blue/30 focus-visible:outline-none transition-all gap-2 group font-vazir shadow-lg shadow-canina-blue/20 cursor-pointer"
>
<span suppressHydrationWarning>{mounted && isInitialized ? getText('hero_btn_products', "مشاهده محصولات") : "مشاهده محصولات"}</span>
</button>
<button
onClick={() => {
const element = document.getElementById('canino-advisor');
if (element) {
element.scrollIntoView({ behavior: 'smooth' });
}
}}
className="flex items-center bg-white border-2 border-medical-gray-900 text-medical-gray-900 px-3 sm:px-10 py-2 sm:py-5 rounded-full font-bold text-lg hover:bg-medical-gray-50 focus-visible:ring-4 focus-visible:ring-canina-blue/30 focus-visible:outline-none transition-all font-vazir shadow-sm cursor-pointer"
>
<span suppressHydrationWarning>{mounted && isInitialized ? getText('hero_btn_advisor', "دستیار سلامت پت") : "دستیار سلامت پت"}</span>
<ChevronLeft className="w-5 h-5 group-hover:-translate-x-1 transition-transform" />
</button>
</div>
</div>
</div>
{/* Visual Element & Interactive Slider */}
<div
className="lg:w-[40%] relative w-full max-w-lg lg:max-w-none mx-auto"
onMouseEnter={() => setIsPaused(true)}
onMouseLeave={() => setIsPaused(false)}
>
<div className="relative aspect-square w-full max-w-md mx-auto">
{/* Glow backdrop behind image */}
<div className="absolute inset-0 bg-gradient-to-tr from-canina-blue/20 via-canina-gold/15 to-transparent rounded-3xl blur-2xl transform -rotate-3 scale-95 pointer-events-none" />
<div className="relative rounded-3xl overflow-hidden shadow-2xl border-4 border-white bg-medical-gray-100 aspect-square w-full"
onTouchStart={onTouchStart}
onTouchMove={onTouchMove}
onTouchEnd={onTouchEnd}
>
{/* Preloaded and persistent slider image stack */}
{heroBanners.length > 0 ? (
heroBanners.map((banner, idx) => (
<motion.div
key={banner.imageUrl || idx}
initial={false}
animate={{
opacity: currentSlide === idx ? 1 : 0,
scale: currentSlide === idx ? 1 : 1.04,
zIndex: currentSlide === idx ? 10 : 0,
}}
transition={{ duration: 0.6, ease: "easeInOut" }}
className="absolute inset-0 w-full h-full"
>
<Image
src={getMediaUrl(banner.imageUrl)}
alt={banner.title || "Canina Pharma Germany"}
fill
priority={idx === 0}
fetchPriority={idx === 0 ? "high" : "auto"}
loading={idx === 0 ? "eager" : "lazy"}
sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 500px"
className="object-cover"
/>
</motion.div>
))
) : activeImageUrl ? (
<div className="absolute inset-0 w-full h-full">
<Image
src={getMediaUrl(activeImageUrl)}
alt={activeBanner?.title || "Canina Pharma Germany"}
fill
priority
fetchPriority="high"
loading="eager"
sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 500px"
className="object-cover"
/>
</div>
) : null}
{/* Gradient overlay and slide caption */}
<div className="absolute bottom-0 left-0 right-0 p-6 sm:p-8 bg-gradient-to-t from-black/85 via-black/40 to-transparent text-white text-right z-20">
<div suppressHydrationWarning className="text-sm font-medium mb-1 opacity-80 uppercase tracking-widest text-[10px]">
{mounted && isInitialized ? getText('hero_image_badge', "سرآمد علمی در پزشکی پت‌ها") : "سرآمد علمی در پزشکی پت‌ها"}
</div>
<div suppressHydrationWarning className="text-lg sm:text-xl font-bold italic tracking-tighter">
{activeBanner?.title || (mounted && isInitialized ? getText('hero_image_title', "مکمل‌های تایید شده دامپزشکی با گواهی IFS") : "مکمل‌های تایید شده دامپزشکی با گواهی IFS")}
</div>
{activeBanner?.linkUrl && (
<a
href={activeBanner.linkUrl}
className="inline-flex items-center gap-1 text-xs text-canina-gold hover:underline font-bold mt-2"
>
<span>اطلاعات بیشتر و خرید</span>
<ArrowLeft className="w-3.5 h-3.5" />
</a>
)}
</div>
{/* Slider Navigation Arrows (shown if multiple slides exist) */}
{totalSlides > 1 && (
<>
<button
onClick={handlePrevSlide}
className="absolute right-4 top-1/2 -translate-y-1/2 w-11 h-11 rounded-full bg-black/40 hover:bg-black/70 text-white backdrop-blur-md flex items-center justify-center transition-all opacity-0 group-hover:opacity-100 z-30 cursor-pointer"
aria-label="اسلاید قبلی"
>
<ChevronRight className="w-5 h-5" />
</button>
<button
onClick={handleNextSlide}
className="absolute left-4 top-1/2 -translate-y-1/2 w-11 h-11 rounded-full bg-black/40 hover:bg-black/70 text-white backdrop-blur-md flex items-center justify-center transition-all opacity-0 group-hover:opacity-100 z-30 cursor-pointer"
aria-label="اسلاید بعدی"
>
<ChevronLeft className="w-5 h-5" />
</button>
{/* Dot Indicators with 44x44 Touch Target & Composited Animations */}
<div className="absolute bottom-4 left-1/2 -translate-x-1/2 flex items-center gap-1 z-30 bg-black/40 backdrop-blur-md px-3 py-1 rounded-full">
{heroBanners.map((_, idx) => (
<button
key={idx}
onClick={() => setCurrentSlide(idx)}
className="w-8 h-8 flex items-center justify-center cursor-pointer p-0 m-0"
aria-label={`رفتن به اسلاید ${idx + 1}`}
>
<span
className={`block rounded-full transition-opacity duration-300 ${currentSlide === idx
? "w-4 h-2 bg-canina-gold opacity-100"
: "w-2 h-2 bg-white opacity-60 hover:opacity-100"
}`}
/>
</button>
))}
</div>
</>
)}
{/* Quality Badge inside the image container */}
<motion.div
animate={{ rotate: 360 }}
transition={{ duration: 20, repeat: Infinity, ease: "linear" }}
className="absolute top-6 right-6 lg:top-8 lg:right-8 z-20 w-20 h-20 lg:w-24 lg:h-24 bg-white shadow-2xl rounded-full p-2 border-2 border-dashed border-canina-blue flex items-center justify-center text-center"
>
<div className="flex flex-col items-center">
<span className="text-lg lg:text-xl font-black text-canina-blue leading-none tracking-tighter">DE</span>
<span suppressHydrationWarning className="text-[6px] lg:text-[7px] font-bold text-medical-gray-500 uppercase tracking-widest mt-0.5 leading-tight">
{mounted && isInitialized ? getText('hero_quality_standard', "استاندارد کیفی آلمان") : "استاندارد کیفی آلمان"}
</span>
</div>
</motion.div>
{/* 3D Action Badge */}
<div className="absolute bottom-24 right-6 bg-canina-gold text-medical-gray-900 px-3.5 py-1.5 rounded-2xl text-xs font-black shadow-2xl z-20 flex items-center gap-1.5 font-vazir border border-white/20 select-none">
<span className="text-sm"></span>
<span>فرمول ۳کاره</span>
</div>
</div>
</div>
</div>
</div>
{/* Stats */}
<div className="max-w-7xl mx-auto px-2 sm:px-4 relative">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.8, duration: 0.6 }}
className="grid grid-cols-3 gap-2 sm:gap-6 py-6 sm:py-10 border-t border-medical-gray-150"
>
{/* Stat 1: Founded Year */}
<div className="bg-medical-gray-50/50 border border-medical-gray-100 rounded-2xl sm:rounded-3xl p-2.5 sm:p-6 flex flex-col sm:flex-row items-center justify-center text-center sm:text-right gap-2 sm:gap-6 group hover:shadow-xl hover:shadow-canina-blue/5 transition-all">
<div className="w-9 h-9 sm:w-14 sm:h-14 bg-white rounded-xl sm:rounded-2xl flex items-center justify-center text-canina-blue shadow-sm group-hover:bg-canina-blue group-hover:text-white transition-all duration-300 flex-shrink-0">
<Calendar className="w-4 h-4 sm:w-7 sm:h-7" />
</div>
<div className="flex flex-col items-center sm:items-start text-center sm:text-right">
<div className="text-base sm:text-3xl font-black text-canina-blue font-vazir leading-none">۱۹۸۴</div>
<div suppressHydrationWarning className="text-[9px] sm:text-xs text-medical-gray-500 font-extrabold uppercase tracking-tight mt-1 sm:mt-2 font-vazir">
{mounted && isInitialized ? getText('hero_stat_founded', "سال تاسیس برند") : "سال تاسیس برند"}
</div>
</div>
</div>
{/* Stat 2: Active Agencies */}
<div className="bg-medical-gray-50/50 border border-medical-gray-100 rounded-2xl sm:rounded-3xl p-2.5 sm:p-6 flex flex-col sm:flex-row items-center justify-center text-center sm:text-right gap-2 sm:gap-6 group hover:shadow-xl hover:shadow-canina-blue/5 transition-all">
<div className="w-9 h-9 sm:w-14 sm:h-14 bg-white rounded-xl sm:rounded-2xl flex items-center justify-center text-canina-blue shadow-sm group-hover:bg-canina-blue group-hover:text-white transition-all duration-300 flex-shrink-0">
<Globe className="w-4 h-4 sm:w-7 sm:h-7" />
</div>
<div className="flex flex-col items-center sm:items-start text-center sm:text-right">
<div className="flex items-baseline justify-center sm:justify-start">
<StatCounter target={40} />
</div>
<div suppressHydrationWarning className="text-[9px] sm:text-xs text-medical-gray-500 font-extrabold uppercase tracking-tight mt-1 sm:mt-2 font-vazir">
{mounted && isInitialized ? getText('hero_stat_agencies', "نمایندگی فعال") : "نمایندگی فعال"}
</div>
</div>
</div>
{/* Stat 3: German Formula */}
<div className="bg-medical-gray-50/50 border border-medical-gray-100 rounded-2xl sm:rounded-3xl p-2.5 sm:p-6 flex flex-col sm:flex-row items-center justify-center text-center sm:text-right gap-2 sm:gap-6 group hover:shadow-xl hover:shadow-canina-blue/5 transition-all">
<div className="w-9 h-9 sm:w-14 sm:h-14 bg-white rounded-xl sm:rounded-2xl flex items-center justify-center text-canina-blue shadow-sm group-hover:bg-canina-blue group-hover:text-white transition-all duration-300 flex-shrink-0">
<ShieldCheck className="w-4 h-4 sm:w-7 sm:h-7" />
</div>
<div className="flex flex-col items-center sm:items-start text-center sm:text-right">
<div className="text-base sm:text-3xl font-black text-canina-blue font-vazir leading-none">۱۰۰٪</div>
<div suppressHydrationWarning className="text-[9px] sm:text-xs text-medical-gray-500 font-extrabold uppercase tracking-tight mt-1 sm:mt-2 font-vazir">
{mounted && isInitialized ? getText('hero_stat_german_formula', "فرمول آلمانی") : "فرمول آلمانی"}
</div>
</div>
</div>
</motion.div>
</div>
</section>
);
}