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 ( +
+ +

{group?.title ?? "محصولات"}

+ {items.length === 0 ? ( + + ) : ( +
+ {items.map((product) => ( + + ))} +
+ )} +
+ ); + } + const [{ items, total }, businessInfo, categoryTree] = await Promise.all([ getProducts({ categoryId: params.categoryId, diff --git a/src/components/home/BlogCarousel.tsx b/src/components/home/BlogCarousel.tsx index 76bb23c..68ba3c0 100644 --- a/src/components/home/BlogCarousel.tsx +++ b/src/components/home/BlogCarousel.tsx @@ -17,6 +17,7 @@ export function BlogCarousel({ posts }: { posts: BlogSummary[] }) { title="آخرین مطالب وبلاگ" enTitle="Latest Blog" action="وبلاگ" + actionHref="/blog" /> {posts.length === 0 ? ( diff --git a/src/components/home/FeaturedCategories.tsx b/src/components/home/FeaturedCategories.tsx index 9cecae2..dbbdc0e 100644 --- a/src/components/home/FeaturedCategories.tsx +++ b/src/components/home/FeaturedCategories.tsx @@ -33,7 +33,7 @@ export function FeaturedCategories({ images?: StaticImageWithUrl[]; }) { 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) => ( -
-
- - - {group.items.map((product) => ( - - ))} - -
-
+ {visibleGroups.map((group, i) => ( + + {i > 0 && banners[(i - 1) % banners.length]} +
+
+ + + {group.items.map((product) => ( + + ))} + +
+
+
))} ); diff --git a/src/components/ui/SectionTitle.tsx b/src/components/ui/SectionTitle.tsx index 4339b4f..b60da2b 100644 --- a/src/components/ui/SectionTitle.tsx +++ b/src/components/ui/SectionTitle.tsx @@ -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 (
@@ -25,13 +26,13 @@ export function SectionTitle({ title, enTitle, subtitle, action }: SectionTitleP

{subtitle}

)}
- {action && ( - + )}
);