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:
@@ -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
@@ -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} />
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user