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:
co-authored by
Claude Sonnet 5
parent
5152b16d61
commit
bc6cafaffa
+44
@@ -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,
|
||||
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 | Notes |
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 25 KiB |
+6
-2
@@ -27,10 +27,14 @@ const syne = Syne({
|
||||
weight: ["600", "700", "800"],
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
export async function generateMetadata(): Promise<Metadata> {
|
||||
const businessInfo = await getBusinessInfo();
|
||||
return {
|
||||
title: "سانیهوم | فروشگاه لوازم خانگی",
|
||||
description: "فروشگاه آنلاین لوازم خانگی — یخچال، لباسشویی، تلویزیون و بیشتر",
|
||||
icons: businessInfo?.faviconUrl ? [{ url: businessInfo.faviconUrl }] : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
export default async function RootLayout({ children }: LayoutProps<"/">) {
|
||||
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">
|
||||
<Providers>
|
||||
<TopBar />
|
||||
<Header categoryTree={categoryTree} categories={categories} />
|
||||
<Header categoryTree={categoryTree} categories={categories} logoUrl={businessInfo?.logoUrl} />
|
||||
<main>{children}</main>
|
||||
<Footer businessInfo={businessInfo} categories={categories} />
|
||||
</Providers>
|
||||
|
||||
+4
-21
@@ -1,19 +1,15 @@
|
||||
import { FeatureBar } from "@/components/home/FeatureBar";
|
||||
import { HeroSlider } from "@/components/home/HeroSlider";
|
||||
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 { TopSellingCarousel } from "@/components/home/TopSellingCarousel";
|
||||
import { ThreeBanners } from "@/components/home/ThreeBanners";
|
||||
import { BrandGroupCarousels } from "@/components/home/BrandGroupCarousels";
|
||||
import { AboutSanihome } from "@/components/home/AboutSanihome";
|
||||
import { BlogCarousel } from "@/components/home/BlogCarousel";
|
||||
import {
|
||||
getBlogs,
|
||||
getBusinessInfo,
|
||||
getCategories,
|
||||
getProducts,
|
||||
getBrandGroups,
|
||||
getStoreSpecials,
|
||||
getStaticImageSlots,
|
||||
imagesForSlot,
|
||||
@@ -21,26 +17,15 @@ import {
|
||||
} from "@/lib/meshkee";
|
||||
|
||||
export default async function Home() {
|
||||
const [
|
||||
categories,
|
||||
storeSpecials,
|
||||
productsResult,
|
||||
brandGroups,
|
||||
staticImageSlots,
|
||||
businessInfo,
|
||||
blogsResult,
|
||||
] = await Promise.all([
|
||||
const [categories, storeSpecials, staticImageSlots, businessInfo, blogsResult] = await Promise.all([
|
||||
getCategories(),
|
||||
getStoreSpecials(),
|
||||
getProducts({ pageSize: 12 }),
|
||||
getBrandGroups(),
|
||||
getStaticImageSlots(),
|
||||
getBusinessInfo(),
|
||||
getBlogs({ pageSize: 8 }),
|
||||
]);
|
||||
|
||||
const contactPhone = businessInfo?.phoneNumbers[0];
|
||||
const specialSaleProducts = storeSpecials[0]?.items ?? [];
|
||||
const twoBannerSlot = staticImageSlots.find((slot) => slot.key === STATIC_IMAGE_KEYS.twoBanner);
|
||||
const threeBannerSlot = staticImageSlots.find(
|
||||
(slot) => slot.key === STATIC_IMAGE_KEYS.threeBannerRow,
|
||||
@@ -51,19 +36,17 @@ export default async function Home() {
|
||||
return (
|
||||
<>
|
||||
<HeroSlider images={heroImages} />
|
||||
<SpecialSaleCarousel products={specialSaleProducts} contactPhone={contactPhone} />
|
||||
<FeatureBar />
|
||||
<FeaturedCategories categories={categories} images={featuredCategoryImages} />
|
||||
<StoreSpecialCarousels groups={storeSpecials} contactPhone={contactPhone} />
|
||||
<FeatureBar />
|
||||
<TwoBanners
|
||||
images={imagesForSlot(staticImageSlots, STATIC_IMAGE_KEYS.twoBanner)}
|
||||
aspectRatio={twoBannerSlot?.aspectRatio}
|
||||
/>
|
||||
<TopSellingCarousel products={productsResult.items} contactPhone={contactPhone} />
|
||||
<ThreeBanners
|
||||
images={imagesForSlot(staticImageSlots, STATIC_IMAGE_KEYS.threeBannerRow)}
|
||||
aspectRatio={threeBannerSlot?.aspectRatio}
|
||||
/>
|
||||
<BrandGroupCarousels brandGroups={brandGroups} contactPhone={contactPhone} />
|
||||
<AboutSanihome />
|
||||
<BlogCarousel posts={blogsResult.items} />
|
||||
</>
|
||||
|
||||
@@ -4,7 +4,6 @@ import { Swiper, SwiperSlide } from "swiper/react";
|
||||
import { FreeMode } from "swiper/modules";
|
||||
import "swiper/css";
|
||||
import "swiper/css/free-mode";
|
||||
import { SectionTitle } from "@/components/ui/SectionTitle";
|
||||
import { EmptyState } from "@/components/ui/EmptyState";
|
||||
import type { Category, StaticImageWithUrl } from "@/lib/meshkee";
|
||||
|
||||
@@ -36,11 +35,6 @@ export function FeaturedCategories({
|
||||
return (
|
||||
<section className="py-10 md:py-14">
|
||||
<div className="mx-auto max-w-7xl px-4">
|
||||
<SectionTitle
|
||||
title="دستهبندیهای پرطرفدار"
|
||||
enTitle="Featured Categories"
|
||||
action="همه دستهها"
|
||||
/>
|
||||
{images.length > 0 ? (
|
||||
<Swiper
|
||||
modules={[FreeMode]}
|
||||
|
||||
@@ -32,12 +32,18 @@ export function Footer({
|
||||
<div className="grid gap-8 md:grid-cols-4">
|
||||
<div>
|
||||
<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">
|
||||
S
|
||||
</div>
|
||||
{businessInfo?.logoDarkUrl || businessInfo?.logoUrl ? (
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<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">
|
||||
SANIHOME
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{businessInfo?.about && (
|
||||
<p className="text-sm leading-relaxed text-white/70">{businessInfo.about}</p>
|
||||
|
||||
@@ -17,12 +17,12 @@ const navLinks = [
|
||||
|
||||
const LOGIN_URL = "https://customer.sanihome.ir";
|
||||
|
||||
function Logo({ large = false }: { large?: boolean }) {
|
||||
function Logo({ src, large = false }: { src?: string | null; large?: boolean }) {
|
||||
return (
|
||||
<a href="/" className="flex shrink-0 items-center">
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img
|
||||
src="/images/logo.png"
|
||||
src={src || "/images/logo.png"}
|
||||
alt="سانیهوم"
|
||||
className={
|
||||
large
|
||||
@@ -56,10 +56,12 @@ function MobileDrawer({
|
||||
open,
|
||||
onClose,
|
||||
categoryTree,
|
||||
logoUrl,
|
||||
}: {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
categoryTree: CategoryTreeGroup[];
|
||||
logoUrl?: string | null;
|
||||
}) {
|
||||
const [categoriesOpen, setCategoriesOpen] = 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">
|
||||
<Logo />
|
||||
<Logo src={logoUrl} />
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
@@ -199,9 +201,11 @@ function MobileDrawer({
|
||||
export function Header({
|
||||
categoryTree,
|
||||
categories,
|
||||
logoUrl,
|
||||
}: {
|
||||
categoryTree: CategoryTreeGroup[];
|
||||
categories: Category[];
|
||||
logoUrl?: string | null;
|
||||
}) {
|
||||
const [menuOpen, setMenuOpen] = useState(false);
|
||||
const [drawerOpen, setDrawerOpen] = useState(false);
|
||||
@@ -232,13 +236,13 @@ export function Header({
|
||||
<span aria-hidden>☰</span>
|
||||
</button>
|
||||
<div className="hidden min-[1200px]:block">
|
||||
<Logo />
|
||||
<Logo src={logoUrl} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-center min-[1200px]:flex-1 min-[1200px]:px-6">
|
||||
<div className="min-[1200px]:hidden">
|
||||
<Logo large />
|
||||
<Logo src={logoUrl} large />
|
||||
</div>
|
||||
<div className="hidden w-full min-[1200px]:block">
|
||||
<SearchBar />
|
||||
@@ -360,6 +364,7 @@ export function Header({
|
||||
open={drawerOpen}
|
||||
onClose={() => setDrawerOpen(false)}
|
||||
categoryTree={categoryTree}
|
||||
logoUrl={logoUrl}
|
||||
/>
|
||||
</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>
|
||||
);
|
||||
}
|
||||
+12
-17
@@ -1,33 +1,29 @@
|
||||
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 ProductGroup } from "@/lib/meshkee";
|
||||
|
||||
export function BrandGroupCarousels({
|
||||
brandGroups,
|
||||
// Renders every admin-managed carousel from GET /store-specials — whatever
|
||||
// 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,
|
||||
}: {
|
||||
brandGroups: ProductGroup[];
|
||||
groups: ProductGroup[];
|
||||
contactPhone?: string;
|
||||
}) {
|
||||
if (brandGroups.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>
|
||||
);
|
||||
}
|
||||
const visibleGroups = groups.filter((group) => group.items.length > 0);
|
||||
|
||||
return (
|
||||
<>
|
||||
{brandGroups.map((group) => (
|
||||
{visibleGroups.map((group) => (
|
||||
<section key={group.id} className="py-10 md:py-14">
|
||||
<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}`}>
|
||||
{group.items.map((product) => (
|
||||
<ProductCard
|
||||
@@ -38,7 +34,6 @@ export function BrandGroupCarousels({
|
||||
oldPrice={product.oldPrice}
|
||||
discount={product.discount}
|
||||
image={product.image ?? "/images/placeholder.svg"}
|
||||
variant="brand"
|
||||
href={productHref(product)}
|
||||
contactPhone={contactPhone}
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
type SectionTitleProps = {
|
||||
title: string;
|
||||
enTitle: string;
|
||||
enTitle?: string;
|
||||
subtitle?: string;
|
||||
action?: string;
|
||||
};
|
||||
@@ -11,6 +11,7 @@ export function SectionTitle({ title, enTitle, subtitle, action }: SectionTitleP
|
||||
<div>
|
||||
<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>
|
||||
{enTitle && (
|
||||
<span
|
||||
className="font-display text-sm font-semibold tracking-wide text-red-dark/30 uppercase md:text-base"
|
||||
lang="en"
|
||||
@@ -18,6 +19,7 @@ export function SectionTitle({ title, enTitle, subtitle, action }: SectionTitleP
|
||||
>
|
||||
{enTitle}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{subtitle && (
|
||||
<p className="mt-1 text-sm text-muted">{subtitle}</p>
|
||||
|
||||
+11
-9
@@ -51,6 +51,7 @@ export type BusinessInfo = {
|
||||
about: string;
|
||||
vision: string;
|
||||
logoUrl: string | null;
|
||||
logoDarkUrl: string | null;
|
||||
faviconUrl: string | null;
|
||||
emails: string[];
|
||||
phoneNumbers: string[];
|
||||
@@ -490,27 +491,28 @@ export type ProductGroup = {
|
||||
id: string;
|
||||
key: string;
|
||||
title: string;
|
||||
sortOrder: number;
|
||||
items: Product[];
|
||||
};
|
||||
|
||||
function normalizeProductGroups(items: Record<string, unknown>[] | undefined): ProductGroup[] {
|
||||
return (items ?? []).map((group) => ({
|
||||
return (items ?? [])
|
||||
.map((group) => ({
|
||||
id: String(group.id ?? group.key ?? ""),
|
||||
key: String(group.key ?? group.id ?? ""),
|
||||
title: String(group.title ?? ""),
|
||||
sortOrder: Number(group.sortOrder ?? 0),
|
||||
items: Array.isArray(group.items)
|
||||
? (group.items as Record<string, unknown>[]).map(normalizeProduct)
|
||||
: [],
|
||||
}));
|
||||
}
|
||||
|
||||
export async function getBrandGroups(): Promise<ProductGroup[]> {
|
||||
const res = await apiGet<{ items: Record<string, unknown>[] }>(
|
||||
`/tenants/${DOMAIN}/website/brand-groups`,
|
||||
);
|
||||
return normalizeProductGroups(res?.items);
|
||||
}))
|
||||
.sort((a, b) => a.sortOrder - b.sortOrder);
|
||||
}
|
||||
|
||||
// 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[]> {
|
||||
const res = await apiGet<{ items: Record<string, unknown>[] }>(
|
||||
`/tenants/${DOMAIN}/store-specials`,
|
||||
|
||||
Reference in New Issue
Block a user