261 lines
9.6 KiB
TypeScript
261 lines
9.6 KiB
TypeScript
"use client";
|
|
|
|
import { useCallback, useEffect, useRef, useState } from "react";
|
|
import Link from "next/link";
|
|
|
|
import type { ReelCarbet } from "@/lib/reels";
|
|
import { buildSrcSet } from "@/lib/image-variants";
|
|
|
|
type Props = {
|
|
carbet: ReelCarbet;
|
|
isActive: boolean;
|
|
shouldPreload: boolean;
|
|
isFavorite: boolean;
|
|
onToggleFavorite: () => void;
|
|
};
|
|
|
|
export function ReelSlide({ carbet, isActive, shouldPreload, isFavorite, onToggleFavorite }: Props) {
|
|
const [mediaIndex, setMediaIndex] = useState(0);
|
|
const [muted, setMuted] = useState(true);
|
|
const touchStart = useRef<{ x: number; y: number } | null>(null);
|
|
const videoRef = useRef<HTMLVideoElement>(null);
|
|
|
|
const current = carbet.media[mediaIndex];
|
|
|
|
const nextMedia = useCallback(() => {
|
|
setMediaIndex((i) => (i + 1) % carbet.media.length);
|
|
}, [carbet.media.length]);
|
|
const prevMedia = useCallback(() => {
|
|
setMediaIndex((i) => (i - 1 + carbet.media.length) % carbet.media.length);
|
|
}, [carbet.media.length]);
|
|
|
|
// Auto-play/pause vidéos quand slide active
|
|
useEffect(() => {
|
|
if (!videoRef.current) return;
|
|
if (isActive && current?.type === "VIDEO") {
|
|
videoRef.current.play().catch(() => {});
|
|
} else {
|
|
videoRef.current.pause();
|
|
}
|
|
}, [isActive, current?.type, mediaIndex]);
|
|
|
|
// Reset au changement de slide (différé pour éviter cascading renders)
|
|
useEffect(() => {
|
|
if (isActive) return;
|
|
queueMicrotask(() => setMediaIndex(0));
|
|
}, [isActive]);
|
|
|
|
// Navigation clavier ← →
|
|
useEffect(() => {
|
|
if (!isActive) return;
|
|
function onKey(e: KeyboardEvent) {
|
|
const tag = (e.target as HTMLElement | null)?.tagName?.toLowerCase();
|
|
if (tag === "input" || tag === "textarea") return;
|
|
if (e.key === "ArrowRight" || e.key === "l") {
|
|
e.preventDefault();
|
|
nextMedia();
|
|
} else if (e.key === "ArrowLeft" || e.key === "h") {
|
|
e.preventDefault();
|
|
prevMedia();
|
|
}
|
|
}
|
|
window.addEventListener("keydown", onKey);
|
|
return () => window.removeEventListener("keydown", onKey);
|
|
}, [isActive, nextMedia, prevMedia]);
|
|
|
|
function onTouchStart(e: React.TouchEvent) {
|
|
const t = e.touches[0];
|
|
touchStart.current = { x: t.clientX, y: t.clientY };
|
|
}
|
|
function onTouchEnd(e: React.TouchEvent) {
|
|
if (!touchStart.current) return;
|
|
const t = e.changedTouches[0];
|
|
const dx = t.clientX - touchStart.current.x;
|
|
const dy = t.clientY - touchStart.current.y;
|
|
touchStart.current = null;
|
|
// Seuil horizontal > vertical pour considérer un swipe horizontal
|
|
if (Math.abs(dx) > 40 && Math.abs(dx) > Math.abs(dy) * 1.2) {
|
|
if (dx < 0) nextMedia();
|
|
else prevMedia();
|
|
}
|
|
}
|
|
|
|
const share = useCallback(async () => {
|
|
const url = `${window.location.origin}/carbets/${carbet.slug}`;
|
|
const title = carbet.title;
|
|
if (navigator.share) {
|
|
navigator.share({ title, url }).catch(() => {});
|
|
} else {
|
|
navigator.clipboard?.writeText(url).catch(() => {});
|
|
}
|
|
}, [carbet.slug, carbet.title]);
|
|
|
|
if (!current) return null;
|
|
|
|
return (
|
|
<div
|
|
className="relative h-full w-full bg-black"
|
|
onTouchStart={onTouchStart}
|
|
onTouchEnd={onTouchEnd}
|
|
>
|
|
{/* Média */}
|
|
<div className="absolute inset-0 flex items-center justify-center">
|
|
{current.type === "VIDEO" ? (
|
|
<video
|
|
ref={videoRef}
|
|
src={shouldPreload ? current.url : undefined}
|
|
data-src={current.url}
|
|
muted={muted}
|
|
playsInline
|
|
loop
|
|
preload={shouldPreload ? "auto" : "none"}
|
|
className="h-full w-full object-cover"
|
|
onClick={() => setMuted((m) => !m)}
|
|
/>
|
|
) : (
|
|
// eslint-disable-next-line @next/next/no-img-element
|
|
<img
|
|
src={shouldPreload ? current.url : undefined}
|
|
srcSet={shouldPreload ? buildSrcSet(current.url) : undefined}
|
|
sizes="(min-width: 768px) 800px, 100vw"
|
|
data-src={current.url}
|
|
alt={`${carbet.title} — média ${mediaIndex + 1}`}
|
|
loading={shouldPreload ? "eager" : "lazy"}
|
|
fetchPriority={shouldPreload ? "high" : "auto"}
|
|
decoding="async"
|
|
className="h-full w-full object-cover"
|
|
/>
|
|
)}
|
|
</div>
|
|
|
|
{/* Voile dégradé en bas pour lisibilité */}
|
|
<div className="pointer-events-none absolute inset-x-0 bottom-0 h-2/5 bg-gradient-to-t from-black/85 via-black/30 to-transparent" />
|
|
|
|
{/* Indicateurs progression médias (sticks en haut) */}
|
|
{carbet.media.length > 1 ? (
|
|
<div className="pointer-events-none absolute left-3 right-3 top-12 flex gap-1">
|
|
{carbet.media.map((_, i) => (
|
|
<span
|
|
key={i}
|
|
className={
|
|
"h-0.5 flex-1 rounded-full " +
|
|
(i === mediaIndex ? "bg-white" : i < mediaIndex ? "bg-white/60" : "bg-white/30")
|
|
}
|
|
/>
|
|
))}
|
|
</div>
|
|
) : null}
|
|
|
|
{/* Zones tap horizontales (50/50) sur desktop */}
|
|
<button
|
|
type="button"
|
|
onClick={prevMedia}
|
|
className="absolute inset-y-0 left-0 z-10 hidden w-1/3 cursor-default md:block"
|
|
aria-label="Média précédent"
|
|
/>
|
|
<button
|
|
type="button"
|
|
onClick={nextMedia}
|
|
className="absolute inset-y-0 right-0 z-10 hidden w-1/3 cursor-default md:block"
|
|
aria-label="Média suivant"
|
|
/>
|
|
|
|
{/* Sidebar boutons droite (mobile) */}
|
|
<div className="absolute bottom-32 right-3 z-20 flex flex-col items-center gap-4">
|
|
<button
|
|
type="button"
|
|
onClick={onToggleFavorite}
|
|
className="flex flex-col items-center text-white"
|
|
aria-label={isFavorite ? "Retirer des favoris" : "Ajouter aux favoris"}
|
|
>
|
|
<span
|
|
className={
|
|
"flex h-12 w-12 items-center justify-center rounded-full backdrop-blur transition " +
|
|
(isFavorite ? "bg-rose-500/90" : "bg-white/10 hover:bg-white/20")
|
|
}
|
|
>
|
|
<svg width="22" height="22" viewBox="0 0 24 24" fill={isFavorite ? "white" : "none"} stroke="currentColor" strokeWidth="2">
|
|
<path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z" />
|
|
</svg>
|
|
</span>
|
|
<span className="mt-0.5 text-[10px] font-semibold">Favori</span>
|
|
</button>
|
|
|
|
<button
|
|
type="button"
|
|
onClick={share}
|
|
className="flex flex-col items-center text-white"
|
|
aria-label="Partager"
|
|
>
|
|
<span className="flex h-12 w-12 items-center justify-center rounded-full bg-white/10 backdrop-blur hover:bg-white/20">
|
|
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
<circle cx="18" cy="5" r="3" />
|
|
<circle cx="6" cy="12" r="3" />
|
|
<circle cx="18" cy="19" r="3" />
|
|
<path d="M8.59 13.51 L15.42 17.49" />
|
|
<path d="M15.41 6.51 L8.59 10.49" />
|
|
</svg>
|
|
</span>
|
|
<span className="mt-0.5 text-[10px] font-semibold">Partager</span>
|
|
</button>
|
|
|
|
{current.type === "VIDEO" ? (
|
|
<button
|
|
type="button"
|
|
onClick={() => setMuted((m) => !m)}
|
|
className="flex flex-col items-center text-white"
|
|
aria-label={muted ? "Activer le son" : "Couper le son"}
|
|
>
|
|
<span className="flex h-10 w-10 items-center justify-center rounded-full bg-white/10 backdrop-blur hover:bg-white/20">
|
|
{muted ? (
|
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
<path d="M11 5L6 9H2v6h4l5 4V5z" />
|
|
<line x1="23" y1="9" x2="17" y2="15" />
|
|
<line x1="17" y1="9" x2="23" y2="15" />
|
|
</svg>
|
|
) : (
|
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
<path d="M11 5L6 9H2v6h4l5 4V5z" />
|
|
<path d="M15.54 8.46a5 5 0 0 1 0 7.07" />
|
|
</svg>
|
|
)}
|
|
</span>
|
|
</button>
|
|
) : null}
|
|
</div>
|
|
|
|
{/* Bloc info bas + CTAs */}
|
|
<div className="absolute inset-x-0 bottom-0 z-10 p-4 pb-6 text-white">
|
|
<div className="mb-2 flex items-baseline gap-2">
|
|
<h2 className="text-lg font-semibold">{carbet.title}</h2>
|
|
{carbet.averageRating !== null ? (
|
|
<span className="text-xs text-white/80">
|
|
★ {carbet.averageRating.toFixed(1)} ({carbet.reviewCount})
|
|
</span>
|
|
) : null}
|
|
</div>
|
|
<div className="mb-2 flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-white/80">
|
|
<span>📍 {carbet.river}</span>
|
|
<span>·</span>
|
|
<span>👥 jusqu'à {carbet.capacity}</span>
|
|
<span>·</span>
|
|
<span className="font-mono font-semibold text-white">{Number(carbet.nightlyPrice).toFixed(0)} € / nuit</span>
|
|
</div>
|
|
<div className="flex flex-wrap gap-2">
|
|
<Link
|
|
href={`/carbets/${carbet.slug}`}
|
|
className="rounded-full bg-white/10 px-4 py-2 text-xs font-semibold backdrop-blur hover:bg-white/20"
|
|
>
|
|
Voir la fiche
|
|
</Link>
|
|
<Link
|
|
href={`/carbets/${carbet.slug}#reserver`}
|
|
className="rounded-full bg-emerald-500 px-4 py-2 text-xs font-semibold hover:bg-emerald-400"
|
|
>
|
|
Réserver
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|