Wire homepage images to Meshkee slots and publish the catalog.
The dashboard can refresh these keys from the live site. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
co-authored by
Cursor
parent
1272977923
commit
1c2d1eb31c
+1
-1
@@ -1,7 +1,7 @@
|
||||
import type { NextConfig } from "next";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
/* config options here */
|
||||
allowedDevOrigins: ["sanihome.com", "localhost"],
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { STATIC_IMAGE_SLOT_CATALOG } from "@/lib/meshkee";
|
||||
|
||||
export function GET() {
|
||||
return NextResponse.json(
|
||||
{ slots: STATIC_IMAGE_SLOT_CATALOG },
|
||||
{
|
||||
headers: {
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
"Cache-Control": "public, max-age=60",
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
+17
-5
@@ -11,18 +11,30 @@ import { SamsungCarousel } from "@/components/home/SamsungCarousel";
|
||||
import { AboutSanihome } from "@/components/home/AboutSanihome";
|
||||
import { BlogCarousel } from "@/components/home/BlogCarousel";
|
||||
import { Footer } from "@/components/home/Footer";
|
||||
import {
|
||||
STATIC_IMAGE_KEYS,
|
||||
firstImageForSlot,
|
||||
getStaticImageSlots,
|
||||
imagesForSlot,
|
||||
} from "@/lib/meshkee";
|
||||
|
||||
export default async function Home() {
|
||||
const slots = await getStaticImageSlots();
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<>
|
||||
<TopBar />
|
||||
<Header />
|
||||
<main>
|
||||
<HeroSlider />
|
||||
<SpecialSaleCarousel />
|
||||
<HeroSlider images={imagesForSlot(slots, STATIC_IMAGE_KEYS.slider)} />
|
||||
<SpecialSaleCarousel
|
||||
banner={firstImageForSlot(slots, STATIC_IMAGE_KEYS.bannerInSpecialSale)}
|
||||
/>
|
||||
<FeatureBar />
|
||||
<FeaturedCategories />
|
||||
<TwoBanners />
|
||||
<FeaturedCategories
|
||||
images={imagesForSlot(slots, STATIC_IMAGE_KEYS.categories)}
|
||||
/>
|
||||
<TwoBanners images={imagesForSlot(slots, STATIC_IMAGE_KEYS.twoBannerRow)} />
|
||||
<TopSellingCarousel />
|
||||
<ThreeBanners />
|
||||
<SamsungCarousel />
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
import { Placeholder } from "@/components/ui/Placeholder";
|
||||
import { SectionTitle } from "@/components/ui/SectionTitle";
|
||||
import { SlotImage } from "@/components/ui/SlotImage";
|
||||
import { categories } from "@/data/sample";
|
||||
import type { StaticImageWithUrl } from "@/lib/meshkee";
|
||||
|
||||
export function FeaturedCategories() {
|
||||
type FeaturedCategoriesProps = {
|
||||
images?: StaticImageWithUrl[];
|
||||
};
|
||||
|
||||
export function FeaturedCategories({ images = [] }: FeaturedCategoriesProps) {
|
||||
return (
|
||||
<section className="py-10 md:py-14">
|
||||
<div className="mx-auto max-w-7xl px-4">
|
||||
@@ -12,27 +18,47 @@ export function FeaturedCategories() {
|
||||
action="همه دستهها"
|
||||
/>
|
||||
<div className="grid grid-cols-2 gap-3 sm:grid-cols-4 lg:grid-cols-8">
|
||||
{categories.map((cat, i) => (
|
||||
<a
|
||||
key={cat.id}
|
||||
href="#"
|
||||
className="animate-slide-in group flex flex-col items-center rounded-xl border border-border bg-surface p-3 transition-all hover:border-red hover:shadow-md"
|
||||
style={{ animationDelay: `${i * 60}ms` }}
|
||||
>
|
||||
<Placeholder
|
||||
src={cat.image}
|
||||
alt={cat.name}
|
||||
width="72px"
|
||||
height="72px"
|
||||
label="72×72"
|
||||
className="mb-3 rounded-full transition-colors group-hover:border-red"
|
||||
/>
|
||||
<span className="text-center text-xs font-medium leading-relaxed md:text-sm">
|
||||
{cat.name}
|
||||
</span>
|
||||
<span className="mt-1 text-[11px] text-muted">{cat.count} محصول</span>
|
||||
</a>
|
||||
))}
|
||||
{images.length > 0
|
||||
? images.map((image, i) => (
|
||||
<div
|
||||
key={image.id}
|
||||
className="animate-slide-in group flex flex-col items-center rounded-xl border border-border bg-surface p-3 transition-all hover:border-red hover:shadow-md"
|
||||
style={{ animationDelay: `${i * 60}ms` }}
|
||||
>
|
||||
<SlotImage
|
||||
url={image.url}
|
||||
title={image.title}
|
||||
linkUrl={image.linkUrl}
|
||||
imgClassName="mb-3 h-[72px] w-[72px] rounded-full object-cover transition-colors group-hover:border-red"
|
||||
/>
|
||||
{image.title ? (
|
||||
<span className="text-center text-xs font-medium leading-relaxed md:text-sm">
|
||||
{image.title}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
))
|
||||
: categories.map((cat, i) => (
|
||||
<a
|
||||
key={cat.id}
|
||||
href="#"
|
||||
className="animate-slide-in group flex flex-col items-center rounded-xl border border-border bg-surface p-3 transition-all hover:border-red hover:shadow-md"
|
||||
style={{ animationDelay: `${i * 60}ms` }}
|
||||
>
|
||||
<Placeholder
|
||||
src={cat.image}
|
||||
alt={cat.name}
|
||||
width="72px"
|
||||
height="72px"
|
||||
label="72×72"
|
||||
className="mb-3 rounded-full transition-colors group-hover:border-red"
|
||||
/>
|
||||
<span className="text-center text-xs font-medium leading-relaxed md:text-sm">
|
||||
{cat.name}
|
||||
</span>
|
||||
<span className="mt-1 text-[11px] text-muted">{cat.count} محصول</span>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -2,15 +2,34 @@
|
||||
|
||||
import { useState } from "react";
|
||||
import { Placeholder } from "@/components/ui/Placeholder";
|
||||
import { SlotImage } from "@/components/ui/SlotImage";
|
||||
import { sliderSlides } from "@/data/sample";
|
||||
import type { StaticImageWithUrl } from "@/lib/meshkee";
|
||||
|
||||
export function HeroSlider() {
|
||||
const [active, setActive] = useState(1);
|
||||
type HeroSliderProps = {
|
||||
images?: StaticImageWithUrl[];
|
||||
};
|
||||
|
||||
const getIndex = (offset: number) => {
|
||||
const len = sliderSlides.length;
|
||||
return (active + offset + len) % len;
|
||||
};
|
||||
export function HeroSlider({ images = [] }: HeroSliderProps) {
|
||||
const remote = images.length > 0;
|
||||
const slides = remote
|
||||
? images.map((image) => ({
|
||||
id: image.id,
|
||||
title: image.title ?? "",
|
||||
src: image.url,
|
||||
linkUrl: image.linkUrl,
|
||||
}))
|
||||
: sliderSlides.map((slide) => ({
|
||||
id: String(slide.id),
|
||||
title: slide.title,
|
||||
src: slide.image,
|
||||
linkUrl: undefined as string | null | undefined,
|
||||
}));
|
||||
|
||||
const [active, setActive] = useState(0);
|
||||
const len = slides.length;
|
||||
|
||||
const getIndex = (offset: number) => (active + offset + len) % len;
|
||||
|
||||
const positions = [
|
||||
{ offset: -1, scale: "scale-[0.92]", opacity: "opacity-55", z: "z-10" },
|
||||
@@ -24,7 +43,30 @@ export function HeroSlider() {
|
||||
<div className="relative h-[220px] sm:h-[280px] md:h-[340px] lg:h-[380px]">
|
||||
{positions.map(({ offset, scale, opacity, z }) => {
|
||||
const isCenter = offset === 0;
|
||||
const slide = sliderSlides[getIndex(offset)];
|
||||
if (!isCenter && len < 2) return null;
|
||||
const slide = slides[getIndex(offset)];
|
||||
|
||||
const heightClass = `w-full h-[200px] sm:h-[260px] md:h-[310px] ${
|
||||
isCenter ? "lg:h-[380px]" : "lg:h-[350px]"
|
||||
}`;
|
||||
|
||||
const media = remote ? (
|
||||
<SlotImage
|
||||
url={slide.src}
|
||||
title={slide.title}
|
||||
linkUrl={isCenter ? slide.linkUrl : undefined}
|
||||
className="block h-full"
|
||||
imgClassName={`${heightClass} object-cover`}
|
||||
/>
|
||||
) : (
|
||||
<Placeholder
|
||||
src={slide.src}
|
||||
alt={slide.title}
|
||||
width="100%"
|
||||
label={isCenter ? "1280×380" : "880×350"}
|
||||
className={`rounded-none border-0 ${heightClass}`}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -38,15 +80,7 @@ export function HeroSlider() {
|
||||
} ${isCenter ? "" : "pointer-events-none"}`}
|
||||
>
|
||||
<div className="overflow-hidden rounded-2xl border border-border bg-surface shadow-lg">
|
||||
<Placeholder
|
||||
src={slide.image}
|
||||
alt={slide.title}
|
||||
width="100%"
|
||||
label={isCenter ? "1280×380" : "880×350"}
|
||||
className={`w-full rounded-none border-0 h-[200px] sm:h-[260px] md:h-[310px] ${
|
||||
isCenter ? "lg:h-[380px]" : "lg:h-[350px]"
|
||||
}`}
|
||||
/>
|
||||
{media}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -56,16 +90,14 @@ export function HeroSlider() {
|
||||
<div className="mt-5 flex items-center justify-center gap-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
setActive((prev) => (prev - 1 + sliderSlides.length) % sliderSlides.length)
|
||||
}
|
||||
onClick={() => setActive((prev) => (prev - 1 + len) % len)}
|
||||
className="flex h-9 w-9 items-center justify-center rounded-full border border-border text-red transition-colors hover:border-red hover:bg-red-light"
|
||||
aria-label="اسلاید قبلی"
|
||||
>
|
||||
→
|
||||
</button>
|
||||
<div className="flex gap-2">
|
||||
{sliderSlides.map((slide, i) => (
|
||||
{slides.map((slide, i) => (
|
||||
<button
|
||||
key={slide.id}
|
||||
type="button"
|
||||
@@ -79,7 +111,7 @@ export function HeroSlider() {
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setActive((prev) => (prev + 1) % sliderSlides.length)}
|
||||
onClick={() => setActive((prev) => (prev + 1) % len)}
|
||||
className="flex h-9 w-9 items-center justify-center rounded-full border border-border text-red transition-colors hover:border-red hover:bg-red-light"
|
||||
aria-label="اسلاید بعدی"
|
||||
>
|
||||
|
||||
@@ -2,14 +2,35 @@ import { Carousel } from "@/components/ui/Carousel";
|
||||
import { Placeholder } from "@/components/ui/Placeholder";
|
||||
import { ProductCard } from "@/components/ui/ProductCard";
|
||||
import { SectionTitle } from "@/components/ui/SectionTitle";
|
||||
import { SlotImage } from "@/components/ui/SlotImage";
|
||||
import { specialSaleProducts } from "@/data/sample";
|
||||
import type { StaticImageWithUrl } from "@/lib/meshkee";
|
||||
|
||||
type SpecialSaleCarouselProps = {
|
||||
banner?: StaticImageWithUrl;
|
||||
};
|
||||
|
||||
function SpecialSaleBanner({ banner }: { banner?: StaticImageWithUrl }) {
|
||||
const frameClassName =
|
||||
"relative w-[220px] shrink-0 self-stretch overflow-hidden rounded-xl border border-border md:w-[240px]";
|
||||
|
||||
if (banner) {
|
||||
return (
|
||||
<div className={frameClassName}>
|
||||
<SlotImage
|
||||
url={banner.url}
|
||||
title={banner.title}
|
||||
linkUrl={banner.linkUrl}
|
||||
alt={banner.title ?? "فروش ویژه"}
|
||||
className="block h-full"
|
||||
imgClassName="h-full min-h-[380px] w-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function SpecialSaleBanner() {
|
||||
return (
|
||||
<a
|
||||
href="#"
|
||||
className="relative w-[220px] shrink-0 self-stretch overflow-hidden rounded-xl border border-border md:w-[240px]"
|
||||
>
|
||||
<a href="#" className={frameClassName}>
|
||||
<Placeholder
|
||||
src="/images/sale-banner.svg"
|
||||
alt="فروش ویژه"
|
||||
@@ -21,7 +42,7 @@ function SpecialSaleBanner() {
|
||||
);
|
||||
}
|
||||
|
||||
export function SpecialSaleCarousel() {
|
||||
export function SpecialSaleCarousel({ banner }: SpecialSaleCarouselProps) {
|
||||
return (
|
||||
<section className="border-y border-red-light bg-red-light/30 py-10 md:py-14">
|
||||
<div className="mx-auto max-w-7xl px-4">
|
||||
@@ -31,7 +52,7 @@ export function SpecialSaleCarousel() {
|
||||
action="مشاهده همه"
|
||||
/>
|
||||
<Carousel ariaLabel="کاروسل فروش ویژه">
|
||||
<SpecialSaleBanner />
|
||||
<SpecialSaleBanner banner={banner} />
|
||||
{specialSaleProducts.map((product) => (
|
||||
<ProductCard
|
||||
key={product.id}
|
||||
|
||||
@@ -1,50 +1,71 @@
|
||||
import { Placeholder } from "@/components/ui/Placeholder";
|
||||
import { SlotImage } from "@/components/ui/SlotImage";
|
||||
import type { StaticImageWithUrl } from "@/lib/meshkee";
|
||||
|
||||
export function TwoBanners() {
|
||||
type TwoBannersProps = {
|
||||
images?: StaticImageWithUrl[];
|
||||
};
|
||||
|
||||
const fallbackBanners = [
|
||||
{
|
||||
src: "/images/banner-1.svg",
|
||||
alt: "تخفیف ویژه یخچال و فریزر",
|
||||
kicker: "Summer Sale",
|
||||
title: "تخفیف ویژه یخچال و فریزر",
|
||||
subtitle: "تا ۲۵٪ تخفیف — تا پایان مرداد",
|
||||
},
|
||||
{
|
||||
src: "/images/banner-2.svg",
|
||||
alt: "تلویزیونهای هوشمند ۲۰۲۶",
|
||||
kicker: "Smart TV",
|
||||
title: "تلویزیونهای هوشمند ۲۰۲۶",
|
||||
subtitle: "QLED و OLED با اقساط ۱۲ ماهه",
|
||||
},
|
||||
];
|
||||
|
||||
export function TwoBanners({ images = [] }: TwoBannersProps) {
|
||||
return (
|
||||
<section className="py-10 md:py-14">
|
||||
<div className="mx-auto max-w-7xl px-4">
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<a
|
||||
href="#"
|
||||
className="group relative overflow-hidden rounded-2xl border border-border bg-surface transition-shadow hover:shadow-lg"
|
||||
>
|
||||
<Placeholder
|
||||
src="/images/banner-1.svg"
|
||||
alt="تخفیف ویژه یخچال و فریزر"
|
||||
width="100%"
|
||||
height="200px"
|
||||
label="640×200"
|
||||
className="w-full rounded-none border-0"
|
||||
/>
|
||||
<div className="absolute inset-0 flex flex-col justify-end bg-gradient-to-t from-red-dark/80 to-transparent p-6">
|
||||
<p className="font-display text-xs tracking-widest text-red-muted uppercase">
|
||||
Summer Sale
|
||||
</p>
|
||||
<h3 className="text-lg font-bold text-white">تخفیف ویژه یخچال و فریزر</h3>
|
||||
<p className="mt-1 text-sm text-white/80">تا ۲۵٪ تخفیف — تا پایان مرداد</p>
|
||||
</div>
|
||||
</a>
|
||||
<a
|
||||
href="#"
|
||||
className="group relative overflow-hidden rounded-2xl border border-border bg-surface transition-shadow hover:shadow-lg"
|
||||
>
|
||||
<Placeholder
|
||||
src="/images/banner-2.svg"
|
||||
alt="تلویزیونهای هوشمند ۲۰۲۶"
|
||||
width="100%"
|
||||
height="200px"
|
||||
label="640×200"
|
||||
className="w-full rounded-none border-0"
|
||||
/>
|
||||
<div className="absolute inset-0 flex flex-col justify-end bg-gradient-to-t from-red-dark/80 to-transparent p-6">
|
||||
<p className="font-display text-xs tracking-widest text-red-muted uppercase">
|
||||
Smart TV
|
||||
</p>
|
||||
<h3 className="text-lg font-bold text-white">تلویزیونهای هوشمند ۲۰۲۶</h3>
|
||||
<p className="mt-1 text-sm text-white/80">QLED و OLED با اقساط ۱۲ ماهه</p>
|
||||
</div>
|
||||
</a>
|
||||
{images.length > 0
|
||||
? images.map((image) => (
|
||||
<div
|
||||
key={image.id}
|
||||
className="group relative overflow-hidden rounded-2xl border border-border bg-surface transition-shadow hover:shadow-lg"
|
||||
>
|
||||
<SlotImage
|
||||
url={image.url}
|
||||
title={image.title}
|
||||
linkUrl={image.linkUrl}
|
||||
className="block"
|
||||
imgClassName="block h-[200px] w-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
))
|
||||
: fallbackBanners.map((banner) => (
|
||||
<a
|
||||
key={banner.src}
|
||||
href="#"
|
||||
className="group relative overflow-hidden rounded-2xl border border-border bg-surface transition-shadow hover:shadow-lg"
|
||||
>
|
||||
<Placeholder
|
||||
src={banner.src}
|
||||
alt={banner.alt}
|
||||
width="100%"
|
||||
height="200px"
|
||||
label="640×200"
|
||||
className="w-full rounded-none border-0"
|
||||
/>
|
||||
<div className="absolute inset-0 flex flex-col justify-end bg-gradient-to-t from-red-dark/80 to-transparent p-6">
|
||||
<p className="font-display text-xs tracking-widest text-red-muted uppercase">
|
||||
{banner.kicker}
|
||||
</p>
|
||||
<h3 className="text-lg font-bold text-white">{banner.title}</h3>
|
||||
<p className="mt-1 text-sm text-white/80">{banner.subtitle}</p>
|
||||
</div>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
type SlotImageProps = {
|
||||
url: string;
|
||||
title?: string | null;
|
||||
linkUrl?: string | null;
|
||||
alt?: string;
|
||||
className?: string;
|
||||
imgClassName?: string;
|
||||
};
|
||||
|
||||
export function SlotImage({
|
||||
url,
|
||||
title,
|
||||
linkUrl,
|
||||
alt,
|
||||
className,
|
||||
imgClassName = "block h-full w-full object-cover",
|
||||
}: SlotImageProps) {
|
||||
const img = (
|
||||
// Meshkee slot URLs are used as-is; do not route through next/image.
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img src={url} alt={alt ?? title ?? ""} className={imgClassName} />
|
||||
);
|
||||
|
||||
if (linkUrl) {
|
||||
return (
|
||||
<a href={linkUrl} className={className}>
|
||||
{img}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
if (className) {
|
||||
return <div className={className}>{img}</div>;
|
||||
}
|
||||
|
||||
return img;
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
export type StaticImage = {
|
||||
id: string;
|
||||
url?: string | null;
|
||||
title?: string | null;
|
||||
linkUrl?: string | null;
|
||||
width?: number | null;
|
||||
height?: number | null;
|
||||
sortOrder?: number;
|
||||
};
|
||||
|
||||
export type StaticImageWithUrl = StaticImage & { url: string };
|
||||
|
||||
export type StaticImageSlot = {
|
||||
id: string;
|
||||
key: string;
|
||||
kind: "single" | "list";
|
||||
label?: string;
|
||||
aspectRatio?: string;
|
||||
recommendedWidth?: number;
|
||||
sortOrder?: number;
|
||||
images?: StaticImage[];
|
||||
};
|
||||
|
||||
export const STATIC_IMAGE_KEYS = {
|
||||
slider: "slider",
|
||||
bannerInSpecialSale: "banner-in-special-sale",
|
||||
categories: "categories",
|
||||
twoBannerRow: "two-banner-row",
|
||||
} as const;
|
||||
|
||||
export const STATIC_IMAGE_SLOT_CATALOG = [
|
||||
{
|
||||
key: STATIC_IMAGE_KEYS.slider,
|
||||
label: "Homepage slider",
|
||||
kind: "list" as const,
|
||||
pageKey: "home",
|
||||
aspectRatio: "16:9",
|
||||
itemCount: null,
|
||||
recommendedWidth: 1440,
|
||||
},
|
||||
{
|
||||
key: STATIC_IMAGE_KEYS.bannerInSpecialSale,
|
||||
label: "Banner in special sale",
|
||||
kind: "single" as const,
|
||||
pageKey: "home",
|
||||
aspectRatio: "12:19",
|
||||
itemCount: 1,
|
||||
recommendedWidth: 480,
|
||||
},
|
||||
{
|
||||
key: STATIC_IMAGE_KEYS.categories,
|
||||
label: "Featured category images",
|
||||
kind: "list" as const,
|
||||
pageKey: "home",
|
||||
aspectRatio: "1:1",
|
||||
itemCount: null,
|
||||
recommendedWidth: 400,
|
||||
},
|
||||
{
|
||||
key: STATIC_IMAGE_KEYS.twoBannerRow,
|
||||
label: "Two banner row",
|
||||
kind: "list" as const,
|
||||
pageKey: "home",
|
||||
aspectRatio: "16:5",
|
||||
itemCount: 2,
|
||||
recommendedWidth: 1440,
|
||||
},
|
||||
];
|
||||
|
||||
const API_BASE = process.env.NEXT_PUBLIC_API_BASE_URL ?? "";
|
||||
const DOMAIN = process.env.NEXT_PUBLIC_DOMAIN ?? "";
|
||||
|
||||
export async function getStaticImageSlots(): Promise<StaticImageSlot[]> {
|
||||
if (!API_BASE || !DOMAIN) return [];
|
||||
|
||||
try {
|
||||
const res = await fetch(
|
||||
`${API_BASE}/tenants/${DOMAIN}/website/static-images`,
|
||||
{ cache: "no-store" },
|
||||
);
|
||||
if (!res.ok) return [];
|
||||
const data: { items?: StaticImageSlot[] } = await res.json();
|
||||
return data.items ?? [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export function imagesForSlot(
|
||||
slots: StaticImageSlot[],
|
||||
key: string,
|
||||
): StaticImageWithUrl[] {
|
||||
const images = slots.find((slot) => slot.key === key)?.images ?? [];
|
||||
return [...images]
|
||||
.filter((image): image is StaticImageWithUrl => Boolean(image.url))
|
||||
.sort((a, b) => (a.sortOrder ?? 0) - (b.sortOrder ?? 0));
|
||||
}
|
||||
|
||||
export function firstImageForSlot(
|
||||
slots: StaticImageSlot[],
|
||||
key: string,
|
||||
): StaticImageWithUrl | undefined {
|
||||
return imagesForSlot(slots, key)[0];
|
||||
}
|
||||
Reference in New Issue
Block a user