Fix sale-banner slot, mobile width, and product card height

- Pull the special-discount carousel's banner out of the swiper into a
  fixed sidebar (no longer scrolls with the product slides) and drop
  carousel looping
- Wire it to a new sale-banner static-image slot
- Fix an invalid xs: breakpoint that was silently keeping the banner
  at its smallest width on all phone sizes
- Tighten ProductCard's title height to match 2 lines of text instead
  of reserving extra space, and drop a redundant top margin on the
  price row that was inflating card height
This commit is contained in:
amirhosein.ashourloo
2026-08-24 10:37:28 +03:30
parent 9965e567b8
commit f10af62573
4 changed files with 78 additions and 48 deletions
@@ -52,6 +52,15 @@ export async function GET() {
itemCount: 3,
recommendedWidth: 520,
},
{
key: "sale-banner",
label: "بنر ثابت کروسل تخفیف‌های ویژه",
kind: "single",
pageKey: "home",
aspectRatio: "4:5",
itemCount: 1,
recommendedWidth: 480,
},
],
});
}
+1 -1
View File
@@ -77,7 +77,7 @@ export default async function Home() {
return (
<main>
<HeroSlider slot={findSlot(slots, "slider")} />
<SpecialSaleCarousel items={specialItems} />
<SpecialSaleCarousel items={specialItems} bannerSlot={findSlot(slots, "sale-banner")} />
<ThreeBanners slot={findSlot(slots, "three-banner-row")} />
<TwoBanners slot={findSlot(slots, "two-banner-row")} />
<TopSellingCarousel items={topSellingItems} />
+66 -45
View File
@@ -4,13 +4,27 @@ import Link from "next/link";
import { Swiper, SwiperSlide } from "swiper/react";
import { Autoplay, Navigation } from "swiper/modules";
import "swiper/css";
import type { SpecialCarouselItem } from "@/lib/types";
import type { SpecialCarouselItem, StaticImageSlot } from "@/lib/types";
import { toCssAspectRatio } from "@/lib/format";
import ProductCard from "./ui/ProductCard";
import { ChevronLeftIcon, ChevronRightIcon } from "./icons";
export default function SpecialSaleCarousel({ items }: { items: SpecialCarouselItem[] }) {
export default function SpecialSaleCarousel({
items,
bannerSlot,
}: {
items: SpecialCarouselItem[];
bannerSlot?: StaticImageSlot;
}) {
if (items.length === 0) return null;
const bannerImage = bannerSlot?.images[0];
const cssAspectRatio = toCssAspectRatio(bannerSlot?.aspectRatio);
const bannerAspectStyle = cssAspectRatio ? { aspectRatio: cssAspectRatio } : undefined;
const bannerSrc = bannerImage?.url ?? "/images/special-sale/banner.svg";
const bannerHref = bannerImage?.linkUrl;
const BannerWrapper = bannerHref ? "a" : "div";
return (
<section id="special-sale" className="boxed py-10 md:py-14">
<div className="flex items-center justify-between mb-6 md:mb-8">
@@ -32,51 +46,58 @@ export default function SpecialSaleCarousel({ items }: { items: SpecialCarouselI
</Link>
</div>
<div className="relative bg-brand rounded-2xl md:rounded-3xl p-4 md:p-8">
<Swiper
modules={[Autoplay, Navigation]}
autoplay={{ delay: 3200, disableOnInteraction: false }}
navigation={{
prevEl: ".sale-prev",
nextEl: ".sale-next",
}}
loop={items.length > 5}
spaceBetween={14}
slidesPerView={1.8}
breakpoints={{
480: { slidesPerView: 2.2 },
640: { slidesPerView: 3 },
1024: { slidesPerView: 4.5, spaceBetween: 20 },
}}
<div className="bg-brand rounded-2xl md:rounded-3xl p-4 md:p-8 flex gap-4 md:gap-6">
{/* Fixed on the right — stays put while the products scroll beside it. */}
<BannerWrapper
{...(bannerHref ? { href: bannerHref } : {})}
className="shrink-0 block w-32 sm:w-40 md:w-56 overflow-hidden rounded-lg"
>
<SwiperSlide>
<img
src="/images/special-sale/banner.svg"
alt=""
className="w-full h-full aspect-[4/5] object-cover rounded-lg"
/>
</SwiperSlide>
{items.map(({ product, discount }) => (
<SwiperSlide key={product.id}>
<ProductCard product={product} discount={discount} />
</SwiperSlide>
))}
</Swiper>
<img
src={bannerSrc}
alt=""
style={bannerAspectStyle}
className={`w-full h-full object-cover ${bannerAspectStyle ? "" : "aspect-[4/5]"}`}
/>
</BannerWrapper>
<button
type="button"
aria-label="اسلاید قبلی"
className="sale-prev hidden sm:flex absolute top-1/2 -translate-y-1/2 start-0 z-10 translate-x-1/2 items-center justify-center size-10 md:size-11 rounded-full bg-white text-brand shadow-md hover:bg-gold hover:text-ink transition-colors"
>
<ChevronRightIcon className="size-5" />
</button>
<button
type="button"
aria-label="اسلاید بعدی"
className="sale-next hidden sm:flex absolute top-1/2 -translate-y-1/2 end-0 z-10 -translate-x-1/2 items-center justify-center size-10 md:size-11 rounded-full bg-white text-brand shadow-md hover:bg-gold hover:text-ink transition-colors"
>
<ChevronLeftIcon className="size-5" />
</button>
<div className="relative flex-1 min-w-0">
<Swiper
modules={[Autoplay, Navigation]}
autoplay={{ delay: 3200, disableOnInteraction: false }}
navigation={{
prevEl: ".sale-prev",
nextEl: ".sale-next",
}}
spaceBetween={14}
slidesPerView={1.4}
breakpoints={{
480: { slidesPerView: 1.8 },
640: { slidesPerView: 2.4 },
1024: { slidesPerView: 3.5, spaceBetween: 20 },
}}
>
{items.map(({ product, discount }) => (
<SwiperSlide key={product.id}>
<ProductCard product={product} discount={discount} />
</SwiperSlide>
))}
</Swiper>
<button
type="button"
aria-label="اسلاید قبلی"
className="sale-prev hidden sm:flex absolute top-1/2 -translate-y-1/2 start-0 z-10 translate-x-1/2 items-center justify-center size-10 md:size-11 rounded-full bg-white text-brand shadow-md hover:bg-gold hover:text-ink transition-colors"
>
<ChevronRightIcon className="size-5" />
</button>
<button
type="button"
aria-label="اسلاید بعدی"
className="sale-next hidden sm:flex absolute top-1/2 -translate-y-1/2 end-0 z-10 -translate-x-1/2 items-center justify-center size-10 md:size-11 rounded-full bg-white text-brand shadow-md hover:bg-gold hover:text-ink transition-colors"
>
<ChevronLeftIcon className="size-5" />
</button>
</div>
</div>
</section>
);
+2 -2
View File
@@ -48,7 +48,7 @@ export default function ProductCard({
<div className="flex flex-1 flex-col p-3.5">
{/* Fixed height regardless of 1 or 2 lines, so card heights stay
consistent across a row of mixed-length titles. */}
<h3 className="h-10 md:h-11 leading-tight text-[13px] md:text-sm text-ink text-right line-clamp-2">
<h3 className="h-9 md:h-10 leading-tight text-[13px] md:text-sm text-ink text-right line-clamp-2">
{title}
</h3>
@@ -69,7 +69,7 @@ export default function ProductCard({
)}
</div>
<div className="mt-2 flex items-center justify-between gap-2">
<div className="flex items-center justify-between gap-2">
<span className="font-bold text-sm md:text-[15px] text-brand truncate">
{price != null ? formatToman(price) : "تماس بگیرید"}
</span>