This commit is contained in:
gpatruno
2026-06-11 17:30:13 +02:00
parent 36b4acb355
commit e45e9f154e
108 changed files with 9163 additions and 167 deletions
+165
View File
@@ -0,0 +1,165 @@
"use client";
import { useCallback, useEffect, useLayoutEffect, useRef, useState } from "react";
import type { GalleryImage } from "@/generated/prisma/client";
import { WaffleMenuImage } from "./WaffleMenuImage";
const INTERVAL_MS = 5000;
const MAX_VISIBLE = 3;
/** gap-3 = 0.75rem — garder aligné avec les classes Tailwind */
const GAP_PX = 12;
const SLIDE_EASE = "cubic-bezier(0.25, 0.46, 0.45, 0.94)";
type Props = {
images: GalleryImage[];
};
/** Nombre de positions « première image à gauche » : 0 … len-3 → len-2 positions */
function positionCount(len: number) {
return Math.max(0, len - 2);
}
export function GalleryCarousel({ images }: Props) {
const viewportRef = useRef<HTMLDivElement>(null);
const [startIndex, setStartIndex] = useState(0);
const [paused, setPaused] = useState(false);
const [metrics, setMetrics] = useState({ itemW: 0, step: 0 });
const len = images.length;
const positions = positionCount(len);
useLayoutEffect(() => {
const el = viewportRef.current;
if (!el) return;
function measure() {
const node = viewportRef.current;
if (!node) return;
const w = node.clientWidth;
if (w <= 0) return;
const itemW = (w - 2 * GAP_PX) / MAX_VISIBLE;
const step = itemW + GAP_PX;
setMetrics((m) =>
m.itemW === itemW && m.step === step ? m : { itemW, step }
);
}
measure();
const ro = new ResizeObserver(measure);
ro.observe(el);
return () => ro.disconnect();
}, []);
const advance = useCallback(() => {
if (len <= MAX_VISIBLE || positions <= 1) return;
setStartIndex((i) => (i + 1) % positions);
}, [len, positions]);
useEffect(() => {
if (len <= MAX_VISIBLE || paused || positions <= 1) return undefined;
const t = window.setInterval(advance, INTERVAL_MS);
return () => window.clearInterval(t);
}, [len, paused, positions, advance]);
useEffect(() => {
if (startIndex >= positions) setStartIndex(0);
}, [startIndex, positions]);
function goToPosition(i: number) {
if (i < 0 || i >= positions || i === startIndex) return;
setStartIndex(i);
}
if (len === 0) {
return (
<p className="rounded-3xl border border-dashed border-chocolat/20 bg-cream/50 px-6 py-12 text-center text-muted">
Photos à venir ajoutez-en depuis ladministration (section Galerie).
</p>
);
}
if (len <= MAX_VISIBLE) {
const gridClass =
len === 1
? "mx-auto grid max-w-md grid-cols-1 gap-3 sm:gap-4"
: len === 2
? "mx-auto grid max-w-2xl grid-cols-2 gap-3 sm:gap-4"
: "grid grid-cols-1 gap-3 sm:grid-cols-3 sm:gap-4";
return (
<div className={gridClass}>
{images.map((img) => (
<figure
key={img.id}
className="group relative aspect-square overflow-hidden rounded-3xl shadow-[0_10px_40px_-10px_rgba(61,43,31,0.2)]"
>
<div className="relative h-full w-full origin-center transition-transform duration-700 ease-[cubic-bezier(0.22,1,0.36,1)] will-change-transform group-hover:scale-[1.035]">
<WaffleMenuImage src={img.imageUrl} alt={img.alt} />
</div>
</figure>
))}
</div>
);
}
const translateX = metrics.step > 0 ? -startIndex * metrics.step : 0;
return (
<div
className="relative"
onMouseEnter={() => setPaused(true)}
onMouseLeave={() => setPaused(false)}
>
<div
ref={viewportRef}
className="overflow-hidden rounded-3xl"
>
<div
className="flex flex-row flex-nowrap gap-3 will-change-transform"
style={{
transform: `translateX(${translateX}px)`,
transition: `transform 650ms ${SLIDE_EASE}`,
}}
>
{images.map((img) => (
<div
key={img.id}
className="shrink-0"
style={
metrics.itemW > 0
? { width: metrics.itemW, minWidth: metrics.itemW }
: { width: "calc((100% - 24px) / 3)", minWidth: "calc((100% - 24px) / 3)" }
}
>
<figure className="group relative aspect-square overflow-hidden rounded-3xl shadow-[0_10px_40px_-10px_rgba(61,43,31,0.2)]">
<div className="relative h-full w-full origin-center transition-transform duration-700 ease-[cubic-bezier(0.22,1,0.36,1)] will-change-transform group-hover:scale-[1.035]">
<WaffleMenuImage src={img.imageUrl} alt={img.alt} />
</div>
</figure>
</div>
))}
</div>
</div>
{positions > 1 ? (
<div className="mt-6 flex flex-wrap justify-center gap-1.5">
{Array.from({ length: positions }).map((_, i) => (
<button
key={`dot-${i}`}
type="button"
aria-label={`Position ${i + 1} sur ${positions}`}
aria-current={i === startIndex}
onClick={() => goToPosition(i)}
className={`h-2 rounded-full transition-all duration-500 ease-out ${
i === startIndex
? "w-8 bg-gaufre"
: "w-2 bg-chocolat/30 hover:bg-chocolat/50"
}`}
/>
))}
</div>
) : null}
</div>
);
}