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";
|
import type { NextConfig } from "next";
|
||||||
|
|
||||||
const nextConfig: NextConfig = {
|
const nextConfig: NextConfig = {
|
||||||
/* config options here */
|
allowedDevOrigins: ["sanihome.com", "localhost"],
|
||||||
};
|
};
|
||||||
|
|
||||||
export default nextConfig;
|
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 { AboutSanihome } from "@/components/home/AboutSanihome";
|
||||||
import { BlogCarousel } from "@/components/home/BlogCarousel";
|
import { BlogCarousel } from "@/components/home/BlogCarousel";
|
||||||
import { Footer } from "@/components/home/Footer";
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<TopBar />
|
<TopBar />
|
||||||
<Header />
|
<Header />
|
||||||
<main>
|
<main>
|
||||||
<HeroSlider />
|
<HeroSlider images={imagesForSlot(slots, STATIC_IMAGE_KEYS.slider)} />
|
||||||
<SpecialSaleCarousel />
|
<SpecialSaleCarousel
|
||||||
|
banner={firstImageForSlot(slots, STATIC_IMAGE_KEYS.bannerInSpecialSale)}
|
||||||
|
/>
|
||||||
<FeatureBar />
|
<FeatureBar />
|
||||||
<FeaturedCategories />
|
<FeaturedCategories
|
||||||
<TwoBanners />
|
images={imagesForSlot(slots, STATIC_IMAGE_KEYS.categories)}
|
||||||
|
/>
|
||||||
|
<TwoBanners images={imagesForSlot(slots, STATIC_IMAGE_KEYS.twoBannerRow)} />
|
||||||
<TopSellingCarousel />
|
<TopSellingCarousel />
|
||||||
<ThreeBanners />
|
<ThreeBanners />
|
||||||
<SamsungCarousel />
|
<SamsungCarousel />
|
||||||
|
|||||||
@@ -1,8 +1,14 @@
|
|||||||
import { Placeholder } from "@/components/ui/Placeholder";
|
import { Placeholder } from "@/components/ui/Placeholder";
|
||||||
import { SectionTitle } from "@/components/ui/SectionTitle";
|
import { SectionTitle } from "@/components/ui/SectionTitle";
|
||||||
|
import { SlotImage } from "@/components/ui/SlotImage";
|
||||||
import { categories } from "@/data/sample";
|
import { categories } from "@/data/sample";
|
||||||
|
import type { StaticImageWithUrl } from "@/lib/meshkee";
|
||||||
|
|
||||||
export function FeaturedCategories() {
|
type FeaturedCategoriesProps = {
|
||||||
|
images?: StaticImageWithUrl[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export function FeaturedCategories({ images = [] }: FeaturedCategoriesProps) {
|
||||||
return (
|
return (
|
||||||
<section className="py-10 md:py-14">
|
<section className="py-10 md:py-14">
|
||||||
<div className="mx-auto max-w-7xl px-4">
|
<div className="mx-auto max-w-7xl px-4">
|
||||||
@@ -12,27 +18,47 @@ export function FeaturedCategories() {
|
|||||||
action="همه دستهها"
|
action="همه دستهها"
|
||||||
/>
|
/>
|
||||||
<div className="grid grid-cols-2 gap-3 sm:grid-cols-4 lg:grid-cols-8">
|
<div className="grid grid-cols-2 gap-3 sm:grid-cols-4 lg:grid-cols-8">
|
||||||
{categories.map((cat, i) => (
|
{images.length > 0
|
||||||
<a
|
? images.map((image, i) => (
|
||||||
key={cat.id}
|
<div
|
||||||
href="#"
|
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"
|
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` }}
|
style={{ animationDelay: `${i * 60}ms` }}
|
||||||
>
|
>
|
||||||
<Placeholder
|
<SlotImage
|
||||||
src={cat.image}
|
url={image.url}
|
||||||
alt={cat.name}
|
title={image.title}
|
||||||
width="72px"
|
linkUrl={image.linkUrl}
|
||||||
height="72px"
|
imgClassName="mb-3 h-[72px] w-[72px] rounded-full object-cover transition-colors group-hover:border-red"
|
||||||
label="72×72"
|
/>
|
||||||
className="mb-3 rounded-full transition-colors group-hover:border-red"
|
{image.title ? (
|
||||||
/>
|
<span className="text-center text-xs font-medium leading-relaxed md:text-sm">
|
||||||
<span className="text-center text-xs font-medium leading-relaxed md:text-sm">
|
{image.title}
|
||||||
{cat.name}
|
</span>
|
||||||
</span>
|
) : null}
|
||||||
<span className="mt-1 text-[11px] text-muted">{cat.count} محصول</span>
|
</div>
|
||||||
</a>
|
))
|
||||||
))}
|
: 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>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -2,15 +2,34 @@
|
|||||||
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Placeholder } from "@/components/ui/Placeholder";
|
import { Placeholder } from "@/components/ui/Placeholder";
|
||||||
|
import { SlotImage } from "@/components/ui/SlotImage";
|
||||||
import { sliderSlides } from "@/data/sample";
|
import { sliderSlides } from "@/data/sample";
|
||||||
|
import type { StaticImageWithUrl } from "@/lib/meshkee";
|
||||||
|
|
||||||
export function HeroSlider() {
|
type HeroSliderProps = {
|
||||||
const [active, setActive] = useState(1);
|
images?: StaticImageWithUrl[];
|
||||||
|
};
|
||||||
|
|
||||||
const getIndex = (offset: number) => {
|
export function HeroSlider({ images = [] }: HeroSliderProps) {
|
||||||
const len = sliderSlides.length;
|
const remote = images.length > 0;
|
||||||
return (active + offset + len) % len;
|
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 = [
|
const positions = [
|
||||||
{ offset: -1, scale: "scale-[0.92]", opacity: "opacity-55", z: "z-10" },
|
{ 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]">
|
<div className="relative h-[220px] sm:h-[280px] md:h-[340px] lg:h-[380px]">
|
||||||
{positions.map(({ offset, scale, opacity, z }) => {
|
{positions.map(({ offset, scale, opacity, z }) => {
|
||||||
const isCenter = offset === 0;
|
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 (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -38,15 +80,7 @@ export function HeroSlider() {
|
|||||||
} ${isCenter ? "" : "pointer-events-none"}`}
|
} ${isCenter ? "" : "pointer-events-none"}`}
|
||||||
>
|
>
|
||||||
<div className="overflow-hidden rounded-2xl border border-border bg-surface shadow-lg">
|
<div className="overflow-hidden rounded-2xl border border-border bg-surface shadow-lg">
|
||||||
<Placeholder
|
{media}
|
||||||
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]"
|
|
||||||
}`}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -56,16 +90,14 @@ export function HeroSlider() {
|
|||||||
<div className="mt-5 flex items-center justify-center gap-3">
|
<div className="mt-5 flex items-center justify-center gap-3">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() =>
|
onClick={() => setActive((prev) => (prev - 1 + len) % len)}
|
||||||
setActive((prev) => (prev - 1 + sliderSlides.length) % sliderSlides.length)
|
|
||||||
}
|
|
||||||
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"
|
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="اسلاید قبلی"
|
aria-label="اسلاید قبلی"
|
||||||
>
|
>
|
||||||
→
|
→
|
||||||
</button>
|
</button>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
{sliderSlides.map((slide, i) => (
|
{slides.map((slide, i) => (
|
||||||
<button
|
<button
|
||||||
key={slide.id}
|
key={slide.id}
|
||||||
type="button"
|
type="button"
|
||||||
@@ -79,7 +111,7 @@ export function HeroSlider() {
|
|||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
type="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"
|
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="اسلاید بعدی"
|
aria-label="اسلاید بعدی"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -2,14 +2,35 @@ import { Carousel } from "@/components/ui/Carousel";
|
|||||||
import { Placeholder } from "@/components/ui/Placeholder";
|
import { Placeholder } from "@/components/ui/Placeholder";
|
||||||
import { ProductCard } from "@/components/ui/ProductCard";
|
import { ProductCard } from "@/components/ui/ProductCard";
|
||||||
import { SectionTitle } from "@/components/ui/SectionTitle";
|
import { SectionTitle } from "@/components/ui/SectionTitle";
|
||||||
|
import { SlotImage } from "@/components/ui/SlotImage";
|
||||||
import { specialSaleProducts } from "@/data/sample";
|
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 (
|
return (
|
||||||
<a
|
<a href="#" className={frameClassName}>
|
||||||
href="#"
|
|
||||||
className="relative w-[220px] shrink-0 self-stretch overflow-hidden rounded-xl border border-border md:w-[240px]"
|
|
||||||
>
|
|
||||||
<Placeholder
|
<Placeholder
|
||||||
src="/images/sale-banner.svg"
|
src="/images/sale-banner.svg"
|
||||||
alt="فروش ویژه"
|
alt="فروش ویژه"
|
||||||
@@ -21,7 +42,7 @@ function SpecialSaleBanner() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SpecialSaleCarousel() {
|
export function SpecialSaleCarousel({ banner }: SpecialSaleCarouselProps) {
|
||||||
return (
|
return (
|
||||||
<section className="border-y border-red-light bg-red-light/30 py-10 md:py-14">
|
<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">
|
<div className="mx-auto max-w-7xl px-4">
|
||||||
@@ -31,7 +52,7 @@ export function SpecialSaleCarousel() {
|
|||||||
action="مشاهده همه"
|
action="مشاهده همه"
|
||||||
/>
|
/>
|
||||||
<Carousel ariaLabel="کاروسل فروش ویژه">
|
<Carousel ariaLabel="کاروسل فروش ویژه">
|
||||||
<SpecialSaleBanner />
|
<SpecialSaleBanner banner={banner} />
|
||||||
{specialSaleProducts.map((product) => (
|
{specialSaleProducts.map((product) => (
|
||||||
<ProductCard
|
<ProductCard
|
||||||
key={product.id}
|
key={product.id}
|
||||||
|
|||||||
@@ -1,50 +1,71 @@
|
|||||||
import { Placeholder } from "@/components/ui/Placeholder";
|
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 (
|
return (
|
||||||
<section className="py-10 md:py-14">
|
<section className="py-10 md:py-14">
|
||||||
<div className="mx-auto max-w-7xl px-4">
|
<div className="mx-auto max-w-7xl px-4">
|
||||||
<div className="grid gap-4 md:grid-cols-2">
|
<div className="grid gap-4 md:grid-cols-2">
|
||||||
<a
|
{images.length > 0
|
||||||
href="#"
|
? images.map((image) => (
|
||||||
className="group relative overflow-hidden rounded-2xl border border-border bg-surface transition-shadow hover:shadow-lg"
|
<div
|
||||||
>
|
key={image.id}
|
||||||
<Placeholder
|
className="group relative overflow-hidden rounded-2xl border border-border bg-surface transition-shadow hover:shadow-lg"
|
||||||
src="/images/banner-1.svg"
|
>
|
||||||
alt="تخفیف ویژه یخچال و فریزر"
|
<SlotImage
|
||||||
width="100%"
|
url={image.url}
|
||||||
height="200px"
|
title={image.title}
|
||||||
label="640×200"
|
linkUrl={image.linkUrl}
|
||||||
className="w-full rounded-none border-0"
|
className="block"
|
||||||
/>
|
imgClassName="block h-[200px] w-full object-cover"
|
||||||
<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">
|
</div>
|
||||||
Summer Sale
|
))
|
||||||
</p>
|
: fallbackBanners.map((banner) => (
|
||||||
<h3 className="text-lg font-bold text-white">تخفیف ویژه یخچال و فریزر</h3>
|
<a
|
||||||
<p className="mt-1 text-sm text-white/80">تا ۲۵٪ تخفیف — تا پایان مرداد</p>
|
key={banner.src}
|
||||||
</div>
|
href="#"
|
||||||
</a>
|
className="group relative overflow-hidden rounded-2xl border border-border bg-surface transition-shadow hover:shadow-lg"
|
||||||
<a
|
>
|
||||||
href="#"
|
<Placeholder
|
||||||
className="group relative overflow-hidden rounded-2xl border border-border bg-surface transition-shadow hover:shadow-lg"
|
src={banner.src}
|
||||||
>
|
alt={banner.alt}
|
||||||
<Placeholder
|
width="100%"
|
||||||
src="/images/banner-2.svg"
|
height="200px"
|
||||||
alt="تلویزیونهای هوشمند ۲۰۲۶"
|
label="640×200"
|
||||||
width="100%"
|
className="w-full rounded-none border-0"
|
||||||
height="200px"
|
/>
|
||||||
label="640×200"
|
<div className="absolute inset-0 flex flex-col justify-end bg-gradient-to-t from-red-dark/80 to-transparent p-6">
|
||||||
className="w-full rounded-none border-0"
|
<p className="font-display text-xs tracking-widest text-red-muted uppercase">
|
||||||
/>
|
{banner.kicker}
|
||||||
<div className="absolute inset-0 flex flex-col justify-end bg-gradient-to-t from-red-dark/80 to-transparent p-6">
|
</p>
|
||||||
<p className="font-display text-xs tracking-widest text-red-muted uppercase">
|
<h3 className="text-lg font-bold text-white">{banner.title}</h3>
|
||||||
Smart TV
|
<p className="mt-1 text-sm text-white/80">{banner.subtitle}</p>
|
||||||
</p>
|
</div>
|
||||||
<h3 className="text-lg font-bold text-white">تلویزیونهای هوشمند ۲۰۲۶</h3>
|
</a>
|
||||||
<p className="mt-1 text-sm text-white/80">QLED و OLED با اقساط ۱۲ ماهه</p>
|
))}
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</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