Files
moalem-shop/src/app/page.tsx
T
amirhosein.ashourlooandClaude Sonnet 5 2249a07dee Fix category-slot matching: identity by linkUrl slug, not array position
The homepage category carousel and installments highlights matched
slot.images[i] to categoryList[i] by array index, assuming the business's
CMS upload order lines up with this site's own category sort order. It
doesn't - live data showed a laptop photo landing on "موبایل" and "لوازم
برقی" appearing on the site despite no photo ever being uploaded for it,
purely from index luck. titleFa isn't reliable either: the same laptop image
was titled "تبلت" in the CMS, contradicting its own linkUrl.

Fix: resolve each image's category from the `category` query param on its
own linkUrl (categorySlugFromLinkUrl() in slug.ts, matched against
ApiCategory.slug via the new findNodeBySlug()). The homepage carousel is now
fully slot-driven - it iterates slot.images (in the CMS's own sortOrder) and
simply omits any category with no matching image, per explicit product
direction, rather than falling back to a placeholder. Installments keeps its
curated HIGHLIGHT_IDS list with a placeholder for uncovered categories,
since that page always shows the same fixed set, but now uses the same
identity-based lookup instead of position.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-06 07:36:24 +03:30

183 lines
6.4 KiB
TypeScript

import HeroSlider, { type Slide } from "@/components/HeroSlider";
import CategoryCarousel from "@/components/CategoryCarousel";
import HotDeals from "@/components/HotDeals";
import ProductCarousel from "@/components/ProductCarousel";
import { OneBanner, ThreeBanners, TwoBanners } from "@/components/PromoBanners";
import BlogCarousel, { type BlogCardData } from "@/components/BlogCarousel";
import {
findSlot,
getBlogs,
getCategories,
getProducts,
getSliders,
getStaticImageSlots,
getStoreSpecials,
safe,
} from "@/lib/api";
import { buildCategoryTree, findNode, subtreeIds } from "@/lib/categoryTree";
import { productToCard, specialToCards } from "@/lib/cards";
import { blogHref, categoryHref } from "@/lib/slug";
import { HOME_CAROUSELS } from "@/data/site";
import { FALLBACK_BLOGS } from "@/data/fallbackBlogs";
// The slider slot is empty on this tenant, so the homepage falls back to the
// campaign banners already running on moalem.shop (same art, same targets).
const FALLBACK_SLIDES: Slide[] = [
{
id: "laptop",
image: "/images/banners/9WYorEOTwn.jpg",
mobileImage: "/images/banners/WRbs72TDPR.jpg",
href: categoryHref("1254", "لپ تاپ"),
alt: "لپ تاپ",
},
{
id: "console",
image: "/images/banners/OiNl9v5gTf.jpg",
mobileImage: "/images/banners/ieYGotL2qd.jpg",
href: categoryHref("1256", "کنسول بازی"),
alt: "کنسول بازی",
},
{
id: "iphone-17",
image: "/images/banners/06SQinO4sG.jpg",
mobileImage: "/images/banners/lAGRh3ks32.jpg",
href: categoryHref("1251", "موبایل"),
alt: "آیفون ۱۷ پرو مکس",
},
];
const EMPTY_PAGE = { items: [], total: 0, page: 1, pageSize: 12 };
export default async function HomePage() {
const [categories, sliders, specials, blogs, imageSlots] = await Promise.all([
safe(getCategories().then(buildCategoryTree), []),
safe(getSliders(), []),
safe(getStoreSpecials(), []),
safe(getBlogs({ pageSize: 10 }), { items: [], total: 0, page: 1, pageSize: 10 }),
safe(getStaticImageSlots("home"), []),
]);
// Empty or failing slots fall through to each section's own placeholder —
// findSlot() already returns undefined for an empty/missing key.
const sliderSlot = findSlot(imageSlots, "slider");
const categoriesSlot = findSlot(imageSlots, "categories");
const oneBannerSlot = findSlot(imageSlots, "one-banner-bg");
const twoBannerSlot = findSlot(imageSlots, "two-banner-bg");
const threeBannerSlot = findSlot(imageSlots, "three-banner-bg");
const slides: Slide[] =
sliders.length > 0
? sliders
.filter((s) => s.imageUrl)
.map((s, i) => ({
id: s.id ?? `slide-${i}`,
image: s.imageUrl!,
mobileImage: s.mobileImageUrl ?? undefined,
href: s.linkUrl ?? "/products",
alt: s.title ?? "بنر فروشگاه موبایل معلم",
}))
: FALLBACK_SLIDES;
// "تخفیف‌های داغ" prefers the curated special-discount group; until it is
// filled in the dashboard it falls back to the newest catalog products.
const discountGroup =
specials.find((s) => s.key === "special-discount" && s.isActive) ?? specials[0];
const dealsFromCms = discountGroup ? specialToCards(discountGroup) : [];
const fallbackDeals = dealsFromCms.length
? []
: (await safe(getProducts({ pageSize: 12 }), EMPTY_PAGE)).items.map(productToCard);
const deals = dealsFromCms.length ? dealsFromCms : fallbackDeals;
const carousels = await Promise.all(
HOME_CAROUSELS.map(async (section) => {
const cat = findNode(categories, section.categoryId);
const ids = cat ? subtreeIds(cat) : [section.categoryId];
const pages = await Promise.all(
ids.map((categoryId) => safe(getProducts({ categoryId, pageSize: 12 }), EMPTY_PAGE))
);
const seen = new Set<string>();
const products = pages
.flatMap((page) => page.items)
.filter((product) => !seen.has(product.id) && seen.add(product.id))
.slice(0, 12)
.map(productToCard);
return {
...section,
href: categoryHref(section.categoryId, cat?.nameFa ?? section.title, cat?.name),
products,
};
})
);
const posts: BlogCardData[] =
blogs.items.length > 0
? blogs.items.map((b) => ({
id: b.id,
href: blogHref(b.id, b.title ?? b.nameFa),
title: b.title ?? b.nameFa ?? "",
image: b.thumbnailUrl ?? b.image ?? null,
category: b.categoryNameFa ?? b.categoryName ?? undefined,
}))
: FALLBACK_BLOGS.map((b) => ({
id: b.id,
href: "/blog",
title: b.title,
image: b.image,
category: b.category,
}));
// The deals countdown runs to the end of the current day, recomputed on each
// server render so the timer never shows a stale target.
const endOfDay = new Date();
endOfDay.setHours(23, 59, 59, 999);
const [mobileCarousel, accessoriesCarousel, laptopCarousel, consoleCarousel] = carousels;
return (
<>
<h1 className="sr-only">فروشگاه موبایل معلم | خرید موبایل، لپ‌تاپ و لوازم جانبی</h1>
<HeroSlider slot={sliderSlot} slides={slides} />
<CategoryCarousel roots={categories} slot={categoriesSlot} />
<HotDeals products={deals} endsAt={endOfDay.getTime() + 2 * 86_400_000} />
<ThreeBanners slot={threeBannerSlot} />
{mobileCarousel && (
<ProductCarousel
title={mobileCarousel.title}
desc={mobileCarousel.desc}
href={mobileCarousel.href}
products={mobileCarousel.products}
/>
)}
<TwoBanners slot={twoBannerSlot} />
{accessoriesCarousel && (
<ProductCarousel
title={accessoriesCarousel.title}
desc={accessoriesCarousel.desc}
href={accessoriesCarousel.href}
products={accessoriesCarousel.products}
/>
)}
{laptopCarousel && (
<ProductCarousel
title={laptopCarousel.title}
desc={laptopCarousel.desc}
href={laptopCarousel.href}
products={laptopCarousel.products}
/>
)}
<OneBanner slot={oneBannerSlot} />
{consoleCarousel && (
<ProductCarousel
title={consoleCarousel.title}
desc={consoleCarousel.desc}
href={consoleCarousel.href}
products={consoleCarousel.products}
/>
)}
<BlogCarousel posts={posts} />
</>
);
}