115 lines
4.5 KiB
TypeScript
115 lines
4.5 KiB
TypeScript
"use client";
|
|
|
|
import Image from "next/image";
|
|
import Link from "next/link";
|
|
import type { CSSProperties } from "react";
|
|
import { Autoplay, Navigation, Pagination } from "swiper/modules";
|
|
import { Swiper, SwiperSlide } from "swiper/react";
|
|
import "swiper/css";
|
|
import "swiper/css/pagination";
|
|
import { ChevronLeft, ChevronRight } from "./icons";
|
|
import { toCssAspectRatio } from "@/lib/format";
|
|
import type { StaticImageSlot } from "@/lib/types";
|
|
|
|
export interface Slide {
|
|
id: string;
|
|
image: string;
|
|
mobileImage?: string;
|
|
href: string;
|
|
alt: string;
|
|
}
|
|
|
|
// Default aspect ratios match the fallback banner assets: 700x400 mobile art,
|
|
// 1296x366 desktop, so neither crop loses the headline. Kept in sync with the
|
|
// "slider" entry in src/app/meshkee/static-image-slots/route.ts — see the
|
|
// note there: if this ratio ever changes, that entry must change with it.
|
|
const FALLBACK_ASPECT_CLASSES = "aspect-[7/4] md:aspect-[1296/366]";
|
|
|
|
// `slot` is the "slider" static-image slot (kind: list, duplicatable) — when
|
|
// it has images they win over the `slides` fallback prop entirely. A CMS
|
|
// slide is one image for every breakpoint (not a separate mobile crop), and
|
|
// slot.aspectRatio (if set) drives the frame instead of the built-in default.
|
|
export default function HeroSlider({ slot, slides = [] }: { slot?: StaticImageSlot; slides?: Slide[] }) {
|
|
const fromCms = (slot?.images.length ?? 0) > 0;
|
|
|
|
const items: Slide[] = fromCms
|
|
? slot!.images.map((img, i) => ({
|
|
id: img.id,
|
|
image: img.url,
|
|
href: img.linkUrl || "/products",
|
|
alt: img.titleFa || img.titleEn || `بنر فروشگاه موبایل معلم ${i + 1}`,
|
|
}))
|
|
: slides;
|
|
|
|
if (items.length === 0) return null;
|
|
|
|
const cssAspect = fromCms ? toCssAspectRatio(slot?.aspectRatio) : undefined;
|
|
const aspectStyle = cssAspect ? ({ aspectRatio: cssAspect } as CSSProperties) : undefined;
|
|
|
|
return (
|
|
<section className="boxed mt-5" aria-label="بنرهای اصلی">
|
|
<div className="relative overflow-hidden rounded-2xl">
|
|
<Swiper
|
|
modules={[Pagination, Navigation, Autoplay]}
|
|
dir="rtl"
|
|
loop={items.length > 1}
|
|
autoplay={{ delay: 5000, disableOnInteraction: false }}
|
|
pagination={{ clickable: true, el: ".hero-dots" }}
|
|
navigation={{ prevEl: ".hero-prev", nextEl: ".hero-next" }}
|
|
className={`w-full ${aspectStyle ? "" : FALLBACK_ASPECT_CLASSES}`}
|
|
style={aspectStyle}
|
|
>
|
|
{items.map((slide) => (
|
|
<SwiperSlide key={slide.id}>
|
|
<Link href={slide.href} className="relative block size-full">
|
|
{fromCms ? (
|
|
<Image src={slide.image} alt={slide.alt} fill priority sizes="100vw" className="object-cover" />
|
|
) : (
|
|
<>
|
|
<Image
|
|
src={slide.mobileImage || slide.image}
|
|
alt={slide.alt}
|
|
fill
|
|
priority
|
|
sizes="100vw"
|
|
className="object-cover md:hidden"
|
|
/>
|
|
<Image
|
|
src={slide.image}
|
|
alt={slide.alt}
|
|
fill
|
|
priority
|
|
sizes="1200px"
|
|
className="hidden object-cover md:block"
|
|
/>
|
|
</>
|
|
)}
|
|
</Link>
|
|
</SwiperSlide>
|
|
))}
|
|
</Swiper>
|
|
|
|
<div className="absolute bottom-4 left-4 z-10 flex items-center gap-2">
|
|
<button
|
|
className="hero-prev flex size-9 items-center justify-center rounded-full bg-white/90 text-ink shadow transition hover:bg-white"
|
|
aria-label="اسلاید قبلی"
|
|
>
|
|
<ChevronRight className="size-5" />
|
|
</button>
|
|
<button
|
|
className="hero-next flex size-9 items-center justify-center rounded-full bg-white/90 text-ink shadow transition hover:bg-white"
|
|
aria-label="اسلاید بعدی"
|
|
>
|
|
<ChevronLeft className="size-5" />
|
|
</button>
|
|
</div>
|
|
|
|
{/* Swiper's own .swiper-pagination rules set left:0/width:100% on this
|
|
element, so it is centred with justify-content rather than a
|
|
translate — a translate would fight those and push it off-canvas. */}
|
|
<div className="hero-dots absolute bottom-5 z-10 flex w-full justify-center gap-1.5" />
|
|
</div>
|
|
</section>
|
|
);
|
|
}
|