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, itemCount: 3,
recommendedWidth: 520, 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 ( return (
<main> <main>
<HeroSlider slot={findSlot(slots, "slider")} /> <HeroSlider slot={findSlot(slots, "slider")} />
<SpecialSaleCarousel items={specialItems} /> <SpecialSaleCarousel items={specialItems} bannerSlot={findSlot(slots, "sale-banner")} />
<ThreeBanners slot={findSlot(slots, "three-banner-row")} /> <ThreeBanners slot={findSlot(slots, "three-banner-row")} />
<TwoBanners slot={findSlot(slots, "two-banner-row")} /> <TwoBanners slot={findSlot(slots, "two-banner-row")} />
<TopSellingCarousel items={topSellingItems} /> <TopSellingCarousel items={topSellingItems} />
+66 -45
View File
@@ -4,13 +4,27 @@ import Link from "next/link";
import { Swiper, SwiperSlide } from "swiper/react"; import { Swiper, SwiperSlide } from "swiper/react";
import { Autoplay, Navigation } from "swiper/modules"; import { Autoplay, Navigation } from "swiper/modules";
import "swiper/css"; 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 ProductCard from "./ui/ProductCard";
import { ChevronLeftIcon, ChevronRightIcon } from "./icons"; 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; 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 ( return (
<section id="special-sale" className="boxed py-10 md:py-14"> <section id="special-sale" className="boxed py-10 md:py-14">
<div className="flex items-center justify-between mb-6 md:mb-8"> <div className="flex items-center justify-between mb-6 md:mb-8">
@@ -32,51 +46,58 @@ export default function SpecialSaleCarousel({ items }: { items: SpecialCarouselI
</Link> </Link>
</div> </div>
<div className="relative bg-brand rounded-2xl md:rounded-3xl p-4 md:p-8"> <div className="bg-brand rounded-2xl md:rounded-3xl p-4 md:p-8 flex gap-4 md:gap-6">
<Swiper {/* Fixed on the right — stays put while the products scroll beside it. */}
modules={[Autoplay, Navigation]} <BannerWrapper
autoplay={{ delay: 3200, disableOnInteraction: false }} {...(bannerHref ? { href: bannerHref } : {})}
navigation={{ className="shrink-0 block w-32 sm:w-40 md:w-56 overflow-hidden rounded-lg"
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 },
}}
> >
<SwiperSlide> <img
<img src={bannerSrc}
src="/images/special-sale/banner.svg" alt=""
alt="" style={bannerAspectStyle}
className="w-full h-full aspect-[4/5] object-cover rounded-lg" className={`w-full h-full object-cover ${bannerAspectStyle ? "" : "aspect-[4/5]"}`}
/> />
</SwiperSlide> </BannerWrapper>
{items.map(({ product, discount }) => (
<SwiperSlide key={product.id}>
<ProductCard product={product} discount={discount} />
</SwiperSlide>
))}
</Swiper>
<button <div className="relative flex-1 min-w-0">
type="button" <Swiper
aria-label="اسلاید قبلی" modules={[Autoplay, Navigation]}
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" autoplay={{ delay: 3200, disableOnInteraction: false }}
> navigation={{
<ChevronRightIcon className="size-5" /> prevEl: ".sale-prev",
</button> nextEl: ".sale-next",
<button }}
type="button" spaceBetween={14}
aria-label="اسلاید بعدی" slidesPerView={1.4}
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" breakpoints={{
> 480: { slidesPerView: 1.8 },
<ChevronLeftIcon className="size-5" /> 640: { slidesPerView: 2.4 },
</button> 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> </div>
</section> </section>
); );
+2 -2
View File
@@ -48,7 +48,7 @@ export default function ProductCard({
<div className="flex flex-1 flex-col p-3.5"> <div className="flex flex-1 flex-col p-3.5">
{/* Fixed height regardless of 1 or 2 lines, so card heights stay {/* Fixed height regardless of 1 or 2 lines, so card heights stay
consistent across a row of mixed-length titles. */} 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} {title}
</h3> </h3>
@@ -69,7 +69,7 @@ export default function ProductCard({
)} )}
</div> </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"> <span className="font-bold text-sm md:text-[15px] text-brand truncate">
{price != null ? formatToman(price) : "تماس بگیرید"} {price != null ? formatToman(price) : "تماس بگیرید"}
</span> </span>