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:
co-authored by
Claude Sonnet 5
parent
bc6cafaffa
commit
7c04316f3a
+18
-1
@@ -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
@@ -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} />
|
||||
</>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -17,6 +17,7 @@ export function BlogCarousel({ posts }: { posts: BlogSummary[] }) {
|
||||
title="آخرین مطالب وبلاگ"
|
||||
enTitle="Latest Blog"
|
||||
action="وبلاگ"
|
||||
actionHref="/blog"
|
||||
/>
|
||||
{posts.length === 0 ? (
|
||||
<EmptyState message="هنوز مطلبی ثبت نشده است." />
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user