diff --git a/public/images/landscapes/christian-gertenbach-AqCVniFulhY-unsplash.jpg b/public/images/landscapes/christian-gertenbach-AqCVniFulhY-unsplash.jpg new file mode 100644 index 0000000..b955927 Binary files /dev/null and b/public/images/landscapes/christian-gertenbach-AqCVniFulhY-unsplash.jpg differ diff --git a/public/images/landscapes/frame1.jpg b/public/images/landscapes/frame1.jpg new file mode 100644 index 0000000..fecc482 Binary files /dev/null and b/public/images/landscapes/frame1.jpg differ diff --git a/public/images/landscapes/ian-dooley-C6BrEIucDDU-unsplash.jpg b/public/images/landscapes/ian-dooley-C6BrEIucDDU-unsplash.jpg new file mode 100644 index 0000000..e628cbe Binary files /dev/null and b/public/images/landscapes/ian-dooley-C6BrEIucDDU-unsplash.jpg differ diff --git a/public/images/landscapes/mojtaba-mosayebzadeh-1nLTyduE1cY-unsplash.jpg b/public/images/landscapes/mojtaba-mosayebzadeh-1nLTyduE1cY-unsplash.jpg new file mode 100644 index 0000000..77dd8f7 Binary files /dev/null and b/public/images/landscapes/mojtaba-mosayebzadeh-1nLTyduE1cY-unsplash.jpg differ diff --git a/public/images/landscapes/mojtaba-mosayebzadeh-WvFy1eFAxjM-unsplash.jpg b/public/images/landscapes/mojtaba-mosayebzadeh-WvFy1eFAxjM-unsplash.jpg new file mode 100644 index 0000000..e47bfc1 Binary files /dev/null and b/public/images/landscapes/mojtaba-mosayebzadeh-WvFy1eFAxjM-unsplash.jpg differ diff --git a/public/images/landscapes/tamara-bellis-nbC4EyCxhb8-unsplash.jpg b/public/images/landscapes/tamara-bellis-nbC4EyCxhb8-unsplash.jpg new file mode 100644 index 0000000..b5930da Binary files /dev/null and b/public/images/landscapes/tamara-bellis-nbC4EyCxhb8-unsplash.jpg differ diff --git a/public/images/landscapes/trafa-thomas-IcmwLRBRoVE-unsplash.jpg b/public/images/landscapes/trafa-thomas-IcmwLRBRoVE-unsplash.jpg new file mode 100644 index 0000000..a0574cc Binary files /dev/null and b/public/images/landscapes/trafa-thomas-IcmwLRBRoVE-unsplash.jpg differ diff --git a/public/images/landscapes/vinicius-amnx-amano-k2yixQS_YH0-unsplash.jpg b/public/images/landscapes/vinicius-amnx-amano-k2yixQS_YH0-unsplash.jpg new file mode 100644 index 0000000..1d642e9 Binary files /dev/null and b/public/images/landscapes/vinicius-amnx-amano-k2yixQS_YH0-unsplash.jpg differ diff --git a/public/images/transparent/bag.png b/public/images/transparent/bag.png new file mode 100644 index 0000000..3527aa9 Binary files /dev/null and b/public/images/transparent/bag.png differ diff --git a/public/images/transparent/mantel.png b/public/images/transparent/mantel.png new file mode 100644 index 0000000..3e26934 Binary files /dev/null and b/public/images/transparent/mantel.png differ diff --git a/public/images/transparent/pants.png b/public/images/transparent/pants.png new file mode 100644 index 0000000..e985142 Binary files /dev/null and b/public/images/transparent/pants.png differ diff --git a/public/images/transparent/scarf.png b/public/images/transparent/scarf.png new file mode 100644 index 0000000..f01dcad Binary files /dev/null and b/public/images/transparent/scarf.png differ diff --git a/public/images/transparent/shirt.png b/public/images/transparent/shirt.png new file mode 100644 index 0000000..e6cfba5 Binary files /dev/null and b/public/images/transparent/shirt.png differ diff --git a/public/images/transparent/shoe.png b/public/images/transparent/shoe.png new file mode 100644 index 0000000..5654d83 Binary files /dev/null and b/public/images/transparent/shoe.png differ diff --git a/src/app/about/page.tsx b/src/app/about/page.tsx index 03bc633..99c0d60 100644 --- a/src/app/about/page.tsx +++ b/src/app/about/page.tsx @@ -32,7 +32,7 @@ export default async function AboutPage() { ); return ( -
+
({ image: t.image })), + ); + + return ( +
+
+ + +
+ +
+

+ سبد خرید شما خالی است +

+

+ محصولی به سبد اضافه نشده است. از کالکشن‌ها دیدن کنید و انتخاب خود را + شروع کنید. +

+ + مشاهده محصولات + +
+ + + +
+ ); +} diff --git a/src/app/categories/[...path]/page.tsx b/src/app/categories/[...path]/page.tsx index 35f658a..e14462c 100644 --- a/src/app/categories/[...path]/page.tsx +++ b/src/app/categories/[...path]/page.tsx @@ -174,7 +174,7 @@ export default async function CategoryPage({ params, searchParams }: PageProps) ); return ( -
+
+
+
0, - ); - return ( -
+
- {/* */} - + {/* Curated bento art from website slots (with local transparent defaults). */} +
+ + + +
); diff --git a/src/app/products/[...path]/page.tsx b/src/app/products/[...path]/page.tsx index e5c3757..d1dad82 100644 --- a/src/app/products/[...path]/page.tsx +++ b/src/app/products/[...path]/page.tsx @@ -89,7 +89,7 @@ export default async function ProductDetailPage({ params }: PageProps) { .slice(0, 8); return ( -
+
+
+
{lead}

-

+

{body}

diff --git a/src/components/home/CategoryBento.tsx b/src/components/home/CategoryBento.tsx index 6cb6e59..9aab4be 100644 --- a/src/components/home/CategoryBento.tsx +++ b/src/components/home/CategoryBento.tsx @@ -3,23 +3,105 @@ import Image from "next/image"; import Link from "next/link"; import { useEffect, useRef, useState } from "react"; -import { CATEGORIES, type Category } from "@/data/home"; +import { type Category } from "@/data/home"; +import { categoryPath } from "@/lib/category-path"; + +type TileLayout = "stack" | "beside"; + +type BentoTile = { + id: string; + nameFa: string; + nameEn: string; + image: string; + href: string; + layout: TileLayout; + className: string; +}; + +/** Fixed bento art + layout; href/names can be overridden from API categories. */ +const BENTO_TILES: BentoTile[] = [ + { + id: "coat", + nameFa: "مانتو", + nameEn: "Mantel", + image: "/images/transparent/mantel.png", + href: categoryPath({ id: "coat", nameEn: "Coat", nameFa: "کت" }), + layout: "stack", + className: "col-span-2 col-start-1 row-span-2 row-start-1", + }, + { + id: "shirts", + nameFa: "پیراهن", + nameEn: "Shirts", + image: "/images/transparent/shirt.png", + href: categoryPath({ id: "shirts", nameEn: "Shirts", nameFa: "پیراهن" }), + layout: "beside", + className: "col-span-4 col-start-3 row-start-1", + }, + { + id: "pants", + nameFa: "شلوار", + nameEn: "Pants", + image: "/images/transparent/pants.png", + href: categoryPath({ id: "pants", nameEn: "Pants", nameFa: "شلوار" }), + layout: "beside", + className: "col-span-2 col-start-3 row-start-2", + }, + { + id: "bags", + nameFa: "کیف زنانه", + nameEn: "Wemon Bag", + image: "/images/transparent/bag.png", + href: categoryPath({ id: "bags", nameEn: "Bags", nameFa: "کیف" }), + layout: "beside", + className: "col-span-2 col-start-5 row-start-2", + }, + { + id: "scarf", + nameFa: "شال", + nameEn: "Scarf", + image: "/images/transparent/scarf.png", + href: categoryPath({ id: "scarf", nameEn: "Scarf", nameFa: "شال" }), + layout: "beside", + className: "col-span-3 col-start-1 row-start-3", + }, + { + id: "shoes", + nameFa: "کفش", + nameEn: "Shoe", + image: "/images/transparent/shoe.png", + href: categoryPath({ id: "shoes", nameEn: "Shoe", nameFa: "کفش" }), + layout: "beside", + className: "col-span-3 col-start-4 row-start-3", + }, +]; type CategoryBentoProps = { categories?: Category[]; }; +const TITLE_FA_WORDS = ["دسته", "بندی", "محصولات", "ما"] as const; + +/** Match PNG proportions so the box hugs the product (no huge empty sides). */ +function imageAspect(id: string): string { + if (id === "shoes" || id === "scarf") return "1 / 1"; + if (id === "coat") return "9 / 16"; + return "2 / 3"; +} + function CategoryTile({ - category, + tile, className = "", index, visible, }: { - category: Category; + tile: BentoTile; className?: string; index: number; visible: boolean; }) { + const isStack = tile.layout === "stack"; + return (
- {category.nameFa + {/* Tight cluster: image width follows aspect ratio, label sits against it */} +
+
+ {tile.nameFa +
+ +
+

+ {tile.nameFa} +

+

+ {tile.nameEn} +

+
+
); } /** - * Layout (width × height): - * 1×3 - 2×1 - * 1×1 - 1×2 - * 1×2 - 1×1 - 1×1 + * Desktop grid is 6×3 equal cells inside a 2:1 frame. + * Mantel covers the top two rows. Scarf and shoe share the full bottom row. + * + * Tile aspect ratios (width:height) for image design: + * - Mantel stack (2×2 of 6): 1:1 → 800×800 + * - Wide beside (4×1 of 6): 4:1 → 1600×400 + * - Short beside (2×1 of 6): 2:1 → 800×400 + * - Bottom row (3×1 of 6): 3:1 → 1200×400 + * + * Six tiles: mantel, shirts, pants, bag, scarf, shoe. */ export default function CategoryBento({ categories }: CategoryBentoProps) { - const source = categories?.length ? categories : CATEGORIES; - const at = (i: number) => source[i % source.length]!; + const tiles = BENTO_TILES.map((tile, index) => { + const match = + categories?.find((c) => c.id === tile.id) ?? categories?.[index]; + if (!match) return tile; + const slotImage = match.image?.includes("/images/transparent/") + ? match.image + : null; + return { + ...tile, + nameFa: match.nameFa || tile.nameFa, + nameEn: match.nameEn || tile.nameEn, + href: match.href || tile.href, + image: slotImage || tile.image, + }; + }); + const ref = useRef(null); const [visible, setVisible] = useState(false); @@ -67,16 +205,24 @@ export default function CategoryBento({ categories }: CategoryBentoProps) { setVisible(true); return; } + const reveal = () => setVisible(true); const io = new IntersectionObserver( ([entry]) => { if (entry.isIntersecting) { - setVisible(true); + reveal(); io.disconnect(); } }, - { threshold: 0.15, rootMargin: "0px 0px -6% 0px" }, + { threshold: 0.08, rootMargin: "0px 0px -4% 0px" }, ); io.observe(el); + // Sticky story frames can delay the first IO callback; catch already-in-view. + const rect = el.getBoundingClientRect(); + const vh = window.innerHeight || 0; + if (rect.top < vh * 0.92 && rect.bottom > vh * 0.08) { + reveal(); + io.disconnect(); + } return () => io.disconnect(); }, []); @@ -85,53 +231,72 @@ export default function CategoryBento({ categories }: CategoryBentoProps) { ref={ref} className="mx-auto w-full max-w-[1600px] px-4 pt-12 pb-6 sm:px-6 sm:pt-16 sm:pb-8 lg:px-10 lg:pt-20 lg:pb-10" > +
+

+ {TITLE_FA_WORDS.map((word, i) => ( + + {word} + + ))} +

+

+ Our Product Categories +

+
+
- {source.slice(0, 5).map((category, i) => ( - - ))} + {tiles.map((tile, i) => { + const isWideMobile = + tile.id === "shoes" || tile.id === "scarf"; + return ( + + ); + })}
-
- - - - - +
+ {tiles.map((tile, i) => ( + + ))}
diff --git a/src/components/home/HeroSlider.tsx b/src/components/home/HeroSlider.tsx index f201ae7..4a5009e 100644 --- a/src/components/home/HeroSlider.tsx +++ b/src/components/home/HeroSlider.tsx @@ -133,7 +133,7 @@ export default function HeroSlider({ slides }: HeroSliderProps) { ) : null} {slide.cta.label ? (
- + {slide.cta.label}
diff --git a/src/components/home/HoverFillButton.tsx b/src/components/home/HoverFillButton.tsx index 4abdeae..eafa349 100644 --- a/src/components/home/HoverFillButton.tsx +++ b/src/components/home/HoverFillButton.tsx @@ -19,7 +19,7 @@ export default function HoverFillButton({ const isDark = variant === "dark"; const isExternal = /^https?:\/\//i.test(href); - const classNames = `group relative inline-flex h-[52px] items-center justify-between gap-3 overflow-hidden rounded-[5px] border px-4 backdrop-blur-sm sm:h-[56px] ${ + const classNames = `group relative inline-flex h-9 w-[11.5rem] max-w-full items-center justify-between gap-0 overflow-hidden rounded-[4px] border px-3 backdrop-blur-sm sm:h-10 sm:w-[12.5rem] sm:px-3.5 ${ isDark ? "border-white bg-transparent text-white" : "border-ink bg-white/30 text-ink" @@ -34,7 +34,7 @@ export default function HoverFillButton({ }`} /> @@ -47,10 +47,10 @@ export default function HoverFillButton({ : "/images/home/icons/caret-left-dark.svg" } alt="" - width={28} - height={28} + width={16} + height={16} unoptimized - className="relative z-10 transition-[filter] duration-[400ms] ease-out group-hover:invert" + className="relative z-10 size-3.5 shrink-0 transition-[filter] duration-[400ms] ease-out group-hover:invert sm:size-4" /> ); diff --git a/src/components/home/LeatherBanner.tsx b/src/components/home/LeatherBanner.tsx index 66215cf..0526107 100644 --- a/src/components/home/LeatherBanner.tsx +++ b/src/components/home/LeatherBanner.tsx @@ -1,13 +1,5 @@ -"use client"; - import Image from "next/image"; -import { - type ReactNode, - useEffect, - useEffectEvent, - useRef, - useState, -} from "react"; +import type { ReactNode } from "react"; import HoverFillButton from "./HoverFillButton"; type LeatherBannerProps = { @@ -19,10 +11,6 @@ type LeatherBannerProps = { children?: ReactNode; }; -function bannerHeight(width: number) { - return Math.min(Math.max(width, 0), 1920) * (585 / 1920); -} - export default function LeatherBanner({ image = "/images/home/banners/leather.png", nameFa = "هنر پوشیدن", @@ -31,11 +19,6 @@ export default function LeatherBanner({ link = "/products?collection=leather", children, }: LeatherBannerProps) { - const trackRef = useRef(null); - const [bannerH, setBannerH] = useState(0); - const [vh, setVh] = useState(0); - const [progress, setProgress] = useState(0); - const enLines = nameEn.trim().split(/\n+/).filter(Boolean); const line1 = enLines[0] ?? "THE ART"; const line2 = enLines[1] ?? (enLines.length === 1 ? "" : "OF DRESSING"); @@ -48,105 +31,70 @@ export default function LeatherBanner({ })() : [line1, line2 || "OF DRESSING"]; - const update = useEffectEvent(() => { - const track = trackRef.current; - if (!track) return; - - const nextVh = window.innerHeight; - const nextBannerH = bannerHeight(track.clientWidth); - setVh(nextVh); - setBannerH(nextBannerH); - - if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) { - setProgress(0); - return; - } - - const rect = track.getBoundingClientRect(); - const grow = Math.max(1, nextVh - nextBannerH); - // 0 when the frame sits on the viewport bottom, 1 when it fills the screen. - const start = nextVh - nextBannerH; - const scrolled = Math.min(grow, Math.max(0, start - rect.top)); - setProgress(scrolled / grow); - }); - - useEffect(() => { - update(); - window.addEventListener("scroll", update, { passive: true }); - window.addEventListener("resize", update); - return () => { - window.removeEventListener("scroll", update); - window.removeEventListener("resize", update); - }; - }, []); - - const frameH = - bannerH > 0 ? bannerH + progress * Math.max(0, vh - bannerH) : 0; - // Subject travels as the frame opens. - const imageY = 78 - progress * 48; + const copy = ( +
+

+ {display1} +

+ {display2 ? ( +

+ {display2} +

+ ) : null} +

+ {nameFa} +

+ {linkText ? ( + + {linkText} + + ) : null} +
+ ); return ( -
-
+
+ {/* Mobile: text first, taller crop pinned to the subject on the left */} +
+
+ {copy} +
+
+ {nameFa} +
+
+ + {/* Desktop: text over wide banner */} +
{nameFa} -
- {/* Outside the sticky frame so it keeps moving with the page. */} -
-
-
-

- {display1} -

- {display2 ? ( -

- {display2} -

- ) : null} -

- {nameFa} -

- {linkText ? ( - - {linkText} - - ) : null} -
+
+ {copy}
- {children ? ( -
{children}
- ) : null} + {children}
); } diff --git a/src/components/home/OpeningFrame.tsx b/src/components/home/OpeningFrame.tsx new file mode 100644 index 0000000..ac0326c --- /dev/null +++ b/src/components/home/OpeningFrame.tsx @@ -0,0 +1,136 @@ +import Image from "next/image"; +import HoverFillButton from "./HoverFillButton"; + +type Highlight = { + kicker: string; + value: string; + caption: string; +}; + +type Detail = { + title: string; + body: string; +}; + +type OpeningFrameProps = { + image?: string; + imageAlt?: string; + eyebrow?: string; + headline?: string; + body?: string; + highlights?: Highlight[]; + details?: Detail[]; + cta?: { label: string; href: string }; +}; + +const DEFAULT_HIGHLIGHTS: Highlight[] = [ + { kicker: "تمرکز روی", value: "دوخت", caption: "و جزئیات پارچه" }, + { kicker: "ساخته برای", value: "ماندگاری", caption: "در استفاده روزمره" }, +]; + +const DEFAULT_DETAILS: Detail[] = [ + { + title: "کیفیت ساخت", + body: "پارچه، دوخت و فیتینگ برای لباسی که هر روز پوشیده می‌شود.", + }, + { + title: "برش معاصر", + body: "خطوطی که راحت می‌نشینند و شکل خود را حفظ می‌کنند.", + }, + { + title: "جزئیات ماندگار", + body: "از یراق تا آستر، هر بخش با دقت انتخاب شده است.", + }, +]; + +export default function OpeningFrame({ + image = "/images/landscapes/frame1.jpg", + imageAlt = "پرتره مد مانا کالکشن", + eyebrow = "هنر پوشیدن", + headline = "به سبک مانا", + body = "هر قطعه مانا از نزدیک معنا پیدا می‌کند. پارچه، دوخت و برش طوری انتخاب شده‌اند که هم برای پوشیدن روزمره راحت باشند و هم سال‌ها شکل خود را حفظ کنند.", + highlights = DEFAULT_HIGHLIGHTS, + details = DEFAULT_DETAILS, + cta = { label: "مشاهده کالکشن", href: "/products" }, +}: OpeningFrameProps) { + return ( +
+
+
+ {imageAlt} +
+ +
+
+ +
+
+

+ {eyebrow} +

+

+ {headline} +

+
+
+ +
+
+

+ {body} +

+ +
+
+ {highlights.map((item) => ( +
+

+ {item.kicker} +

+

+ {item.value} +

+

+ {item.caption} +

+
+ ))} +
+ +
    + {details.map((item) => ( +
  • +

    + {item.title} +

    +

    + {item.body} +

    +
  • + ))} +
+
+ + {cta?.label ? ( +
+ + {cta.label} + +
+ ) : null} +
+
+
+
+ ); +} diff --git a/src/components/home/PartnerCta.tsx b/src/components/home/PartnerCta.tsx index 2c4df21..878d4fa 100644 --- a/src/components/home/PartnerCta.tsx +++ b/src/components/home/PartnerCta.tsx @@ -4,7 +4,7 @@ import { STORE_URL } from "@/lib/public-api"; export default function PartnerCta() { return (
-
+

مایل هستید ما محصول شما را برای شما بفروشیم؟ @@ -17,7 +17,7 @@ export default function PartnerCta() { شروع به همکاری diff --git a/src/components/home/ProductRow.tsx b/src/components/home/ProductRow.tsx index f88e260..0c2a67f 100644 --- a/src/components/home/ProductRow.tsx +++ b/src/components/home/ProductRow.tsx @@ -239,15 +239,26 @@ export default function ProductRow({ ); } + const carouselTitle = OVERLAY_GRID_TITLES[promo]; + return (

+
+ +
+
, + document.body, + ) + : null; + return (
-
-

- فیلترها -

- -
- - - -
- -
- - -
+ + + {chips}
- {active.length > 0 ? ( -
    - {active.map((chip) => ( -
  • - -

    - {chip.label} - : - {chip.value} -

    -
  • - ))} -
- ) : null} + {/* Desktop / tablet: inline filters */} +
+
+

+ فیلترها +

+ {fields} + {actions} +
+ {chips} +
+ + {modal}
); } diff --git a/src/components/products/ProductGrid.tsx b/src/components/products/ProductGrid.tsx index 8cec932..8588e0e 100644 --- a/src/components/products/ProductGrid.tsx +++ b/src/components/products/ProductGrid.tsx @@ -9,7 +9,7 @@ export default function ProductGrid({ products }: ProductGridProps) { return (
    {products.map((product) => (
  • diff --git a/src/data/home.ts b/src/data/home.ts index e11f9f1..d30fa62 100644 --- a/src/data/home.ts +++ b/src/data/home.ts @@ -431,3 +431,42 @@ export const SOCIAL_LINKS = [ { id: "whatsapp", href: "#", icon: "/images/home/social/whatsapp.svg", label: "واتساپ" }, { id: "linkedin", href: "#", icon: "/images/home/social/linkedin.svg", label: "لینکدین" }, ] as const; + +export const STORY_STEPS = [ + { + en: "Mantel", + fa: "مانتو", + text: "مانتوهایی با برش معاصر؛ ساده، شهری و مناسب هر روز.", + src: "/images/landscapes/ian-dooley-C6BrEIucDDU-unsplash.jpg", + alt: "کالکشن مانتو مانا", + position: "center 28%", + href: "/products?category=coat", + }, + { + en: "Shirts", + fa: "پیراهن", + text: "پیراهن‌هایی با پارچه لطیف و ایستایی تمیز برای استایل روزمره.", + src: "/images/landscapes/tamara-bellis-nbC4EyCxhb8-unsplash.jpg", + alt: "کالکشن پیراهن مانا", + position: "center 35%", + href: "/products?category=shirts", + }, + { + en: "Pants", + fa: "شلوار", + text: "شلوارهایی با فرم خوش‌ایست که شکل خود را حفظ می‌کنند.", + src: "/images/landscapes/vinicius-amnx-amano-k2yixQS_YH0-unsplash.jpg", + alt: "کالکشن شلوار مانا", + position: "center 30%", + href: "/products?category=pants", + }, + { + en: "Bags", + fa: "کیف زنانه", + text: "کیف‌هایی با جزئیات دقیق؛ تکمیل‌کننده استایل مانا.", + src: "/images/landscapes/mojtaba-mosayebzadeh-1nLTyduE1cY-unsplash.jpg", + alt: "کالکشن کیف زنانه مانا", + position: "center 40%", + href: "/products?category=bags", + }, +] as const; diff --git a/src/lib/map-home-slots.ts b/src/lib/map-home-slots.ts index c5cac49..e9dc349 100644 --- a/src/lib/map-home-slots.ts +++ b/src/lib/map-home-slots.ts @@ -2,17 +2,69 @@ import type { Category } from "@/data/home"; import { ABOUT, BLACK_ITEMS, - CATEGORIES, MEGA_MENU_TABS, + STORY_STEPS, } from "@/data/home"; import type { HeroSlideData } from "@/components/home/HeroSlider"; +import type { StoryStep } from "@/components/home/StickyStory"; import { categoryPath } from "@/lib/category-path"; import type { PublicWebsiteSlotItem } from "@/lib/website-slots"; import { firstForSlot, itemsForSlot } from "@/lib/website-slots"; +/** Slot order for CategoryBento / category-strip (sortOrder 0…5). */ +const CATEGORY_BENTO_FALLBACKS: Category[] = [ + { + id: "coat", + nameFa: "مانتو", + nameEn: "Mantel", + image: "/images/transparent/mantel.png", + href: categoryPath({ id: "coat", nameEn: "Coat", nameFa: "کت" }), + }, + { + id: "shirts", + nameFa: "پیراهن", + nameEn: "Shirts", + image: "/images/transparent/shirt.png", + href: categoryPath({ id: "shirts", nameEn: "Shirts", nameFa: "پیراهن" }), + }, + { + id: "pants", + nameFa: "شلوار", + nameEn: "Pants", + image: "/images/transparent/pants.png", + href: categoryPath({ id: "pants", nameEn: "Pants", nameFa: "شلوار" }), + }, + { + id: "bags", + nameFa: "کیف زنانه", + nameEn: "Wemon Bag", + image: "/images/transparent/bag.png", + href: categoryPath({ id: "bags", nameEn: "Bags", nameFa: "کیف" }), + }, + { + id: "scarf", + nameFa: "شال", + nameEn: "Scarf", + image: "/images/transparent/scarf.png", + href: categoryPath({ id: "scarf", nameEn: "Scarf", nameFa: "شال" }), + }, + { + id: "shoes", + nameFa: "کفش", + nameEn: "Shoe", + image: "/images/transparent/shoe.png", + href: categoryPath({ id: "shoes", nameEn: "Shoe", nameFa: "کفش" }), + }, +]; + export type HomeSlotContent = { heroSlides: HeroSlideData[]; categories: Category[]; + openingFrame: { + image: string; + imageAlt: string; + }; + stickyStory: StoryStep[]; leather: { image: string; nameFa: string; @@ -96,6 +148,12 @@ function firstSlot( return slotItems(items, slug, ...legacy)[0] ?? null; } +/** Only accept curated transparent PNGs from slots — ignore legacy white-bg icons. */ +function slotTransparentImage(imageUrl?: string | null) { + if (!imageUrl) return null; + return imageUrl.includes("/images/transparent/") ? imageUrl : null; +} + export function mapHomeSlots( items: PublicWebsiteSlotItem[], ): HomeSlotContent { @@ -118,32 +176,58 @@ export function mapHomeSlots( const categoryItems = slotItems(items, "category-strip"); const categories: Category[] = categoryItems.length > 0 - ? categoryItems.map((item, index) => { - const fallback = CATEGORIES[index % CATEGORIES.length]; + ? CATEGORY_BENTO_FALLBACKS.map((fallback, index) => { + const item = + categoryItems.find((row) => row.sortOrder === index) ?? + categoryItems[index]; return { - id: item.id || `cat-${index}`, - nameFa: item.nameFa || `دسته ${index + 1}`, - nameEn: item.nameEn || "", - image: item.imageUrl || fallback.image, - href: - item.link || - (fallback - ? categoryPath(fallback) - : categoryPath({ id: "coat", nameEn: "Coat", nameFa: "کت" })), + id: fallback.id, + // Keep curated bento labels; old category-strip rows had unrelated names. + nameFa: fallback.nameFa, + nameEn: fallback.nameEn, + // Prefer curated transparent art. Only use a slot image when it is + // already a storefront transparent asset (not legacy white-bg icons). + image: slotTransparentImage(item?.imageUrl) || fallback.image, + href: item?.link || fallback.href, }; }) - : CATEGORIES.map((cat) => { + : CATEGORY_BENTO_FALLBACKS.map((cat) => { const legacy = firstForSlot(items, `home.category.${cat.id}`); if (!legacy) return cat; return { ...cat, nameFa: legacy.nameFa || cat.nameFa, nameEn: legacy.nameEn || cat.nameEn, - image: legacy.imageUrl || cat.image, + image: slotTransparentImage(legacy.imageUrl) || cat.image, href: legacy.link || cat.href || categoryPath(cat), }; }); + const openingSlot = firstSlot(items, "opening-frame"); + const openingFrame = { + image: openingSlot?.imageUrl || "/images/landscapes/frame1.jpg", + imageAlt: openingSlot?.nameFa || "پرتره مد مانا کالکشن", + }; + + const storyItems = slotItems(items, "sticky-story"); + /** Prefer curated category titles; only take media/link from slots. */ + const stickyStory: StoryStep[] = STORY_STEPS.map((step, index) => { + const item = + storyItems.find((row) => row.sortOrder === index) ?? storyItems[index]; + const slotTitleIsCurated = + item?.nameFa === step.fa || item?.nameEn === step.en; + return { + en: slotTitleIsCurated && item?.nameEn ? item.nameEn : step.en, + fa: slotTitleIsCurated && item?.nameFa ? item.nameFa : step.fa, + text: + slotTitleIsCurated && item?.subtext ? item.subtext : step.text, + src: item?.imageUrl || step.src, + alt: step.alt, + position: step.position, + href: item?.link || step.href, + }; + }); + const leatherSlot = firstSlot( items, "leather-banner", @@ -284,6 +368,8 @@ export function mapHomeSlots( return { heroSlides, categories, + openingFrame, + stickyStory, leather, promoSale, promoDiscount,