Make homepage carousels fully dynamic, drop brand carousels, dynamic logo/favicon

- Replace the two hardcoded homepage carousels (SpecialSaleCarousel
  pinned to store-specials group 0 only, TopSellingCarousel reading
  from an unrelated plain product query) with StoreSpecialCarousels:
  loops over every group GET /store-specials returns, using each
  group's own title. A new carousel created in the admin panel (e.g.
  the "بهترین‌ها از نگاه شما" one just added) shows up automatically,
  no code change needed.
- Remove the brand-groups integration and its "سامسونگ" carousel
  entirely (getBrandGroups, BrandGroupCarousels) — brand-name
  carousels aren't wanted on this site per explicit instruction.
- SectionTitle's enTitle is now optional, since dynamic store-specials
  groups only have one title string, not a Persian/English pair.
- Move featured categories above the hero-adjacent special carousels
  per the requested homepage order; drop the "دسته‌بندی‌های پرطرفدار"
  heading and its "همه دسته‌ها" button, per request.
- Wire the header/footer logo and the favicon to the real business
  data (logoUrl/logoDarkUrl/faviconUrl from GET business-info) instead
  of static files; removed src/app/favicon.ico, which was silently
  winning over the new dynamic <link rel="icon">.
- CONTEXT.md: document the dynamic-carousel contract and the
  logo/favicon wiring so neither regresses to hardcoded.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
amirhosein.ashourloo
2026-09-15 17:41:46 +03:30
co-authored by Claude Sonnet 5
parent 5152b16d61
commit bc6cafaffa
12 changed files with 118 additions and 194 deletions
+44
View File
@@ -195,6 +195,50 @@ them in the dashboard; the code path just needs to fall back to whatever the
`FeaturedCategories.tsx` for the pattern: real slot images when present, `FeaturedCategories.tsx` for the pattern: real slot images when present,
else the pre-existing UI unchanged). else the pre-existing UI unchanged).
## Homepage product carousels — fully dynamic, don't hardcode a name/key
`GET /tenants/{domain}/store-specials` returns **every** admin-managed named
carousel as one flat list — e.g. "Special Sale", "پرفروش‌ها", "بهترین‌ها از
نگاه شما" all come back from this one call, each with its own `title` and
`items`. `StoreSpecialCarousels.tsx` maps over the **whole list** (sorted by
`sortOrder`, empty groups skipped) and renders one titled carousel section
per group, using `group.title` from the API as the heading — no group
id/key/title is special-cased in code. This means a brand-new carousel
created in the Meshkee dashboard (any name) appears on the homepage the next
time the page renders, with zero code changes.
**Do not**:
- Hardcode a Persian/English title pair for one of these carousels (that was
the old `SpecialSaleCarousel`/`TopSellingCarousel` mistake — two separate
components each pinned to one group, with a made-up title instead of the
API's own `title`, and `TopSellingCarousel` didn't even read from
store-specials, it re-queried the plain product list).
- Reintroduce a **brand-name carousel** (`GET /website/brand-groups` — used
to render a "سامسونگ" section). The user explicitly removed this: brand
carousels aren't wanted on this site at all. If similar per-brand curation
is wanted later, it should go through admin-managed store-specials groups
(a group can be curated by brand already), not a separate brand-groups
integration.
`SectionTitle`'s `enTitle` prop is optional for exactly this reason — these
dynamic groups only have one `title` string from the API, not a Persian/
English pair, so the small English-caps subtitle just doesn't render for them.
## Branding: logo & favicon are dynamic, not static files
`BusinessInfo` (`getBusinessInfo()`, already fetched in `layout.tsx`) carries
`logoUrl`, `logoDarkUrl`, `faviconUrl` straight from the API — the same
values `GET /tenants/{domain}` and `GET .../website/favicon` also expose.
`Header`'s `Logo` takes a `src` prop (falls back to `public/images/logo.png`
only if the API value is null) threaded down from `layout.tsx`; `Footer`
prefers `logoDarkUrl` (falls back to `logoUrl`) since it sits on a dark
background. The favicon is set via `generateMetadata()`'s `icons` field —
**there is no `src/app/favicon.ico` file** (deliberately removed): Next.js's
static-file favicon convention and a dynamic `metadata.icons` entry both
render a `<link rel="icon">` and browsers aren't guaranteed to prefer the
second one, so keeping both was silently serving the stale static icon.
Don't add `app/favicon.ico` (or `public/favicon.ico`) back.
## Route map ## Route map
| Route | Notes | | Route | Notes |
Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

+6 -2
View File
@@ -27,10 +27,14 @@ const syne = Syne({
weight: ["600", "700", "800"], weight: ["600", "700", "800"],
}); });
export const metadata: Metadata = { export async function generateMetadata(): Promise<Metadata> {
const businessInfo = await getBusinessInfo();
return {
title: "سانی‌هوم | فروشگاه لوازم خانگی", title: "سانی‌هوم | فروشگاه لوازم خانگی",
description: "فروشگاه آنلاین لوازم خانگی — یخچال، لباسشویی، تلویزیون و بیشتر", description: "فروشگاه آنلاین لوازم خانگی — یخچال، لباسشویی، تلویزیون و بیشتر",
icons: businessInfo?.faviconUrl ? [{ url: businessInfo.faviconUrl }] : undefined,
}; };
}
export default async function RootLayout({ children }: LayoutProps<"/">) { export default async function RootLayout({ children }: LayoutProps<"/">) {
const [categoryTree, categories, businessInfo] = await Promise.all([ const [categoryTree, categories, businessInfo] = await Promise.all([
@@ -48,7 +52,7 @@ export default async function RootLayout({ children }: LayoutProps<"/">) {
<body className="min-h-full flex flex-col"> <body className="min-h-full flex flex-col">
<Providers> <Providers>
<TopBar /> <TopBar />
<Header categoryTree={categoryTree} categories={categories} /> <Header categoryTree={categoryTree} categories={categories} logoUrl={businessInfo?.logoUrl} />
<main>{children}</main> <main>{children}</main>
<Footer businessInfo={businessInfo} categories={categories} /> <Footer businessInfo={businessInfo} categories={categories} />
</Providers> </Providers>
+4 -21
View File
@@ -1,19 +1,15 @@
import { FeatureBar } from "@/components/home/FeatureBar"; import { FeatureBar } from "@/components/home/FeatureBar";
import { HeroSlider } from "@/components/home/HeroSlider"; import { HeroSlider } from "@/components/home/HeroSlider";
import { FeaturedCategories } from "@/components/home/FeaturedCategories"; import { FeaturedCategories } from "@/components/home/FeaturedCategories";
import { SpecialSaleCarousel } from "@/components/home/SpecialSaleCarousel"; import { StoreSpecialCarousels } from "@/components/home/StoreSpecialCarousels";
import { TwoBanners } from "@/components/home/TwoBanners"; import { TwoBanners } from "@/components/home/TwoBanners";
import { TopSellingCarousel } from "@/components/home/TopSellingCarousel";
import { ThreeBanners } from "@/components/home/ThreeBanners"; import { ThreeBanners } from "@/components/home/ThreeBanners";
import { BrandGroupCarousels } from "@/components/home/BrandGroupCarousels";
import { AboutSanihome } from "@/components/home/AboutSanihome"; import { AboutSanihome } from "@/components/home/AboutSanihome";
import { BlogCarousel } from "@/components/home/BlogCarousel"; import { BlogCarousel } from "@/components/home/BlogCarousel";
import { import {
getBlogs, getBlogs,
getBusinessInfo, getBusinessInfo,
getCategories, getCategories,
getProducts,
getBrandGroups,
getStoreSpecials, getStoreSpecials,
getStaticImageSlots, getStaticImageSlots,
imagesForSlot, imagesForSlot,
@@ -21,26 +17,15 @@ import {
} from "@/lib/meshkee"; } from "@/lib/meshkee";
export default async function Home() { export default async function Home() {
const [ const [categories, storeSpecials, staticImageSlots, businessInfo, blogsResult] = await Promise.all([
categories,
storeSpecials,
productsResult,
brandGroups,
staticImageSlots,
businessInfo,
blogsResult,
] = await Promise.all([
getCategories(), getCategories(),
getStoreSpecials(), getStoreSpecials(),
getProducts({ pageSize: 12 }),
getBrandGroups(),
getStaticImageSlots(), getStaticImageSlots(),
getBusinessInfo(), getBusinessInfo(),
getBlogs({ pageSize: 8 }), getBlogs({ pageSize: 8 }),
]); ]);
const contactPhone = businessInfo?.phoneNumbers[0]; const contactPhone = businessInfo?.phoneNumbers[0];
const specialSaleProducts = storeSpecials[0]?.items ?? [];
const twoBannerSlot = staticImageSlots.find((slot) => slot.key === STATIC_IMAGE_KEYS.twoBanner); const twoBannerSlot = staticImageSlots.find((slot) => slot.key === STATIC_IMAGE_KEYS.twoBanner);
const threeBannerSlot = staticImageSlots.find( const threeBannerSlot = staticImageSlots.find(
(slot) => slot.key === STATIC_IMAGE_KEYS.threeBannerRow, (slot) => slot.key === STATIC_IMAGE_KEYS.threeBannerRow,
@@ -51,19 +36,17 @@ export default async function Home() {
return ( return (
<> <>
<HeroSlider images={heroImages} /> <HeroSlider images={heroImages} />
<SpecialSaleCarousel products={specialSaleProducts} contactPhone={contactPhone} />
<FeatureBar />
<FeaturedCategories categories={categories} images={featuredCategoryImages} /> <FeaturedCategories categories={categories} images={featuredCategoryImages} />
<StoreSpecialCarousels groups={storeSpecials} contactPhone={contactPhone} />
<FeatureBar />
<TwoBanners <TwoBanners
images={imagesForSlot(staticImageSlots, STATIC_IMAGE_KEYS.twoBanner)} images={imagesForSlot(staticImageSlots, STATIC_IMAGE_KEYS.twoBanner)}
aspectRatio={twoBannerSlot?.aspectRatio} aspectRatio={twoBannerSlot?.aspectRatio}
/> />
<TopSellingCarousel products={productsResult.items} contactPhone={contactPhone} />
<ThreeBanners <ThreeBanners
images={imagesForSlot(staticImageSlots, STATIC_IMAGE_KEYS.threeBannerRow)} images={imagesForSlot(staticImageSlots, STATIC_IMAGE_KEYS.threeBannerRow)}
aspectRatio={threeBannerSlot?.aspectRatio} aspectRatio={threeBannerSlot?.aspectRatio}
/> />
<BrandGroupCarousels brandGroups={brandGroups} contactPhone={contactPhone} />
<AboutSanihome /> <AboutSanihome />
<BlogCarousel posts={blogsResult.items} /> <BlogCarousel posts={blogsResult.items} />
</> </>
@@ -4,7 +4,6 @@ import { Swiper, SwiperSlide } from "swiper/react";
import { FreeMode } from "swiper/modules"; import { FreeMode } from "swiper/modules";
import "swiper/css"; import "swiper/css";
import "swiper/css/free-mode"; import "swiper/css/free-mode";
import { SectionTitle } from "@/components/ui/SectionTitle";
import { EmptyState } from "@/components/ui/EmptyState"; import { EmptyState } from "@/components/ui/EmptyState";
import type { Category, StaticImageWithUrl } from "@/lib/meshkee"; import type { Category, StaticImageWithUrl } from "@/lib/meshkee";
@@ -36,11 +35,6 @@ export function FeaturedCategories({
return ( return (
<section className="py-10 md:py-14"> <section className="py-10 md:py-14">
<div className="mx-auto max-w-7xl px-4"> <div className="mx-auto max-w-7xl px-4">
<SectionTitle
title="دسته‌بندی‌های پرطرفدار"
enTitle="Featured Categories"
action="همه دسته‌ها"
/>
{images.length > 0 ? ( {images.length > 0 ? (
<Swiper <Swiper
modules={[FreeMode]} modules={[FreeMode]}
+9 -3
View File
@@ -32,12 +32,18 @@ export function Footer({
<div className="grid gap-8 md:grid-cols-4"> <div className="grid gap-8 md:grid-cols-4">
<div> <div>
<div className="mb-4 flex items-center gap-2"> <div className="mb-4 flex items-center gap-2">
<div className="flex h-9 w-9 items-center justify-center rounded-lg bg-white text-base font-bold text-red-dark"> {businessInfo?.logoDarkUrl || businessInfo?.logoUrl ? (
S // eslint-disable-next-line @next/next/no-img-element
</div> <img
src={businessInfo.logoDarkUrl ?? businessInfo.logoUrl ?? undefined}
alt={businessInfo.nameFa || "سانی‌هوم"}
className="h-9 w-auto object-contain"
/>
) : (
<span className="font-display text-lg font-bold tracking-tight"> <span className="font-display text-lg font-bold tracking-tight">
SANIHOME SANIHOME
</span> </span>
)}
</div> </div>
{businessInfo?.about && ( {businessInfo?.about && (
<p className="text-sm leading-relaxed text-white/70">{businessInfo.about}</p> <p className="text-sm leading-relaxed text-white/70">{businessInfo.about}</p>
+10 -5
View File
@@ -17,12 +17,12 @@ const navLinks = [
const LOGIN_URL = "https://customer.sanihome.ir"; const LOGIN_URL = "https://customer.sanihome.ir";
function Logo({ large = false }: { large?: boolean }) { function Logo({ src, large = false }: { src?: string | null; large?: boolean }) {
return ( return (
<a href="/" className="flex shrink-0 items-center"> <a href="/" className="flex shrink-0 items-center">
{/* eslint-disable-next-line @next/next/no-img-element */} {/* eslint-disable-next-line @next/next/no-img-element */}
<img <img
src="/images/logo.png" src={src || "/images/logo.png"}
alt="سانی‌هوم" alt="سانی‌هوم"
className={ className={
large large
@@ -56,10 +56,12 @@ function MobileDrawer({
open, open,
onClose, onClose,
categoryTree, categoryTree,
logoUrl,
}: { }: {
open: boolean; open: boolean;
onClose: () => void; onClose: () => void;
categoryTree: CategoryTreeGroup[]; categoryTree: CategoryTreeGroup[];
logoUrl?: string | null;
}) { }) {
const [categoriesOpen, setCategoriesOpen] = useState(false); const [categoriesOpen, setCategoriesOpen] = useState(false);
const [mounted, setMounted] = useState(false); const [mounted, setMounted] = useState(false);
@@ -100,7 +102,7 @@ function MobileDrawer({
}`} }`}
> >
<div className="flex items-center justify-between border-b border-border p-5"> <div className="flex items-center justify-between border-b border-border p-5">
<Logo /> <Logo src={logoUrl} />
<button <button
type="button" type="button"
onClick={onClose} onClick={onClose}
@@ -199,9 +201,11 @@ function MobileDrawer({
export function Header({ export function Header({
categoryTree, categoryTree,
categories, categories,
logoUrl,
}: { }: {
categoryTree: CategoryTreeGroup[]; categoryTree: CategoryTreeGroup[];
categories: Category[]; categories: Category[];
logoUrl?: string | null;
}) { }) {
const [menuOpen, setMenuOpen] = useState(false); const [menuOpen, setMenuOpen] = useState(false);
const [drawerOpen, setDrawerOpen] = useState(false); const [drawerOpen, setDrawerOpen] = useState(false);
@@ -232,13 +236,13 @@ export function Header({
<span aria-hidden>☰</span> <span aria-hidden>☰</span>
</button> </button>
<div className="hidden min-[1200px]:block"> <div className="hidden min-[1200px]:block">
<Logo /> <Logo src={logoUrl} />
</div> </div>
</div> </div>
<div className="flex items-center justify-center min-[1200px]:flex-1 min-[1200px]:px-6"> <div className="flex items-center justify-center min-[1200px]:flex-1 min-[1200px]:px-6">
<div className="min-[1200px]:hidden"> <div className="min-[1200px]:hidden">
<Logo large /> <Logo src={logoUrl} large />
</div> </div>
<div className="hidden w-full min-[1200px]:block"> <div className="hidden w-full min-[1200px]:block">
<SearchBar /> <SearchBar />
@@ -360,6 +364,7 @@ export function Header({
open={drawerOpen} open={drawerOpen}
onClose={() => setDrawerOpen(false)} onClose={() => setDrawerOpen(false)}
categoryTree={categoryTree} categoryTree={categoryTree}
logoUrl={logoUrl}
/> />
</header> </header>
); );
@@ -1,65 +0,0 @@
import { Carousel } from "@/components/ui/Carousel";
import { Placeholder } from "@/components/ui/Placeholder";
import { ProductCard } from "@/components/ui/ProductCard";
import { SectionTitle } from "@/components/ui/SectionTitle";
import { EmptyState } from "@/components/ui/EmptyState";
import { productHref, type Product } from "@/lib/meshkee";
function SpecialSaleBanner() {
return (
<a
href="#"
className="relative w-[220px] shrink-0 self-stretch overflow-hidden rounded-xl border border-border md:w-[240px]"
>
<Placeholder
src="/images/sale-banner.svg"
alt="فروش ویژه"
width="100%"
className="h-full min-h-[380px] w-full rounded-none border-0"
/>
</a>
);
}
export function SpecialSaleCarousel({
products,
contactPhone,
}: {
products: Product[];
contactPhone?: string;
}) {
return (
<section className="border-y border-red-light bg-red-light/30 py-10 md:py-14">
<div className="mx-auto max-w-7xl px-4">
<SectionTitle
title="فروش ویژه"
enTitle="Special Sale"
action="مشاهده همه"
/>
{products.length === 0 ? (
<EmptyState message="هنوز محصولی برای فروش ویژه ثبت نشده است." />
) : (
<Carousel ariaLabel="کاروسل فروش ویژه">
<SpecialSaleBanner />
{products.map((product) => (
<ProductCard
key={product.id}
name={product.name}
brand={product.brand ?? ""}
price={product.price}
oldPrice={product.oldPrice}
discount={product.discount}
image={product.image ?? "/images/placeholder.svg"}
variant="sale"
href={productHref(product)}
contactPhone={contactPhone}
variantId={product.variantId}
slug={product.slug}
/>
))}
</Carousel>
)}
</div>
</section>
);
}
@@ -1,33 +1,29 @@
import { Carousel } from "@/components/ui/Carousel"; import { Carousel } from "@/components/ui/Carousel";
import { ProductCard } from "@/components/ui/ProductCard"; import { ProductCard } from "@/components/ui/ProductCard";
import { SectionTitle } from "@/components/ui/SectionTitle"; import { SectionTitle } from "@/components/ui/SectionTitle";
import { EmptyState } from "@/components/ui/EmptyState";
import { productHref, type ProductGroup } from "@/lib/meshkee"; import { productHref, type ProductGroup } from "@/lib/meshkee";
export function BrandGroupCarousels({ // Renders every admin-managed carousel from GET /store-specials — whatever
brandGroups, // groups exist there, in their configured order. Nothing here is keyed off
// a specific group id/key/title, so a new carousel created in the Meshkee
// dashboard (any name) shows up on the homepage automatically, with no code
// change. Groups with no items are skipped (honest empty state — no heading
// over nothing).
export function StoreSpecialCarousels({
groups,
contactPhone, contactPhone,
}: { }: {
brandGroups: ProductGroup[]; groups: ProductGroup[];
contactPhone?: string; contactPhone?: string;
}) { }) {
if (brandGroups.length === 0) { const visibleGroups = groups.filter((group) => group.items.length > 0);
return (
<section className="py-10 md:py-14">
<div className="mx-auto max-w-7xl px-4">
<SectionTitle title="محصولات برندها" enTitle="Brand Collections" />
<EmptyState message="هنوز محصولی برای برندها ثبت نشده است." />
</div>
</section>
);
}
return ( return (
<> <>
{brandGroups.map((group) => ( {visibleGroups.map((group) => (
<section key={group.id} className="py-10 md:py-14"> <section key={group.id} className="py-10 md:py-14">
<div className="mx-auto max-w-7xl px-4"> <div className="mx-auto max-w-7xl px-4">
<SectionTitle title={group.title} enTitle={group.title} action="مشاهده همه" /> <SectionTitle title={group.title} />
<Carousel ariaLabel={`کاروسل ${group.title}`}> <Carousel ariaLabel={`کاروسل ${group.title}`}>
{group.items.map((product) => ( {group.items.map((product) => (
<ProductCard <ProductCard
@@ -38,7 +34,6 @@ export function BrandGroupCarousels({
oldPrice={product.oldPrice} oldPrice={product.oldPrice}
discount={product.discount} discount={product.discount}
image={product.image ?? "/images/placeholder.svg"} image={product.image ?? "/images/placeholder.svg"}
variant="brand"
href={productHref(product)} href={productHref(product)}
contactPhone={contactPhone} contactPhone={contactPhone}
variantId={product.variantId} variantId={product.variantId}
@@ -1,46 +0,0 @@
import { Carousel } from "@/components/ui/Carousel";
import { ProductCard } from "@/components/ui/ProductCard";
import { SectionTitle } from "@/components/ui/SectionTitle";
import { EmptyState } from "@/components/ui/EmptyState";
import { productHref, type Product } from "@/lib/meshkee";
export function TopSellingCarousel({
products,
contactPhone,
}: {
products: Product[];
contactPhone?: string;
}) {
return (
<section className="py-10 md:py-14">
<div className="mx-auto max-w-7xl px-4">
<SectionTitle
title="پرفروش‌ترین‌ها"
enTitle="Top Selling"
action="مشاهده همه"
/>
{products.length === 0 ? (
<EmptyState message="هنوز محصولی ثبت نشده است." />
) : (
<Carousel ariaLabel="کاروسل پرفروش‌ترین‌ها">
{products.map((product) => (
<ProductCard
key={product.id}
name={product.name}
brand={product.brand ?? ""}
price={product.price}
oldPrice={product.oldPrice}
discount={product.discount}
image={product.image ?? "/images/placeholder.svg"}
href={productHref(product)}
contactPhone={contactPhone}
variantId={product.variantId}
slug={product.slug}
/>
))}
</Carousel>
)}
</div>
</section>
);
}
+3 -1
View File
@@ -1,6 +1,6 @@
type SectionTitleProps = { type SectionTitleProps = {
title: string; title: string;
enTitle: string; enTitle?: string;
subtitle?: string; subtitle?: string;
action?: string; action?: string;
}; };
@@ -11,6 +11,7 @@ export function SectionTitle({ title, enTitle, subtitle, action }: SectionTitleP
<div> <div>
<div className="flex flex-wrap items-baseline gap-x-3 gap-y-1"> <div className="flex flex-wrap items-baseline gap-x-3 gap-y-1">
<h2 className="text-xl font-bold text-red-dark md:text-2xl">{title}</h2> <h2 className="text-xl font-bold text-red-dark md:text-2xl">{title}</h2>
{enTitle && (
<span <span
className="font-display text-sm font-semibold tracking-wide text-red-dark/30 uppercase md:text-base" className="font-display text-sm font-semibold tracking-wide text-red-dark/30 uppercase md:text-base"
lang="en" lang="en"
@@ -18,6 +19,7 @@ export function SectionTitle({ title, enTitle, subtitle, action }: SectionTitleP
> >
{enTitle} {enTitle}
</span> </span>
)}
</div> </div>
{subtitle && ( {subtitle && (
<p className="mt-1 text-sm text-muted">{subtitle}</p> <p className="mt-1 text-sm text-muted">{subtitle}</p>
+11 -9
View File
@@ -51,6 +51,7 @@ export type BusinessInfo = {
about: string; about: string;
vision: string; vision: string;
logoUrl: string | null; logoUrl: string | null;
logoDarkUrl: string | null;
faviconUrl: string | null; faviconUrl: string | null;
emails: string[]; emails: string[];
phoneNumbers: string[]; phoneNumbers: string[];
@@ -490,27 +491,28 @@ export type ProductGroup = {
id: string; id: string;
key: string; key: string;
title: string; title: string;
sortOrder: number;
items: Product[]; items: Product[];
}; };
function normalizeProductGroups(items: Record<string, unknown>[] | undefined): ProductGroup[] { function normalizeProductGroups(items: Record<string, unknown>[] | undefined): ProductGroup[] {
return (items ?? []).map((group) => ({ return (items ?? [])
.map((group) => ({
id: String(group.id ?? group.key ?? ""), id: String(group.id ?? group.key ?? ""),
key: String(group.key ?? group.id ?? ""), key: String(group.key ?? group.id ?? ""),
title: String(group.title ?? ""), title: String(group.title ?? ""),
sortOrder: Number(group.sortOrder ?? 0),
items: Array.isArray(group.items) items: Array.isArray(group.items)
? (group.items as Record<string, unknown>[]).map(normalizeProduct) ? (group.items as Record<string, unknown>[]).map(normalizeProduct)
: [], : [],
})); }))
} .sort((a, b) => a.sortOrder - b.sortOrder);
export async function getBrandGroups(): Promise<ProductGroup[]> {
const res = await apiGet<{ items: Record<string, unknown>[] }>(
`/tenants/${DOMAIN}/website/brand-groups`,
);
return normalizeProductGroups(res?.items);
} }
// Admin-managed, named product carousels ("Special Sale", "پرفروش‌ها",
// "بهترین‌ها از نگاه شما", ...) — the whole list, not a fixed set of known
// keys, so a new one created in the dashboard shows up on the homepage
// automatically. See StoreSpecialCarousels.tsx.
export async function getStoreSpecials(): Promise<ProductGroup[]> { export async function getStoreSpecials(): Promise<ProductGroup[]> {
const res = await apiGet<{ items: Record<string, unknown>[] }>( const res = await apiGet<{ items: Record<string, unknown>[] }>(
`/tenants/${DOMAIN}/store-specials`, `/tenants/${DOMAIN}/store-specials`,