"use client"; import { useCallback, useEffect, useRef, useState } from "react"; import Image from "next/image"; import type { Product } from "@/data/home"; import BilingualTitle from "@/components/ui/BilingualTitle"; import ProductCard from "./ProductCard"; import OverlayProductCard from "./OverlayProductCard"; import PromoCard, { type PromoSlotOverride } from "./PromoCard"; type ProductRowProps = { products: Product[]; promo: "sale" | "discount" | "autumn"; className?: string; /** Stagger fade-in for promo + product cards when scrolled into view */ stagger?: boolean; promoSlot?: PromoSlotOverride | null; /** * `carousel` — promo + horizontal product scroller (default) * `overlay-grid` — 2×4 image-only cards; black hover overlay with name + desc */ layout?: "carousel" | "overlay-grid"; }; const OVERLAY_GRID_TITLES: Record< ProductRowProps["promo"], { titleFa: string; titleEn: string } > = { sale: { titleFa: "فروش ویژه", titleEn: "Sale" }, discount: { titleFa: "پیشنهادهای ویژه", titleEn: "Special offers" }, autumn: { titleFa: "کالکشن پاییزه", titleEn: "Autumn collection" }, }; const CARD_W = "w-[260px] shrink-0 sm:w-[280px] lg:w-[300px]"; export default function ProductRow({ products, promo, className = "", stagger = false, promoSlot, layout = "carousel", }: ProductRowProps) { const viewportRef = useRef(null); const trackRef = useRef(null); const sectionRef = useRef(null); const offsetRef = useRef(0); const maxRef = useRef(0); const stepRef = useRef(288); const [offset, setOffset] = useState(0); const [maxOffset, setMaxOffset] = useState(0); const [visible, setVisible] = useState(!stagger); const dragRef = useRef<{ pointerId: number | null; startX: number; startOffset: number; dragging: boolean; }>({ pointerId: null, startX: 0, startOffset: 0, dragging: false }); const applyOffset = useCallback((value: number) => { const next = Math.min(maxRef.current, Math.max(0, value)); offsetRef.current = next; setOffset(next); }, []); const measure = useCallback(() => { const viewport = viewportRef.current; const track = trackRef.current; if (!viewport || !track) return; const card = track.querySelector("[data-carousel-card]"); const styles = getComputedStyle(track); const gap = parseFloat(styles.columnGap || styles.gap || "8") || 8; const cardW = card?.offsetWidth ?? 280; stepRef.current = cardW + gap; maxRef.current = Math.max(0, track.scrollWidth - viewport.clientWidth); setMaxOffset(maxRef.current); applyOffset(offsetRef.current); }, [applyOffset]); useEffect(() => { if (layout !== "carousel") return; measure(); const viewport = viewportRef.current; if (!viewport) return; const ro = new ResizeObserver(() => measure()); ro.observe(viewport); if (trackRef.current) ro.observe(trackRef.current); window.addEventListener("resize", measure); const t = window.setTimeout(measure, 200); return () => { ro.disconnect(); window.removeEventListener("resize", measure); window.clearTimeout(t); }; }, [measure, products, promo, layout]); useEffect(() => { if (!stagger) return; const el = sectionRef.current; if (!el) return; if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) { setVisible(true); return; } const io = new IntersectionObserver( ([entry]) => { if (entry.isIntersecting) { setVisible(true); io.disconnect(); } }, { threshold: 0.15, rootMargin: "0px 0px -6% 0px" }, ); io.observe(el); return () => io.disconnect(); }, [stagger]); const go = (direction: "prev" | "next") => { const delta = direction === "next" ? stepRef.current : -stepRef.current; applyOffset(offsetRef.current + delta); }; useEffect(() => { if (layout !== "carousel") return; const viewport = viewportRef.current; if (!viewport) return; const onPointerDown = (e: PointerEvent) => { if (e.button !== 0) return; const target = e.target as HTMLElement; if (target.closest("a, button")) return; dragRef.current = { pointerId: e.pointerId, startX: e.clientX, startOffset: offsetRef.current, dragging: false, }; }; const onPointerMove = (e: PointerEvent) => { const drag = dragRef.current; if (drag.pointerId !== e.pointerId) return; const dx = drag.startX - e.clientX; if (!drag.dragging) { if (Math.abs(e.clientX - drag.startX) < 8) return; drag.dragging = true; viewport.setPointerCapture(e.pointerId); if (trackRef.current) trackRef.current.style.transition = "none"; } applyOffset(drag.startOffset + dx); e.preventDefault(); }; const onPointerUp = (e: PointerEvent) => { const drag = dragRef.current; if (drag.pointerId !== e.pointerId) return; const wasDragging = drag.dragging; drag.pointerId = null; drag.dragging = false; if (trackRef.current) trackRef.current.style.transition = ""; if (wasDragging) { try { viewport.releasePointerCapture(e.pointerId); } catch { /* ignore */ } const suppress = (ev: Event) => { ev.preventDefault(); ev.stopPropagation(); viewport.removeEventListener("click", suppress, true); }; viewport.addEventListener("click", suppress, true); window.setTimeout( () => viewport.removeEventListener("click", suppress, true), 0, ); } }; viewport.addEventListener("pointerdown", onPointerDown); viewport.addEventListener("pointermove", onPointerMove, { passive: false }); viewport.addEventListener("pointerup", onPointerUp); viewport.addEventListener("pointercancel", onPointerUp); return () => { viewport.removeEventListener("pointerdown", onPointerDown); viewport.removeEventListener("pointermove", onPointerMove); viewport.removeEventListener("pointerup", onPointerUp); viewport.removeEventListener("pointercancel", onPointerUp); }; }, [applyOffset, layout]); if (layout === "overlay-grid") { const gridProducts = products.slice(0, 8); const title = OVERLAY_GRID_TITLES[promo]; return (
{gridProducts.map((product, i) => (
))}
); } return (
{products.map((product, i) => (
))}
); }