Wire homepage banners to Meshkee static-image slots.
Publish a slot catalog so the dashboard can import keys, and keep local placeholders when a slot is empty. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
co-authored by
Cursor
parent
46e4835360
commit
49b83223f1
+25
-10
@@ -1,8 +1,17 @@
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { SlotImage } from "@/components/SlotImage";
|
||||
import { SITE } from "@/lib/site";
|
||||
import type { StaticImageWithUrl } from "@/lib/static-images";
|
||||
import { slotAlt, slotAspectStyle } from "@/lib/static-images";
|
||||
|
||||
export function AboutUs() {
|
||||
export function AboutUs({
|
||||
image,
|
||||
aspectRatio,
|
||||
}: {
|
||||
image?: StaticImageWithUrl;
|
||||
aspectRatio?: string;
|
||||
}) {
|
||||
return (
|
||||
<section id="about" className="container-site mt-14">
|
||||
<div className="grid md:grid-cols-2 gap-8 items-center rounded-2xl bg-zinc-50 p-5 md:p-8">
|
||||
@@ -14,15 +23,21 @@ export function AboutUs() {
|
||||
مشاهده بیشتر
|
||||
</Link>
|
||||
</div>
|
||||
<div className="relative flex aspect-[16/10] items-center justify-center rounded-2xl bg-white p-8 md:p-12">
|
||||
<Image
|
||||
src="/images/logo/logo.png"
|
||||
alt={SITE.nameFa}
|
||||
width={360}
|
||||
height={120}
|
||||
className="h-auto w-full max-w-sm object-contain"
|
||||
/>
|
||||
</div>
|
||||
{image ? (
|
||||
<div className="relative overflow-hidden rounded-2xl bg-white" style={slotAspectStyle(aspectRatio, "8:5")}>
|
||||
<SlotImage image={image} fill alt={slotAlt(image, SITE.nameFa)} />
|
||||
</div>
|
||||
) : (
|
||||
<div className="relative flex aspect-[16/10] items-center justify-center rounded-2xl bg-white p-8 md:p-12">
|
||||
<Image
|
||||
src="/images/logo/logo.png"
|
||||
alt={SITE.nameFa}
|
||||
width={360}
|
||||
height={120}
|
||||
className="h-auto w-full max-w-sm object-contain"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
|
||||
+92
-28
@@ -1,47 +1,111 @@
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { SlotImage } from "@/components/SlotImage";
|
||||
import { banners3, banners4 } from "@/lib/data";
|
||||
import type { StaticImageWithUrl } from "@/lib/static-images";
|
||||
import { slotAlt, slotAspectStyle } from "@/lib/static-images";
|
||||
import { Media } from "./Media";
|
||||
|
||||
export function BannerGrid4() {
|
||||
const banner4Overlay = {
|
||||
background: "linear-gradient(to top, rgba(0,0,0,0.88) 0%, rgba(0,0,0,0.55) 45%, transparent 100%)",
|
||||
};
|
||||
|
||||
type BannerSlotProps = {
|
||||
images?: StaticImageWithUrl[];
|
||||
aspectRatio?: string;
|
||||
};
|
||||
|
||||
export function BannerGrid4({ images = [], aspectRatio }: BannerSlotProps) {
|
||||
return (
|
||||
<section className="container-site mt-12">
|
||||
<div className="grid grid-cols-2 gap-3 md:gap-4">
|
||||
{banners4.map((banner) => (
|
||||
<Link key={banner.href} href={banner.href} className="relative block overflow-hidden rounded-2xl">
|
||||
<div className="relative aspect-[21/9] min-h-[140px]">
|
||||
<Image src={banner.image} alt="" fill sizes="50vw" className="object-cover object-center" />
|
||||
</div>
|
||||
<div
|
||||
className="pointer-events-none absolute inset-x-0 bottom-0 h-[70%]"
|
||||
style={{
|
||||
background:
|
||||
"linear-gradient(to top, rgba(0,0,0,0.88) 0%, rgba(0,0,0,0.55) 45%, transparent 100%)",
|
||||
}}
|
||||
/>
|
||||
<div className="absolute inset-x-0 bottom-3 md:bottom-4 px-3 text-center text-white">
|
||||
<p className="text-base md:text-xl font-extrabold leading-none">{banner.title}</p>
|
||||
<p dir="ltr" className="mt-0.5 font-carter text-[9px] md:text-[10px] tracking-[0.4em] text-brand uppercase">
|
||||
{banner.english}
|
||||
</p>
|
||||
<p className="mt-0.5 text-sm md:text-base leading-tight text-white/90">{banner.subtitle}</p>
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
{images.length > 0
|
||||
? images.map((image) => {
|
||||
const title = image.titleFa?.trim();
|
||||
const english = image.titleEn?.trim();
|
||||
const subtitle = image.subtext?.trim();
|
||||
const hasCopy = Boolean(title || english || subtitle);
|
||||
const frame = (
|
||||
<div
|
||||
className="relative overflow-hidden rounded-2xl"
|
||||
style={slotAspectStyle(aspectRatio, "21:9")}
|
||||
>
|
||||
<SlotImage image={{ ...image, linkUrl: null }} fill alt={slotAlt(image)} />
|
||||
{hasCopy ? (
|
||||
<>
|
||||
<div className="pointer-events-none absolute inset-x-0 bottom-0 h-[70%]" style={banner4Overlay} />
|
||||
<div className="pointer-events-none absolute inset-x-0 bottom-3 md:bottom-4 px-3 text-center text-white">
|
||||
{title ? (
|
||||
<p className="text-base md:text-xl font-extrabold leading-none">{title}</p>
|
||||
) : null}
|
||||
{english ? (
|
||||
<p
|
||||
dir="ltr"
|
||||
className="mt-0.5 font-carter text-[9px] md:text-[10px] tracking-[0.4em] text-brand uppercase"
|
||||
>
|
||||
{english}
|
||||
</p>
|
||||
) : null}
|
||||
{subtitle ? (
|
||||
<p className="mt-0.5 text-sm md:text-base leading-tight text-white/90">{subtitle}</p>
|
||||
) : null}
|
||||
</div>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
return image.linkUrl ? (
|
||||
<a key={image.id} href={image.linkUrl} className="block">
|
||||
{frame}
|
||||
</a>
|
||||
) : (
|
||||
<div key={image.id}>{frame}</div>
|
||||
);
|
||||
})
|
||||
: banners4.map((banner) => (
|
||||
<Link key={banner.href} href={banner.href} className="relative block overflow-hidden rounded-2xl">
|
||||
<div className="relative aspect-[21/9] min-h-[140px]">
|
||||
<Image src={banner.image} alt={banner.title} fill sizes="50vw" className="object-cover object-center" />
|
||||
</div>
|
||||
<div className="pointer-events-none absolute inset-x-0 bottom-0 h-[70%]" style={banner4Overlay} />
|
||||
<div className="absolute inset-x-0 bottom-3 md:bottom-4 px-3 text-center text-white">
|
||||
<p className="text-base md:text-xl font-extrabold leading-none">{banner.title}</p>
|
||||
<p
|
||||
dir="ltr"
|
||||
className="mt-0.5 font-carter text-[9px] md:text-[10px] tracking-[0.4em] text-brand uppercase"
|
||||
>
|
||||
{banner.english}
|
||||
</p>
|
||||
<p className="mt-0.5 text-sm md:text-base leading-tight text-white/90">{banner.subtitle}</p>
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
export function BannerRow3() {
|
||||
export function BannerRow3({ images = [], aspectRatio }: BannerSlotProps) {
|
||||
return (
|
||||
<section className="container-site mt-12">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3 md:gap-4">
|
||||
{banners3.map((banner) => (
|
||||
<Link key={banner.href} href={banner.href} className="overflow-hidden rounded-2xl">
|
||||
<Media src={banner.image} alt="" className="aspect-[2.4/1]" sizes="33vw" />
|
||||
</Link>
|
||||
))}
|
||||
{images.length > 0
|
||||
? images.map((image) => (
|
||||
<div key={image.id} className="overflow-hidden rounded-2xl">
|
||||
<SlotImage
|
||||
image={image}
|
||||
alt={slotAlt(image)}
|
||||
imgClassName="h-auto w-full object-cover"
|
||||
aspectRatio={aspectRatio}
|
||||
fallbackAspect="12:5"
|
||||
/>
|
||||
</div>
|
||||
))
|
||||
: banners3.map((banner) => (
|
||||
<Link key={banner.href} href={banner.href} className="overflow-hidden rounded-2xl">
|
||||
<Media src={banner.image} alt={banner.alt} className="aspect-[2.4/1]" sizes="33vw" />
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
|
||||
+67
-23
@@ -1,9 +1,12 @@
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
import { SlotImage } from "@/components/SlotImage";
|
||||
import { categories } from "@/lib/data";
|
||||
import type { StaticImageWithUrl } from "@/lib/static-images";
|
||||
import { slotAlt, slotAspectStyle } from "@/lib/static-images";
|
||||
import { DrawFrame } from "./DrawFrame";
|
||||
|
||||
export function Categories() {
|
||||
export function Categories({ images = [] }: { images?: StaticImageWithUrl[] }) {
|
||||
return (
|
||||
<section className="container-site mt-12">
|
||||
<div className="flex items-end justify-between mb-6">
|
||||
@@ -13,28 +16,69 @@ export function Categories() {
|
||||
</Link>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3 md:gap-4">
|
||||
{categories.map((cat) => (
|
||||
<Link
|
||||
key={cat.slug}
|
||||
href={`/products/${cat.slug}`}
|
||||
className="draw-frame-host relative flex items-center gap-3 rounded-2xl border border-black/5 bg-zinc-50 px-3 py-2 text-brand"
|
||||
>
|
||||
<Image
|
||||
src={cat.image}
|
||||
alt={cat.name}
|
||||
width={88}
|
||||
height={88}
|
||||
className="size-[72px] sm:size-20 shrink-0 object-contain"
|
||||
/>
|
||||
<span className="min-w-0">
|
||||
<span className="block text-lg font-medium text-ink leading-none">{cat.name}</span>
|
||||
<span dir="ltr" className="mt-1 block font-oswald text-[10px] font-normal tracking-[0.32em] text-zinc-400 uppercase">
|
||||
{cat.english}
|
||||
</span>
|
||||
</span>
|
||||
<DrawFrame />
|
||||
</Link>
|
||||
))}
|
||||
{images.length > 0
|
||||
? images.map((image) => {
|
||||
const label = slotAlt(image);
|
||||
const card = (
|
||||
<div className="draw-frame-host relative flex items-center gap-3 rounded-2xl border border-black/5 bg-zinc-50 px-3 py-2 text-brand">
|
||||
<div className="size-[72px] sm:size-20 shrink-0 overflow-hidden" style={slotAspectStyle("1:1")}>
|
||||
<SlotImage
|
||||
image={{ ...image, linkUrl: null }}
|
||||
alt={label}
|
||||
imgClassName="h-full w-full object-contain"
|
||||
/>
|
||||
</div>
|
||||
{label || image.subtext ? (
|
||||
<span className="min-w-0">
|
||||
{label ? (
|
||||
<span className="block text-lg font-medium text-ink leading-none">{label}</span>
|
||||
) : null}
|
||||
{image.subtext ? (
|
||||
<span
|
||||
dir="ltr"
|
||||
className="mt-1 block font-oswald text-[10px] font-normal tracking-[0.32em] text-zinc-400 uppercase"
|
||||
>
|
||||
{image.subtext}
|
||||
</span>
|
||||
) : null}
|
||||
</span>
|
||||
) : null}
|
||||
<DrawFrame />
|
||||
</div>
|
||||
);
|
||||
return image.linkUrl ? (
|
||||
<a key={image.id} href={image.linkUrl}>
|
||||
{card}
|
||||
</a>
|
||||
) : (
|
||||
<div key={image.id}>{card}</div>
|
||||
);
|
||||
})
|
||||
: categories.map((cat) => (
|
||||
<Link
|
||||
key={cat.slug}
|
||||
href={`/products/${cat.slug}`}
|
||||
className="draw-frame-host relative flex items-center gap-3 rounded-2xl border border-black/5 bg-zinc-50 px-3 py-2 text-brand"
|
||||
>
|
||||
<Image
|
||||
src={cat.image}
|
||||
alt={cat.name}
|
||||
width={88}
|
||||
height={88}
|
||||
className="size-[72px] sm:size-20 shrink-0 object-contain"
|
||||
/>
|
||||
<span className="min-w-0">
|
||||
<span className="block text-lg font-medium text-ink leading-none">{cat.name}</span>
|
||||
<span
|
||||
dir="ltr"
|
||||
className="mt-1 block font-oswald text-[10px] font-normal tracking-[0.32em] text-zinc-400 uppercase"
|
||||
>
|
||||
{cat.english}
|
||||
</span>
|
||||
</span>
|
||||
<DrawFrame />
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
|
||||
+210
-71
@@ -3,103 +3,242 @@
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { useEffect, useState } from "react";
|
||||
import { SlotImage } from "@/components/SlotImage";
|
||||
import { heroSides, heroSlides } from "@/lib/data";
|
||||
import type { StaticImageWithUrl } from "@/lib/static-images";
|
||||
import { slotAlt, slotAspectStyle } from "@/lib/static-images";
|
||||
|
||||
const overlayStyle = {
|
||||
background:
|
||||
"linear-gradient(to left, rgba(0,0,0,0.88) 0%, rgba(0,0,0,0.72) 42%, rgba(0,0,0,0.28) 72%, transparent 100%)",
|
||||
};
|
||||
|
||||
export function HeroSlider() {
|
||||
type Props = {
|
||||
images?: StaticImageWithUrl[];
|
||||
aspectRatio?: string;
|
||||
sideImages?: StaticImageWithUrl[];
|
||||
sideAspectRatio?: string;
|
||||
};
|
||||
|
||||
export function HeroSlider({
|
||||
images = [],
|
||||
aspectRatio,
|
||||
sideImages = [],
|
||||
sideAspectRatio,
|
||||
}: Props) {
|
||||
const slides = images;
|
||||
const [index, setIndex] = useState(0);
|
||||
const [paused, setPaused] = useState(false);
|
||||
const count = slides.length > 0 ? slides.length : heroSlides.length;
|
||||
|
||||
useEffect(() => {
|
||||
if (paused) return;
|
||||
setIndex((current) => (count ? current % count : 0));
|
||||
}, [count]);
|
||||
|
||||
useEffect(() => {
|
||||
if (paused || count < 2) return;
|
||||
const id = window.setInterval(() => {
|
||||
setIndex((current) => (current + 1) % heroSlides.length);
|
||||
setIndex((current) => (current + 1) % count);
|
||||
}, 5000);
|
||||
return () => window.clearInterval(id);
|
||||
}, [paused]);
|
||||
}, [paused, count]);
|
||||
|
||||
const sliderStyle = slotAspectStyle(aspectRatio, "21:8");
|
||||
|
||||
return (
|
||||
<section className="container-site pt-5">
|
||||
<div
|
||||
className="relative overflow-hidden rounded-2xl aspect-[21/8] min-h-[240px] md:min-h-[360px]"
|
||||
className="relative overflow-hidden rounded-2xl min-h-[240px] md:min-h-[360px]"
|
||||
style={sliderStyle ?? { aspectRatio: "21 / 8" }}
|
||||
onMouseEnter={() => setPaused(true)}
|
||||
onMouseLeave={() => setPaused(false)}
|
||||
>
|
||||
{heroSlides.map((slide, i) => (
|
||||
<div
|
||||
key={slide.title}
|
||||
aria-hidden={i !== index}
|
||||
className={`absolute inset-0 transition-opacity duration-700 ease-in-out ${
|
||||
i === index ? "opacity-100 z-[1]" : "opacity-0 z-0 pointer-events-none"
|
||||
}`}
|
||||
>
|
||||
<Image
|
||||
src={slide.image}
|
||||
alt=""
|
||||
fill
|
||||
sizes="1200px"
|
||||
priority={i === 0}
|
||||
className="object-cover object-[28%_center]"
|
||||
{slides.length > 0
|
||||
? slides.map((slide, i) => {
|
||||
const title = slide.titleFa?.trim();
|
||||
const english = slide.titleEn?.trim();
|
||||
const subtitle = slide.subtext?.trim();
|
||||
const hasCopy = Boolean(title || english || subtitle);
|
||||
return (
|
||||
<div
|
||||
key={slide.id}
|
||||
aria-hidden={i !== index}
|
||||
className={`absolute inset-0 transition-opacity duration-700 ease-in-out ${
|
||||
i === index ? "opacity-100 z-[1]" : "opacity-0 z-0 pointer-events-none"
|
||||
}`}
|
||||
>
|
||||
<SlotImage image={slide} fill alt={slotAlt(slide, `اسلاید ${i + 1}`)} />
|
||||
{hasCopy ? (
|
||||
<>
|
||||
<div
|
||||
className="pointer-events-none absolute inset-y-0 right-0 z-[2] w-[58%] md:w-[46%]"
|
||||
style={overlayStyle}
|
||||
/>
|
||||
<div className="pointer-events-none absolute z-[3] right-6 md:right-12 top-1/2 -translate-y-1/2 max-w-[230px] sm:max-w-sm text-white text-right">
|
||||
{title ? (
|
||||
<p className="text-2xl sm:text-3xl md:text-4xl font-extrabold leading-none">{title}</p>
|
||||
) : null}
|
||||
{english ? (
|
||||
<p
|
||||
dir="ltr"
|
||||
className="mt-0.5 font-carter text-[10px] sm:text-xs md:text-sm tracking-[0.42em] text-brand uppercase"
|
||||
>
|
||||
{english}
|
||||
</p>
|
||||
) : null}
|
||||
{subtitle ? (
|
||||
<p className="mt-1 text-lg sm:text-xl md:text-2xl font-medium leading-tight text-white/90">
|
||||
{subtitle}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
})
|
||||
: heroSlides.map((slide, i) => (
|
||||
<div
|
||||
key={slide.title}
|
||||
aria-hidden={i !== index}
|
||||
className={`absolute inset-0 transition-opacity duration-700 ease-in-out ${
|
||||
i === index ? "opacity-100 z-[1]" : "opacity-0 z-0 pointer-events-none"
|
||||
}`}
|
||||
>
|
||||
<Image
|
||||
src={slide.image}
|
||||
alt={slide.title}
|
||||
fill
|
||||
sizes="1200px"
|
||||
priority={i === 0}
|
||||
className="object-cover object-[28%_center]"
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{slides.length === 0 ? (
|
||||
<>
|
||||
<div
|
||||
className="pointer-events-none absolute inset-y-0 right-0 z-[2] w-[58%] md:w-[46%]"
|
||||
style={overlayStyle}
|
||||
/>
|
||||
{heroSlides.map((slide, i) => (
|
||||
<div
|
||||
key={`${slide.title}-copy`}
|
||||
className={`absolute z-[3] right-6 md:right-12 top-1/2 -translate-y-1/2 max-w-[230px] sm:max-w-sm text-white text-right transition-opacity duration-700 ${
|
||||
i === index ? "opacity-100" : "opacity-0 pointer-events-none"
|
||||
}`}
|
||||
>
|
||||
<p className="text-2xl sm:text-3xl md:text-4xl font-extrabold leading-none">{slide.title}</p>
|
||||
<p
|
||||
dir="ltr"
|
||||
className="mt-0.5 font-carter text-[10px] sm:text-xs md:text-sm tracking-[0.42em] text-brand uppercase"
|
||||
>
|
||||
{slide.english}
|
||||
</p>
|
||||
<p className="mt-1 text-lg sm:text-xl md:text-2xl font-medium leading-tight text-white/90">
|
||||
{slide.subtitle}
|
||||
</p>
|
||||
<Link
|
||||
href={slide.href}
|
||||
className="mt-5 inline-flex items-center justify-center rounded-lg bg-brand px-5 py-2.5 text-sm sm:text-base font-bold text-white hover:bg-[#e01e1e]"
|
||||
>
|
||||
{slide.button}
|
||||
</Link>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
) : null}
|
||||
|
||||
{count > 1 ? (
|
||||
<div className="absolute bottom-4 left-1/2 z-[4] -translate-x-1/2 flex gap-2">
|
||||
{Array.from({ length: count }, (_, i) => (
|
||||
<button
|
||||
key={slides[i]?.id ?? heroSlides[i]?.title ?? i}
|
||||
type="button"
|
||||
aria-label={`اسلاید ${i + 1}`}
|
||||
onClick={() => setIndex(i)}
|
||||
className={`h-2.5 rounded-full transition-all ${i === index ? "w-7 bg-brand" : "w-2.5 bg-white/80"}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
|
||||
<div className="pointer-events-none absolute inset-y-0 right-0 z-[2] w-[58%] md:w-[46%]" style={overlayStyle} />
|
||||
|
||||
{heroSlides.map((slide, i) => (
|
||||
<div
|
||||
key={`${slide.title}-copy`}
|
||||
className={`absolute z-[3] right-6 md:right-12 top-1/2 -translate-y-1/2 max-w-[230px] sm:max-w-sm text-white text-right transition-opacity duration-700 ${
|
||||
i === index ? "opacity-100" : "opacity-0 pointer-events-none"
|
||||
}`}
|
||||
>
|
||||
<p className="text-2xl sm:text-3xl md:text-4xl font-extrabold leading-none">{slide.title}</p>
|
||||
<p dir="ltr" className="mt-0.5 font-carter text-[10px] sm:text-xs md:text-sm tracking-[0.42em] text-brand uppercase">
|
||||
{slide.english}
|
||||
</p>
|
||||
<p className="mt-1 text-lg sm:text-xl md:text-2xl font-medium leading-tight text-white/90">{slide.subtitle}</p>
|
||||
<Link
|
||||
href={slide.href}
|
||||
className="mt-5 inline-flex items-center justify-center rounded-lg bg-brand px-5 py-2.5 text-sm sm:text-base font-bold text-white hover:bg-[#e01e1e]"
|
||||
>
|
||||
{slide.button}
|
||||
</Link>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<div className="absolute bottom-4 left-1/2 z-[4] -translate-x-1/2 flex gap-2">
|
||||
{heroSlides.map((slide, i) => (
|
||||
<button
|
||||
key={slide.title}
|
||||
type="button"
|
||||
aria-label={`اسلاید ${i + 1}`}
|
||||
onClick={() => setIndex(i)}
|
||||
className={`h-2.5 rounded-full transition-all ${i === index ? "w-7 bg-brand" : "w-2.5 bg-white/80"}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className="mt-4 grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
{heroSides.map((side) => (
|
||||
<Link key={side.title} href={side.href} className="relative block overflow-hidden rounded-2xl">
|
||||
<div className="relative aspect-[21/8] sm:aspect-[2.6/1]">
|
||||
<Image src={side.image} alt="" fill sizes="600px" className="object-cover object-[28%_center]" />
|
||||
</div>
|
||||
<div className="pointer-events-none absolute inset-y-0 right-0 w-[62%] md:w-[52%]" style={overlayStyle} />
|
||||
<div className="absolute right-4 md:right-6 top-1/2 -translate-y-1/2 max-w-[180px] md:max-w-[220px] text-white text-right">
|
||||
<p className="text-base md:text-xl font-extrabold leading-none">{side.title}</p>
|
||||
<p dir="ltr" className="mt-0.5 font-carter text-[9px] md:text-[10px] tracking-[0.4em] text-brand uppercase">
|
||||
{side.english}
|
||||
</p>
|
||||
<p className="mt-0.5 text-sm md:text-base leading-tight text-white/90">{side.subtitle}</p>
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
{sideImages.length > 0
|
||||
? sideImages.map((image) => {
|
||||
const title = image.titleFa?.trim();
|
||||
const english = image.titleEn?.trim();
|
||||
const subtitle = image.subtext?.trim();
|
||||
const hasCopy = Boolean(title || english || subtitle);
|
||||
const frame = (
|
||||
<div
|
||||
className="relative overflow-hidden rounded-2xl"
|
||||
style={slotAspectStyle(sideAspectRatio, "13:5")}
|
||||
>
|
||||
<SlotImage image={{ ...image, linkUrl: null }} fill alt={slotAlt(image)} />
|
||||
{hasCopy ? (
|
||||
<>
|
||||
<div
|
||||
className="pointer-events-none absolute inset-y-0 right-0 w-[62%] md:w-[52%]"
|
||||
style={overlayStyle}
|
||||
/>
|
||||
<div className="pointer-events-none absolute right-4 md:right-6 top-1/2 -translate-y-1/2 max-w-[180px] md:max-w-[220px] text-white text-right">
|
||||
{title ? (
|
||||
<p className="text-base md:text-xl font-extrabold leading-none">{title}</p>
|
||||
) : null}
|
||||
{english ? (
|
||||
<p
|
||||
dir="ltr"
|
||||
className="mt-0.5 font-carter text-[9px] md:text-[10px] tracking-[0.4em] text-brand uppercase"
|
||||
>
|
||||
{english}
|
||||
</p>
|
||||
) : null}
|
||||
{subtitle ? (
|
||||
<p className="mt-0.5 text-sm md:text-base leading-tight text-white/90">{subtitle}</p>
|
||||
) : null}
|
||||
</div>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
return image.linkUrl ? (
|
||||
<a key={image.id} href={image.linkUrl} className="block">
|
||||
{frame}
|
||||
</a>
|
||||
) : (
|
||||
<div key={image.id}>{frame}</div>
|
||||
);
|
||||
})
|
||||
: heroSides.map((side) => (
|
||||
<Link key={side.title} href={side.href} className="relative block overflow-hidden rounded-2xl">
|
||||
<div className="relative aspect-[21/8] sm:aspect-[2.6/1]">
|
||||
<Image
|
||||
src={side.image}
|
||||
alt={side.title}
|
||||
fill
|
||||
sizes="600px"
|
||||
className="object-cover object-[28%_center]"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="pointer-events-none absolute inset-y-0 right-0 w-[62%] md:w-[52%]"
|
||||
style={overlayStyle}
|
||||
/>
|
||||
<div className="absolute right-4 md:right-6 top-1/2 -translate-y-1/2 max-w-[180px] md:max-w-[220px] text-white text-right">
|
||||
<p className="text-base md:text-xl font-extrabold leading-none">{side.title}</p>
|
||||
<p
|
||||
dir="ltr"
|
||||
className="mt-0.5 font-carter text-[9px] md:text-[10px] tracking-[0.4em] text-brand uppercase"
|
||||
>
|
||||
{side.english}
|
||||
</p>
|
||||
<p className="mt-0.5 text-sm md:text-base leading-tight text-white/90">{side.subtitle}</p>
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import type { StaticImageWithUrl } from "@/lib/static-images";
|
||||
import { slotAlt, slotAspectStyle } from "@/lib/static-images";
|
||||
|
||||
type Props = {
|
||||
image: StaticImageWithUrl;
|
||||
alt?: string;
|
||||
className?: string;
|
||||
imgClassName?: string;
|
||||
aspectRatio?: string;
|
||||
fallbackAspect?: string;
|
||||
fill?: boolean;
|
||||
};
|
||||
|
||||
export function SlotImage({
|
||||
image,
|
||||
alt,
|
||||
className = "",
|
||||
imgClassName,
|
||||
aspectRatio,
|
||||
fallbackAspect,
|
||||
fill = false,
|
||||
}: Props) {
|
||||
const style = fill ? undefined : slotAspectStyle(aspectRatio, fallbackAspect);
|
||||
const img = (
|
||||
// Slot URLs come from Meshkee CDN and may not be in next/image remotePatterns.
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img
|
||||
src={image.url}
|
||||
alt={slotAlt(image, alt)}
|
||||
style={style}
|
||||
className={
|
||||
imgClassName ??
|
||||
(fill
|
||||
? "absolute inset-0 h-full w-full object-cover"
|
||||
: "h-auto w-full object-cover")
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
if (!image.linkUrl) {
|
||||
return className ? <div className={className}>{img}</div> : img;
|
||||
}
|
||||
|
||||
return (
|
||||
<a href={image.linkUrl} className={className || (fill ? "absolute inset-0 block" : "block")}>
|
||||
{img}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user