diff --git a/CONTEXT.md b/CONTEXT.md
index f0b1473..17b013e 100644
--- a/CONTEXT.md
+++ b/CONTEXT.md
@@ -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 |
diff --git a/src/app/page.tsx b/src/app/page.tsx
index 55f5a3e..fd31add 100644
--- a/src/app/page.tsx
+++ b/src/app/page.tsx
@@ -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() {
<>
-
+
-
-
>
diff --git a/src/app/products/page.tsx b/src/app/products/page.tsx
index 9cd52e2..8910c16 100644
--- a/src/app/products/page.tsx
+++ b/src/app/products/page.tsx
@@ -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 (
+
{images.length > 0 ? (
diff --git a/src/components/home/StoreSpecialCarousels.tsx b/src/components/home/StoreSpecialCarousels.tsx
index 717fc3d..db4a0dd 100644
--- a/src/components/home/StoreSpecialCarousels.tsx
+++ b/src/components/home/StoreSpecialCarousels.tsx
@@ -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 = [
+ ,
+ ,
+ ];
return (
<>
- {visibleGroups.map((group) => (
-
-