setPaused(true)}
onMouseLeave={() => setPaused(false)}
>
- {heroSlides.map((slide, i) => (
-
-
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 (
+
+
+ {hasCopy ? (
+ <>
+
+
+ {title ? (
+
{title}
+ ) : null}
+ {english ? (
+
+ {english}
+
+ ) : null}
+ {subtitle ? (
+
+ {subtitle}
+
+ ) : null}
+
+ >
+ ) : null}
+
+ );
+ })
+ : heroSlides.map((slide, i) => (
+
+
+
+ ))}
+
+ {slides.length === 0 ? (
+ <>
+
+ {heroSlides.map((slide, i) => (
+
+
{slide.title}
+
+ {slide.english}
+
+
+ {slide.subtitle}
+
+
+ {slide.button}
+
+
+ ))}
+ >
+ ) : null}
+
+ {count > 1 ? (
+
+ {Array.from({ length: count }, (_, i) => (
+
- ))}
-
-
-
- {heroSlides.map((slide, i) => (
-
-
{slide.title}
-
- {slide.english}
-
-
{slide.subtitle}
-
- {slide.button}
-
-
- ))}
-
-
- {heroSlides.map((slide, i) => (
-
+ ) : null}
- {heroSides.map((side) => (
-
-
-
-
-
-
-
{side.title}
-
- {side.english}
-
-
{side.subtitle}
-
-
- ))}
+ {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 = (
+
+
+ {hasCopy ? (
+ <>
+
+
+ {title ? (
+
{title}
+ ) : null}
+ {english ? (
+
+ {english}
+
+ ) : null}
+ {subtitle ? (
+
{subtitle}
+ ) : null}
+
+ >
+ ) : null}
+
+ );
+ return image.linkUrl ? (
+
+ {frame}
+
+ ) : (
+
{frame}
+ );
+ })
+ : heroSides.map((side) => (
+
+
+
+
+
+
+
{side.title}
+
+ {side.english}
+
+
{side.subtitle}
+
+
+ ))}
);
diff --git a/components/SlotImage.tsx b/components/SlotImage.tsx
new file mode 100644
index 0000000..cb1738b
--- /dev/null
+++ b/components/SlotImage.tsx
@@ -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
+

+ );
+
+ if (!image.linkUrl) {
+ return className ?
{img}
: img;
+ }
+
+ return (
+
+ {img}
+
+ );
+}
diff --git a/lib/static-images.ts b/lib/static-images.ts
new file mode 100644
index 0000000..80a13a5
--- /dev/null
+++ b/lib/static-images.ts
@@ -0,0 +1,171 @@
+import { API } from "./site";
+
+export type StaticImage = {
+ id: string;
+ url?: string | null;
+ titleFa?: string | null;
+ titleEn?: string | null;
+ subtext?: 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;
+ pageKey?: string;
+ kind: "single" | "list";
+ label?: string;
+ aspectRatio?: string;
+ recommendedWidth?: number;
+ itemCount?: number | null;
+ sortOrder?: number;
+ images?: StaticImage[];
+};
+
+export type StaticImageCatalogSlot = {
+ key: string;
+ label: string;
+ kind: "single" | "list";
+ pageKey: string;
+ aspectRatio: string;
+ itemCount: number | null;
+ recommendedWidth: number;
+};
+
+/**
+ * Slot keys the dashboard Refresh button imports from
+ * GET /meshkee/static-image-slots. Keep aspectRatio + recommendedWidth in
+ * sync with the live CSS when a slot's size changes.
+ */
+export const STATIC_IMAGE_KEYS = {
+ slider: "slider",
+ bannersBelowSlider: "banners-below-slider",
+ categories: "categories",
+ fourBannerSection: "four-banner-section",
+ threeBannerRow: "three-banner-row",
+ aboutUsImage: "about-us-image",
+} as const;
+
+export const STATIC_IMAGE_SLOT_CATALOG: StaticImageCatalogSlot[] = [
+ {
+ key: STATIC_IMAGE_KEYS.slider,
+ label: "Homepage slider",
+ kind: "list",
+ pageKey: "home",
+ aspectRatio: "21:8",
+ itemCount: null,
+ recommendedWidth: 1200,
+ },
+ {
+ key: STATIC_IMAGE_KEYS.bannersBelowSlider,
+ label: "Banners below slider",
+ kind: "list",
+ pageKey: "home",
+ aspectRatio: "13:5",
+ itemCount: 2,
+ recommendedWidth: 600,
+ },
+ {
+ key: STATIC_IMAGE_KEYS.categories,
+ label: "Homepage categories",
+ kind: "list",
+ pageKey: "home",
+ aspectRatio: "1:1",
+ itemCount: null,
+ recommendedWidth: 160,
+ },
+ {
+ key: STATIC_IMAGE_KEYS.fourBannerSection,
+ label: "Four banner section",
+ kind: "list",
+ pageKey: "home",
+ aspectRatio: "21:9",
+ itemCount: 4,
+ recommendedWidth: 600,
+ },
+ {
+ key: STATIC_IMAGE_KEYS.threeBannerRow,
+ label: "Three banner row",
+ kind: "list",
+ pageKey: "home",
+ aspectRatio: "12:5",
+ itemCount: 3,
+ recommendedWidth: 480,
+ },
+ {
+ key: STATIC_IMAGE_KEYS.aboutUsImage,
+ label: "About us image",
+ kind: "single",
+ pageKey: "home",
+ aspectRatio: "8:5",
+ itemCount: 1,
+ recommendedWidth: 720,
+ },
+];
+
+export function slotAspectStyle(
+ ratio?: string | null,
+ fallback?: string,
+): { aspectRatio: string } | undefined {
+ const value = ratio?.trim() || fallback;
+ if (!value) return undefined;
+ return { aspectRatio: value.replace(":", " / ") };
+}
+
+export function slotAlt(image: StaticImage, fallback = ""): string {
+ return image.titleFa?.trim() || image.titleEn?.trim() || fallback;
+}
+
+export function imagesForSlot(slots: StaticImageSlot[], key: string): StaticImageWithUrl[] {
+ const slot = slots.find((item) => item.key === key);
+ const raw = slot?.images ?? [];
+ const sorted = [...raw].sort((a, b) => (a.sortOrder ?? 0) - (b.sortOrder ?? 0));
+ const result: StaticImageWithUrl[] = [];
+ for (const image of sorted) {
+ const url = typeof image.url === "string" ? image.url.trim() : "";
+ if (!url) continue;
+ result.push({ ...image, url });
+ }
+ if (slot?.kind === "single") return result.slice(0, 1);
+ return result;
+}
+
+export function firstImageForSlot(
+ slots: StaticImageSlot[],
+ key: string,
+): StaticImageWithUrl | undefined {
+ return imagesForSlot(slots, key)[0];
+}
+
+export function slotByKey(slots: StaticImageSlot[], key: string): StaticImageSlot | undefined {
+ return slots.find((slot) => slot.key === key);
+}
+
+export async function getStaticImageSlots(pageKey?: string): Promise
{
+ try {
+ const url = new URL(`${API.base}/tenants/${API.domain}/website/static-images`);
+ if (pageKey) url.searchParams.set("pageKey", pageKey);
+ const response = await fetch(url, { next: { revalidate: 120 } });
+ if (!response.ok) return [];
+ const data = (await response.json()) as { items?: StaticImageSlot[] };
+ return data.items ?? [];
+ } catch {
+ return [];
+ }
+}
+
+export async function getStaticImageSlot(key: string): Promise {
+ try {
+ const url = new URL(`${API.base}/tenants/${API.domain}/website/static-images/${key}`);
+ const response = await fetch(url, { next: { revalidate: 120 } });
+ if (!response.ok) return null;
+ return (await response.json()) as StaticImageSlot;
+ } catch {
+ return null;
+ }
+}