update
This commit is contained in:
@@ -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 l’administration (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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user