first commit
This commit is contained in:
@@ -0,0 +1,184 @@
|
||||
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, carouselCategories, 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"), []),
|
||||
]);
|
||||
|
||||
const carouselCats = carouselCategories(categories);
|
||||
|
||||
// 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 categories={carouselCats} 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} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user