Interleave banners between carousels, real "view all" links, bigger desktop logo

- StoreSpecialCarousels now inserts one of the two banner sections
  (TwoBanners/ThreeBanners, cycling) between every pair of carousels
  instead of stacking all carousels together with the banners fixed
  elsewhere on the page — works for any number of dynamic carousels.
- SectionTitle's action button is a real link now (actionHref), not an
  inert <button>. Each carousel's "مشاهده همه" goes to
  /products?specialId={group.id}, which renders that exact
  admin-curated group in the full product grid — store-specials groups
  have no pagination, so this is genuinely the complete set, not a
  truncated preview. Also fixed BlogCarousel's dead "وبلاگ" button the
  same way, now linking to /blog.
- Reduced FeaturedCategories' vertical padding (py-10/14 -> py-4/6).
- Desktop header logo was smaller than the mobile one (36px vs 46px) —
  bumped to 40/48px so it reads properly at desktop width.
- CONTEXT.md: document the banner-interleaving and specialId patterns.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
amirhosein.ashourloo
2026-09-15 17:57:01 +03:30
co-authored by Claude Sonnet 5
parent bc6cafaffa
commit 7c04316f3a
8 changed files with 134 additions and 44 deletions
+18 -1
View File
@@ -224,6 +224,23 @@ time the page renders, with zero code changes.
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.
**Banner interleaving**: the two static-image banner sections (`TwoBanners`,
`ThreeBanners`) are rendered *inside* `StoreSpecialCarousels`, one inserted
between every pair of carousels (never before the first or after the last),
cycling through the two if there end up being more than two carousels. Don't
move them back out to `page.tsx` as fixed-position siblings — that only
works for a known, fixed carousel count, which this deliberately isn't.
**"مشاهده همه" (view all)**: each carousel's action button links to
`/products?specialId={group.id}`. `store-specials` groups are admin-curated,
already-complete lists (the endpoint has no pagination) — there's no
"more items than the carousel shows" to fetch, so `/products` with
`specialId` set just re-renders that exact group's `items` in the full grid
instead of calling `getProducts()`. This is why the pattern is safe to keep
fully generic (any group id works, forever) rather than needing a
per-carousel filter mapping — there isn't a filter to map to; it's the same
list either way.
## Branding: logo & favicon are dynamic, not static files
`BusinessInfo` (`getBusinessInfo()`, already fetched in `layout.tsx`) carries
@@ -244,7 +261,7 @@ Don't add `app/favicon.ico` (or `public/favicon.ico`) back.
| Route | Notes |
|---|---|
| `/` | Homepage — all the carousels/banners |
| `/products`, `/products/[slug]` | Catalog browsing, category filter via `?categoryId=` |
| `/products`, `/products/[slug]` | Catalog browsing, category filter via `?categoryId=`, one store-specials group via `?specialId=` (see above) |
| `/products/id/[id]` | Redirect-only resolver, see "Group-source id vs productId" above |
| `/store-items`, `/store-items/[id]` | Variant-level browsing/detail, see above |
| `/blog`, `/blog/[slug]` | Blog listing/detail |
+8 -11
View File
@@ -2,8 +2,6 @@ import { FeatureBar } from "@/components/home/FeatureBar";
import { HeroSlider } from "@/components/home/HeroSlider";
import { FeaturedCategories } from "@/components/home/FeaturedCategories";
import { StoreSpecialCarousels } from "@/components/home/StoreSpecialCarousels";
import { TwoBanners } from "@/components/home/TwoBanners";
import { ThreeBanners } from "@/components/home/ThreeBanners";
import { AboutSanihome } from "@/components/home/AboutSanihome";
import { BlogCarousel } from "@/components/home/BlogCarousel";
import {
@@ -37,16 +35,15 @@ export default async function Home() {
<>
<HeroSlider images={heroImages} />
<FeaturedCategories categories={categories} images={featuredCategoryImages} />
<StoreSpecialCarousels groups={storeSpecials} contactPhone={contactPhone} />
<StoreSpecialCarousels
groups={storeSpecials}
contactPhone={contactPhone}
twoBannerImages={imagesForSlot(staticImageSlots, STATIC_IMAGE_KEYS.twoBanner)}
twoBannerAspectRatio={twoBannerSlot?.aspectRatio}
threeBannerImages={imagesForSlot(staticImageSlots, STATIC_IMAGE_KEYS.threeBannerRow)}
threeBannerAspectRatio={threeBannerSlot?.aspectRatio}
/>
<FeatureBar />
<TwoBanners
images={imagesForSlot(staticImageSlots, STATIC_IMAGE_KEYS.twoBanner)}
aspectRatio={twoBannerSlot?.aspectRatio}
/>
<ThreeBanners
images={imagesForSlot(staticImageSlots, STATIC_IMAGE_KEYS.threeBannerRow)}
aspectRatio={threeBannerSlot?.aspectRatio}
/>
<AboutSanihome />
<BlogCarousel posts={blogsResult.items} />
</>
+49 -1
View File
@@ -6,6 +6,7 @@ import {
getBusinessInfo,
getCategoryTree,
getProducts,
getStoreSpecials,
productHref,
} from "@/lib/meshkee";
@@ -25,10 +26,57 @@ function findCategoryLabel(tree: CategoryTreeGroup[], id: string): string | unde
export default async function ProductsPage({
searchParams,
}: {
searchParams: Promise<{ categoryId?: string; q?: string; page?: string }>;
searchParams: Promise<{ categoryId?: string; q?: string; page?: string; specialId?: string }>;
}) {
const params = await searchParams;
const page = Math.max(1, Number(params.page ?? 1) || 1);
// "مشاهده همه" from a homepage carousel — those are admin-curated,
// already-complete lists (GET /store-specials has no pagination), so show
// that exact group's items rather than re-querying the general catalog.
if (params.specialId) {
const groups = await getStoreSpecials();
const group = groups.find((g) => g.id === params.specialId);
const businessInfo = await getBusinessInfo();
const contactPhone = businessInfo?.phoneNumbers[0];
const items = group?.items ?? [];
return (
<div className="mx-auto max-w-7xl px-4 py-10">
<Breadcrumb
items={[
{ label: "خانه", href: "/" },
{ label: "محصولات", href: "/products" },
{ label: group?.title ?? "کروسل" },
]}
/>
<h1 className="mb-6 text-2xl font-bold text-red-dark">{group?.title ?? "محصولات"}</h1>
{items.length === 0 ? (
<EmptyState message="محصولی یافت نشد." />
) : (
<div className="grid grid-cols-2 gap-4 sm:grid-cols-3 lg:grid-cols-4">
{items.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)}
fluid
contactPhone={contactPhone}
variantId={product.variantId}
slug={product.slug}
/>
))}
</div>
)}
</div>
);
}
const [{ items, total }, businessInfo, categoryTree] = await Promise.all([
getProducts({
categoryId: params.categoryId,
+1
View File
@@ -17,6 +17,7 @@ export function BlogCarousel({ posts }: { posts: BlogSummary[] }) {
title="آخرین مطالب وبلاگ"
enTitle="Latest Blog"
action="وبلاگ"
actionHref="/blog"
/>
{posts.length === 0 ? (
<EmptyState message="هنوز مطلبی ثبت نشده است." />
+1 -1
View File
@@ -33,7 +33,7 @@ export function FeaturedCategories({
images?: StaticImageWithUrl[];
}) {
return (
<section className="py-10 md:py-14">
<section className="py-4 md:py-6">
<div className="mx-auto max-w-7xl px-4">
{images.length > 0 ? (
<Swiper
+1 -1
View File
@@ -27,7 +27,7 @@ function Logo({ src, large = false }: { src?: string | null; large?: boolean })
className={
large
? "h-[42px] w-auto object-contain md:h-[46px]"
: "h-8 w-auto md:h-9"
: "h-10 w-auto object-contain md:h-12"
}
/>
</a>
+50 -24
View File
@@ -1,7 +1,10 @@
import { Fragment } from "react";
import { Carousel } from "@/components/ui/Carousel";
import { ProductCard } from "@/components/ui/ProductCard";
import { SectionTitle } from "@/components/ui/SectionTitle";
import { productHref, type ProductGroup } from "@/lib/meshkee";
import { TwoBanners } from "@/components/home/TwoBanners";
import { ThreeBanners } from "@/components/home/ThreeBanners";
import { productHref, type ProductGroup, type StaticImageWithUrl } from "@/lib/meshkee";
// Renders every admin-managed carousel from GET /store-specials — whatever
// groups exist there, in their configured order. Nothing here is keyed off
@@ -9,40 +12,63 @@ import { productHref, type ProductGroup } from "@/lib/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).
//
// One of the two banner sections is inserted between every pair of
// carousels (never before the first or after the last), cycling if there
// end up being more than two carousels.
export function StoreSpecialCarousels({
groups,
contactPhone,
twoBannerImages,
twoBannerAspectRatio,
threeBannerImages,
threeBannerAspectRatio,
}: {
groups: ProductGroup[];
contactPhone?: string;
twoBannerImages?: StaticImageWithUrl[];
twoBannerAspectRatio?: string;
threeBannerImages?: StaticImageWithUrl[];
threeBannerAspectRatio?: string;
}) {
const visibleGroups = groups.filter((group) => group.items.length > 0);
const banners = [
<TwoBanners key="two" images={twoBannerImages} aspectRatio={twoBannerAspectRatio} />,
<ThreeBanners key="three" images={threeBannerImages} aspectRatio={threeBannerAspectRatio} />,
];
return (
<>
{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} />
<Carousel ariaLabel={`کاروسل ${group.title}`}>
{group.items.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>
{visibleGroups.map((group, i) => (
<Fragment key={group.id}>
{i > 0 && banners[(i - 1) % banners.length]}
<section className="py-10 md:py-14">
<div className="mx-auto max-w-7xl px-4">
<SectionTitle
title={group.title}
action="مشاهده همه"
actionHref={`/products?specialId=${group.id}`}
/>
<Carousel ariaLabel={`کاروسل ${group.title}`}>
{group.items.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>
</Fragment>
))}
</>
);
+6 -5
View File
@@ -3,9 +3,10 @@ type SectionTitleProps = {
enTitle?: string;
subtitle?: string;
action?: string;
actionHref?: string;
};
export function SectionTitle({ title, enTitle, subtitle, action }: SectionTitleProps) {
export function SectionTitle({ title, enTitle, subtitle, action, actionHref }: SectionTitleProps) {
return (
<div className="mb-6 flex items-end justify-between gap-4">
<div>
@@ -25,13 +26,13 @@ export function SectionTitle({ title, enTitle, subtitle, action }: SectionTitleP
<p className="mt-1 text-sm text-muted">{subtitle}</p>
)}
</div>
{action && (
<button
type="button"
{action && actionHref && (
<a
href={actionHref}
className="shrink-0 text-sm font-medium text-red transition-colors hover:text-red-dark"
>
{action} ←
</button>
</a>
)}
</div>
);