207 lines
7.8 KiB
TypeScript
207 lines
7.8 KiB
TypeScript
import { ChevronLeft, ChevronRight } from "lucide-react";
|
|
import { useEffect, useRef, useState } from "react";
|
|
|
|
import alimentationJpg from "&/images/engagements/alimentation.jpg";
|
|
import alimentationWebp from "&/images/engagements/alimentation.webp";
|
|
import controlJpg from "&/images/engagements/controles-independants.jpg";
|
|
import controlWebp from "&/images/engagements/controles-independants.webp";
|
|
import enrichissementsJpg from "&/images/engagements/enrichissements.jpg";
|
|
import enrichissementsWebp from "&/images/engagements/enrichissements.webp";
|
|
import espaceDeViePng from "&/images/engagements/espace-de-vie.jpg";
|
|
import espaceDeVieWebp from "&/images/engagements/espace-de-vie.webp";
|
|
import farmersJpg from "&/images/engagements/formation-eleveurs.jpg";
|
|
import farmersWebp from "&/images/engagements/formation-eleveurs.webp";
|
|
import monitoringJpg from "&/images/engagements/surveillance-renforcee.jpg";
|
|
import monitoringWebp from "&/images/engagements/surveillance-renforcee.webp";
|
|
import useDeviceType from "@/hooks/use-device-type";
|
|
import { GetTranslation } from "@/lib/utils";
|
|
|
|
const ENGAGEMENTS = [
|
|
{
|
|
webp: espaceDeVieWebp,
|
|
fallback: espaceDeViePng,
|
|
title: "front.engagements.livingSpace.title",
|
|
text: "front.engagements.livingSpace.text",
|
|
imgAlt: "front.engagements.livingSpace.imgAlt",
|
|
imgTitle: "front.engagements.livingSpace.imgTitle",
|
|
},
|
|
{
|
|
webp: enrichissementsWebp,
|
|
fallback: enrichissementsJpg,
|
|
title: "front.engagements.enrichments.title",
|
|
text: "front.engagements.enrichments.text",
|
|
imgAlt: "front.engagements.enrichments.imgAlt",
|
|
imgTitle: "front.engagements.enrichments.imgTitle",
|
|
},
|
|
{
|
|
webp: alimentationWebp,
|
|
fallback: alimentationJpg,
|
|
title: "front.engagements.feeding.title",
|
|
text: "front.engagements.feeding.text",
|
|
imgAlt: "front.engagements.feeding.imgAlt",
|
|
imgTitle: "front.engagements.feeding.imgTitle",
|
|
},
|
|
{
|
|
webp: monitoringWebp,
|
|
fallback: monitoringJpg,
|
|
title: "front.engagements.monitoring.title",
|
|
text: "front.engagements.monitoring.text",
|
|
imgAlt: "front.engagements.monitoring.imgAlt",
|
|
imgTitle: "front.engagements.monitoring.imgTitle",
|
|
},
|
|
{
|
|
webp: farmersWebp,
|
|
fallback: farmersJpg,
|
|
title: "front.engagements.trainingFarmers.title",
|
|
text: "front.engagements.trainingFarmers.text",
|
|
imgAlt: "front.engagements.trainingFarmers.imgAlt",
|
|
imgTitle: "front.engagements.trainingFarmers.imgTitle",
|
|
},
|
|
{
|
|
webp: controlWebp,
|
|
fallback: controlJpg,
|
|
title: "front.engagements.control.title",
|
|
text: "front.engagements.control.text",
|
|
imgAlt: "front.engagements.control.imgAlt",
|
|
imgTitle: "front.engagements.control.imgTitle",
|
|
linkLabel: "front.engagements.control.linkLabel",
|
|
textAfter: "front.engagements.control.text_after",
|
|
linkUrl: "https://certipaq.com",
|
|
},
|
|
];
|
|
|
|
export default function EngagementsSection() {
|
|
const scrollerRef = useRef<HTMLDivElement>(null);
|
|
const cardRefs = useRef<(HTMLDivElement | null)[]>([]);
|
|
const [activeIndex, setActiveIndex] = useState(0);
|
|
const { isMobile } = useDeviceType();
|
|
|
|
const visibleCount = isMobile ? 1 : 3;
|
|
const dotsCount = Math.max(ENGAGEMENTS.length - visibleCount + 1, 1);
|
|
|
|
useEffect(() => {
|
|
const scroller = scrollerRef.current;
|
|
if (!scroller) return;
|
|
|
|
const handleScroll = () => {
|
|
const maxScroll = scroller.scrollWidth - scroller.clientWidth;
|
|
const ratio = maxScroll > 0 ? scroller.scrollLeft / maxScroll : 0;
|
|
const index = Math.round(ratio * (dotsCount - 1));
|
|
setActiveIndex(index);
|
|
};
|
|
|
|
scroller.addEventListener("scroll", handleScroll, { passive: true });
|
|
handleScroll();
|
|
return () => {
|
|
scroller.removeEventListener("scroll", handleScroll);
|
|
};
|
|
}, [dotsCount]);
|
|
|
|
const scrollToIndex = (index: number) => {
|
|
const clamped = Math.max(0, Math.min(index, dotsCount - 1));
|
|
const card = cardRefs.current[clamped];
|
|
const scroller = scrollerRef.current;
|
|
if (!card || !scroller) return;
|
|
|
|
scroller.scrollTo({ left: card.offsetLeft, behavior: "smooth" });
|
|
};
|
|
|
|
return (
|
|
<section
|
|
id={GetTranslation("front.engagements.slug")}
|
|
className="relative w-full overflow-hidden bg-white px-4 py-14 md:px-12 lg:px-[60px]"
|
|
>
|
|
<div className="mx-auto max-w-[1320px] text-center">
|
|
<h2 className="font-sans text-3xl lg:text-[32px] font-extrabold text-neutral-800 uppercase">
|
|
{GetTranslation("front.engagements.title")}
|
|
</h2>
|
|
<p className="mx-auto mt-3 max-w-[820px] font-sans text-[22px] leading-relaxed text-neutral-700">
|
|
{GetTranslation("front.engagements.subtitle")}
|
|
</p>
|
|
|
|
<div className="relative mt-10">
|
|
<button
|
|
onClick={() => {
|
|
scrollToIndex(activeIndex - 1);
|
|
}}
|
|
aria-label={GetTranslation("front.slides.previous")}
|
|
className="absolute left-0 top-[110px] cursor-pointer z-10 -translate-x-1/2 p-2.5 text-band transition-colors hover:text-black md:-left-6"
|
|
>
|
|
<ChevronLeft size={52} />
|
|
</button>
|
|
<div
|
|
ref={scrollerRef}
|
|
className="flex snap-x snap-mandatory gap-6 overflow-x-auto scroll-smooth px-4 pb-2 md:gap-6 md:px-0"
|
|
style={{ scrollbarWidth: "none" }}
|
|
>
|
|
{ENGAGEMENTS.map((item, index) => (
|
|
<div
|
|
key={item.title}
|
|
ref={(el) => {
|
|
cardRefs.current[index] = el;
|
|
}}
|
|
className="flex w-[calc(100%-0rem)] flex-none snap-center flex-col items-center gap-3 md:w-[calc((100%-4rem)/3)]"
|
|
>
|
|
<picture>
|
|
<source srcSet={item.webp} type="image/webp" />
|
|
<img
|
|
src={item.fallback}
|
|
alt={GetTranslation(item.imgAlt)}
|
|
title={GetTranslation(item.imgTitle)}
|
|
className="h-[280px] w-full rounded-2xl object-cover md:h-[220px]"
|
|
/>
|
|
</picture>
|
|
<h3 className="font-sans text-[26px] font-bold text-neutral-800">
|
|
{GetTranslation(item.title)}
|
|
</h3>
|
|
<p className="font-sans text-[18px] leading-relaxed text-neutral-700">
|
|
{GetTranslation(item.text)}{" "}
|
|
{item.linkLabel && item.textAfter && (
|
|
<>
|
|
{" "}
|
|
<a
|
|
href={item.linkUrl}
|
|
target="_blank"
|
|
rel="noreferrer noopener"
|
|
className="font-bold underline"
|
|
>
|
|
{GetTranslation(item.linkLabel)}
|
|
</a>{" "}
|
|
{GetTranslation(item.textAfter)}
|
|
</>
|
|
)}
|
|
</p>
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
<button
|
|
onClick={() => {
|
|
scrollToIndex(activeIndex + 1);
|
|
}}
|
|
aria-label={GetTranslation("front.slides.next")}
|
|
className="absolute right-0 top-[110px] cursor-pointer z-10 translate-x-1/2 p-2.5 text-band transition-colors hover:text-black md:-right-6"
|
|
>
|
|
<ChevronRight size={52} />
|
|
</button>
|
|
</div>
|
|
|
|
<div className="mt-6 flex items-center justify-center gap-2">
|
|
{Array.from({ length: dotsCount }).map((_, index) => (
|
|
<button
|
|
key={index}
|
|
onClick={() => {
|
|
scrollToIndex(index);
|
|
}}
|
|
className={`rounded-full transition-all duration-300 ${
|
|
index === activeIndex
|
|
? "h-5 w-5 bg-neutral-400"
|
|
: "h-3.5 w-3.5 bg-band"
|
|
}`}
|
|
/>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</section>
|
|
);
|
|
}
|