+ {/* Outside the sticky frame so it keeps moving with the page. */}
+
+
+
+ {children ? (
+
{children}
+ ) : null}
);
}
diff --git a/src/components/home/OverlayProductCard.tsx b/src/components/home/OverlayProductCard.tsx
new file mode 100644
index 0000000..8fc5d59
--- /dev/null
+++ b/src/components/home/OverlayProductCard.tsx
@@ -0,0 +1,63 @@
+"use client";
+
+import Image from "next/image";
+import Link from "next/link";
+import type { Product } from "@/data/home";
+import { productPath } from "@/lib/product-path";
+
+const FALLBACK_DESC =
+ "لورم ایپسوم متن ساختگی با تولید سادگی نامفهوم از صنعت چاپ و با استفاده از طراحان گرافیک است.";
+
+type OverlayProductCardProps = {
+ product: Product;
+ className?: string;
+};
+
+export default function OverlayProductCard({
+ product,
+ className = "",
+}: OverlayProductCardProps) {
+ const href = product.href ?? productPath(product);
+ const description = product.description?.trim() || FALLBACK_DESC;
+
+ return (
+
+
+
+
+
+
+
+ {product.nameFa}
+
+ {product.nameEn ? (
+
+ {product.nameEn}
+
+ ) : null}
+
+ {description}
+
+
+
+
+
+ );
+}
diff --git a/src/components/home/ProductCard.tsx b/src/components/home/ProductCard.tsx
index 3f9c925..8042a23 100644
--- a/src/components/home/ProductCard.tsx
+++ b/src/components/home/ProductCard.tsx
@@ -5,6 +5,9 @@ import Link from "next/link";
import type { Product } from "@/data/home";
import { productPath } from "@/lib/product-path";
+const FALLBACK_DESC =
+ "لورم ایپسوم متن ساختگی با تولید سادگی نامفهوم از صنعت چاپ و با استفاده از طراحان گرافیک است.";
+
type ProductCardProps = {
product: Product;
className?: string;
@@ -13,6 +16,10 @@ type ProductCardProps = {
export default function ProductCard({ product, className = "" }: ProductCardProps) {
const href = product.href ?? productPath(product);
const hoverSrc = product.imageHover;
+ const colors = product.colors ?? [];
+ const sizes = product.sizes ?? [];
+ const hasVars = colors.length > 0 || sizes.length > 0;
+ const description = product.description?.trim() || FALLBACK_DESC;
return (
-
+
{product.nameFa}
@@ -58,14 +65,48 @@ export default function ProductCard({ product, className = "" }: ProductCardProp
-
-
- {product.price}
+ {hasVars ? (
+
+ {sizes.length > 0 ? (
+
+ {sizes.slice(0, 5).map((size) => (
+
+ {size.labelFa}
+
+ ))}
+
+ ) : null}
+ {colors.length > 0 ? (
+
+ {colors.slice(0, 6).map((color) => (
+
+ ))}
+
+ ) : null}
- IRT
+ ) : null}
+
+
+
+ {/* Vars occupy ~1 line — clamp desc to 1 so the image height stays even */}
+
+ {description}
diff --git a/src/components/home/ProductRow.tsx b/src/components/home/ProductRow.tsx
index 64ce814..f88e260 100644
--- a/src/components/home/ProductRow.tsx
+++ b/src/components/home/ProductRow.tsx
@@ -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(null);
const trackRef = useRef(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 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
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 (
+
+
+
+ {gridProducts.map((product, i) => (
+
+
+
+ ))}
+
+
+ );
+ }
return (
-
- {!scrolled ? (
-
- ) : null}
+ <>
+
+
+
+
-
-
- M
-
-
+
-
-
+
+
+
+
+
+
+
+
+
+
+
+
setMegaOpen(false)}
+ tabs={megaTabs}
+ />
-
-
-
setMegaOpen(false)}
- tabs={megaTabs}
- />
-
-
-
+
+ {/* Matches fixed header height so page content starts below it */}
+
+ >
);
}
diff --git a/src/data/home.ts b/src/data/home.ts
index ef1505f..6d4390b 100644
--- a/src/data/home.ts
+++ b/src/data/home.ts
@@ -1,5 +1,16 @@
import { categoryPath } from "@/lib/category-path";
+export type ProductColorOption = {
+ id: string;
+ labelFa: string;
+ swatch?: string;
+};
+
+export type ProductSizeOption = {
+ id: string;
+ labelFa: string;
+};
+
export type Product = {
id: string | number;
nameFa: string;
@@ -9,6 +20,10 @@ export type Product = {
/** Second photo revealed on card hover (slides in from the left). */
imageHover?: string;
href?: string;
+ /** Short card description (2 lines); falls back to لورم ایپسوم */
+ description?: string;
+ colors?: ProductColorOption[];
+ sizes?: ProductSizeOption[];
};
export type Category = {
@@ -218,6 +233,17 @@ export const SALE_PRODUCTS: Product[] = [
price: "6.900.000",
image: "/images/home/products/coat-blue.jpg",
imageHover: "/images/home/products/coat-beige.jpg",
+ colors: [
+ { id: "blue", labelFa: "آبی", swatch: "#2f4a6e" },
+ { id: "beige", labelFa: "بژ", swatch: "#c4a882" },
+ { id: "navy", labelFa: "سرمهای", swatch: "#1b2a4a" },
+ ],
+ sizes: [
+ { id: "s", labelFa: "S" },
+ { id: "m", labelFa: "M" },
+ { id: "l", labelFa: "L" },
+ { id: "xl", labelFa: "XL" },
+ ],
},
{
id: "p2",
@@ -226,6 +252,16 @@ export const SALE_PRODUCTS: Product[] = [
price: "6.900.000",
image: "/images/home/products/coat-beige.jpg",
imageHover: "/images/home/products/coat-navy.jpg",
+ colors: [
+ { id: "beige", labelFa: "بژ", swatch: "#c4a882" },
+ { id: "cream", labelFa: "کرم", swatch: "#e8dcc8" },
+ { id: "black", labelFa: "مشکی", swatch: "#0a0a0a" },
+ ],
+ sizes: [
+ { id: "m", labelFa: "M" },
+ { id: "l", labelFa: "L" },
+ { id: "xl", labelFa: "XL" },
+ ],
},
{
id: "p3",
@@ -234,6 +270,17 @@ export const SALE_PRODUCTS: Product[] = [
price: "6.900.000",
image: "/images/home/products/coat-navy.jpg",
imageHover: "/images/home/products/coat-check.jpg",
+ colors: [
+ { id: "navy", labelFa: "سرمهای", swatch: "#1b2a4a" },
+ { id: "black", labelFa: "مشکی", swatch: "#0a0a0a" },
+ ],
+ sizes: [
+ { id: "s", labelFa: "S" },
+ { id: "m", labelFa: "M" },
+ { id: "l", labelFa: "L" },
+ { id: "xl", labelFa: "XL" },
+ { id: "xxl", labelFa: "XXL" },
+ ],
},
{
id: "p4",
@@ -242,6 +289,15 @@ export const SALE_PRODUCTS: Product[] = [
price: "6.900.000",
image: "/images/home/products/coat-check.jpg",
imageHover: "/images/home/products/coat-blue.jpg",
+ colors: [
+ { id: "check", labelFa: "چهارخانه", swatch: "#6b6358" },
+ { id: "beige", labelFa: "بژ", swatch: "#c4a882" },
+ ],
+ sizes: [
+ { id: "s", labelFa: "S" },
+ { id: "m", labelFa: "M" },
+ { id: "l", labelFa: "L" },
+ ],
},
{
id: "p5",
@@ -250,6 +306,16 @@ export const SALE_PRODUCTS: Product[] = [
price: "6.900.000",
image: "/images/home/products/coat-blue.jpg",
imageHover: "/images/home/products/coat-navy.jpg",
+ colors: [
+ { id: "blue", labelFa: "آبی", swatch: "#2f4a6e" },
+ { id: "navy", labelFa: "سرمهای", swatch: "#1b2a4a" },
+ { id: "gray", labelFa: "طوسی", swatch: "#8a8a8a" },
+ ],
+ sizes: [
+ { id: "m", labelFa: "M" },
+ { id: "l", labelFa: "L" },
+ { id: "xl", labelFa: "XL" },
+ ],
},
{
id: "p6",
@@ -258,6 +324,16 @@ export const SALE_PRODUCTS: Product[] = [
price: "6.900.000",
image: "/images/home/products/coat-beige.jpg",
imageHover: "/images/home/products/coat-check.jpg",
+ colors: [
+ { id: "beige", labelFa: "بژ", swatch: "#c4a882" },
+ { id: "cream", labelFa: "کرم", swatch: "#e8dcc8" },
+ ],
+ sizes: [
+ { id: "s", labelFa: "S" },
+ { id: "m", labelFa: "M" },
+ { id: "l", labelFa: "L" },
+ { id: "xl", labelFa: "XL" },
+ ],
},
];
diff --git a/src/lib/map-home-slots.ts b/src/lib/map-home-slots.ts
index c404290..c5cac49 100644
--- a/src/lib/map-home-slots.ts
+++ b/src/lib/map-home-slots.ts
@@ -102,15 +102,10 @@ export function mapHomeSlots(
const heroItems = slotItems(items, "hero-slider", "home.hero");
const heroSlides: HeroSlideData[] =
heroItems.length > 0
- ? heroItems.map((item, i) => ({
+ ? heroItems.map((item) => ({
id: item.id,
image: item.imageUrl || "/images/home/banners/hero.jpg",
- imagePosition:
- i === 0
- ? "object-[center_20%]"
- : i === 1
- ? "object-[center_40%]"
- : "object-[center_55%]",
+ imagePosition: "object-[center_top]",
title: item.nameFa || item.nameEn || "مانا کالکشن",
subtitle: item.nameEn || item.subtext || "",
cta: {
diff --git a/src/lib/products-api.ts b/src/lib/products-api.ts
index cf8f801..2d95d3c 100644
--- a/src/lib/products-api.ts
+++ b/src/lib/products-api.ts
@@ -53,6 +53,17 @@ const FALLBACK_IMAGE = "/images/home/products/coat-blue.jpg";
export function mapApiProductToCard(p: ApiProduct): Product {
const display = p.discountPrice && p.discountPrice > 0 ? p.discountPrice : p.price;
const gallery = p.galleryUrls?.filter(Boolean) ?? [];
+ const variantColors = (p.variants ?? []).map((v) => v.colorId).filter(Boolean);
+ const variantSizes = (p.variants ?? []).map((v) => v.sizeId).filter(Boolean);
+ const colors = uniqueOptions(
+ [...(p.category?.colors ?? []), ...variantColors],
+ "color",
+ );
+ const sizes = uniqueOptions(
+ [...(p.category?.sizes ?? []), ...variantSizes],
+ "size",
+ );
+ const description = (p.summary || p.description || "").trim() || undefined;
return {
id: p.id,
nameFa: p.nameFa || "بدون نام",
@@ -61,6 +72,16 @@ export function mapApiProductToCard(p: ApiProduct): Product {
image: p.imageUrl || gallery[0] || FALLBACK_IMAGE,
imageHover: gallery[1] || gallery[0] || p.imageUrl || undefined,
href: productPath(p),
+ description,
+ colors: colors.map((c) => ({
+ id: c.id,
+ labelFa: c.labelFa,
+ swatch: c.swatch,
+ })),
+ sizes: sizes.map((s) => ({
+ id: s.id,
+ labelFa: s.labelFa,
+ })),
};
}