Open the leather banner to the full viewport on scroll and align homepage frames.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Alireza Hassani
2026-10-07 18:26:46 +03:30
co-authored by Cursor
parent f44058bd6e
commit beac5f3b73
11 changed files with 621 additions and 193 deletions
+64 -6
View File
@@ -3,7 +3,9 @@
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 = {
@@ -13,10 +15,23 @@ type ProductRowProps = {
/** 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 CARD_W =
"w-[260px] shrink-0 sm:w-[280px] lg:w-[300px]";
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,
@@ -24,6 +39,7 @@ export default function ProductRow({
className = "",
stagger = false,
promoSlot,
layout = "carousel",
}: ProductRowProps) {
const viewportRef = useRef<HTMLDivElement>(null);
const trackRef = useRef<HTMLDivElement>(null);
@@ -66,6 +82,7 @@ export default function ProductRow({
}, [applyOffset]);
useEffect(() => {
if (layout !== "carousel") return;
measure();
const viewport = viewportRef.current;
if (!viewport) return;
@@ -79,7 +96,7 @@ export default function ProductRow({
window.removeEventListener("resize", measure);
window.clearTimeout(t);
};
}, [measure, products, promo]);
}, [measure, products, promo, layout]);
useEffect(() => {
if (!stagger) return;
@@ -108,13 +125,13 @@ export default function ProductRow({
};
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;
// Never steal pointer from links/buttons — cards are full <a> links
if (target.closest("a, button")) return;
dragRef.current = {
@@ -131,7 +148,6 @@ export default function ProductRow({
const dx = drag.startX - e.clientX;
if (!drag.dragging) {
// Wait for a real drag before capturing — plain clicks still hit <a>
if (Math.abs(e.clientX - drag.startX) < 8) return;
drag.dragging = true;
viewport.setPointerCapture(e.pointerId);
@@ -179,7 +195,49 @@ export default function ProductRow({
viewport.removeEventListener("pointerup", onPointerUp);
viewport.removeEventListener("pointercancel", onPointerUp);
};
}, [applyOffset]);
}, [applyOffset, layout]);
if (layout === "overlay-grid") {
const gridProducts = products.slice(0, 8);
const title = OVERLAY_GRID_TITLES[promo];
return (
<section
ref={sectionRef}
dir="rtl"
className={`mx-auto w-full max-w-[1600px] px-4 sm:px-6 lg:px-10 ${className}`}
>
<BilingualTitle
titleFa={promoSlot?.nameFa?.trim() || title.titleFa}
titleEn={promoSlot?.nameEn?.trim() || title.titleEn}
align="center"
size="lg"
className="mb-6 sm:mb-8"
/>
<div className="grid grid-cols-2 gap-2 sm:gap-2.5 md:grid-cols-4 lg:gap-3">
{gridProducts.map((product, i) => (
<div
key={`${promo}-overlay-${product.id}-${i}`}
className="seq-fade-item"
style={
stagger
? {
transitionDelay: visible ? `${i * 70}ms` : "0ms",
opacity: visible ? 1 : 0,
transform: visible
? "translate3d(0,0,0)"
: "translate3d(0,28px,0)",
}
: undefined
}
>
<OverlayProductCard product={product} />
</div>
))}
</div>
</section>
);
}
return (
<section