Files
brb-store/lib/static-images.ts
Alireza HassaniandCursor 24b5649abd Polish storefront SEO, specials, and homepage UX.
Publish Meshkee sitemap-config, map product-source specials without variants, restore hero autoplay, and tighten page heroes, product images, and section order.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-22 21:48:00 +03:30

228 lines
5.8 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { meshkeeApiBase, websiteDomain } from "@/lib/config";
export type SlotKind = "single" | "list";
export type SlotImage = {
id: string;
url: string;
titleFa?: string | null;
titleEn?: string | null;
subtext?: string | null;
linkUrl?: string | null;
width?: number | null;
height?: number | null;
sortOrder?: number | null;
};
export type StaticImageSlotCatalogItem = {
key: string;
label: string;
kind: SlotKind;
pageKey: string;
aspectRatio: string;
itemCount: number | null;
recommendedWidth: number;
};
export type StaticImageSlot = StaticImageSlotCatalogItem & {
images: SlotImage[];
};
export type SlotMediaItem = {
id: string;
image: string;
href?: string;
title?: string;
kicker?: string;
text?: string;
fallbackSrc?: string;
};
/**
* Homepage slot catalog published at GET /meshkee/static-image-slots.
* Ratios match the live homepage at max-w-[1280px]. If a section’s CSS size
* or aspect ratio changes, update this catalog in the same change — Meshkee
* reads it as the static-image source of truth.
*/
export const STATIC_IMAGE_SLOTS: StaticImageSlotCatalogItem[] = [
{
key: "slider",
label: "Homepage slider",
kind: "list",
pageKey: "home",
aspectRatio: "2:1",
itemCount: null,
recommendedWidth: 1280,
},
{
key: "three-icon-image",
label: "Three service icons",
kind: "list",
pageKey: "home",
aspectRatio: "1:1",
itemCount: 3,
recommendedWidth: 128,
},
{
key: "categories",
label: "Category carousel",
kind: "list",
pageKey: "home",
aspectRatio: "14:23",
itemCount: null,
recommendedWidth: 280,
},
{
key: "about-us-image",
label: "About us image",
kind: "single",
pageKey: "home",
aspectRatio: "4:3",
itemCount: 1,
recommendedWidth: 720,
},
{
key: "five-banner-section",
label: "Five banner section",
kind: "list",
pageKey: "home",
aspectRatio: "2:1",
itemCount: 5,
recommendedWidth: 606,
},
{
key: "two-banner-row",
label: "Two banner row",
kind: "list",
pageKey: "home",
aspectRatio: "2:1",
itemCount: 2,
recommendedWidth: 606,
},
{
key: "full-row-image",
label: "Full-row contact banner",
kind: "single",
pageKey: "home",
aspectRatio: "64:9",
itemCount: 1,
recommendedWidth: 1920,
},
{
key: "two-branch-image",
label: "Two branch images",
kind: "list",
pageKey: "home",
aspectRatio: "2:1",
itemCount: 2,
recommendedWidth: 606,
},
];
export function catalogSlot(key: string) {
return STATIC_IMAGE_SLOTS.find((slot) => slot.key === key);
}
export function parseAspectRatio(ratio?: string | null) {
if (!ratio) return undefined;
const match = /^(\d+(?:\.\d+)?)\s*:\s*(\d+(?:\.\d+)?)$/.exec(ratio.trim());
if (!match) return undefined;
return `${match[1]} / ${match[2]}`;
}
export function pickSlot(slots: StaticImageSlot[], key: string) {
return slots.find((slot) => slot.key === key);
}
function normalizeImages(images: SlotImage[] | undefined) {
return [...(images ?? [])]
.filter((image) => Boolean(image?.url))
.sort((a, b) => (a.sortOrder ?? 0) - (b.sortOrder ?? 0));
}
export function imagesFromSlot(slot?: StaticImageSlot | null): SlotImage[] {
if (!slot) return [];
const images = normalizeImages(slot.images);
if (slot.kind === "single") return images.slice(0, 1);
return images;
}
function toMedia(image: SlotImage, fallback?: SlotMediaItem): SlotMediaItem {
return {
id: String(image.id),
image: image.url,
href: image.linkUrl?.trim() || fallback?.href,
title: image.titleFa?.trim() || fallback?.title,
kicker: image.titleEn?.trim() || fallback?.kicker,
text: image.subtext?.trim() || fallback?.text,
fallbackSrc: fallback?.image || fallback?.fallbackSrc,
};
}
export function overlaySlotImages(
slot: StaticImageSlot | undefined,
fallbacks: SlotMediaItem[],
key: string,
): { items: SlotMediaItem[]; aspectRatio?: string } {
const catalog = catalogSlot(key);
const aspectRatio = slot?.aspectRatio || catalog?.aspectRatio;
const kind = slot?.kind ?? catalog?.kind ?? "list";
const itemCount = slot?.itemCount ?? catalog?.itemCount ?? null;
const apiImages = imagesFromSlot(slot ? { ...slot, kind, images: slot.images ?? [] } : undefined);
if (kind === "single") {
const image = apiImages[0];
const fallback = fallbacks[0];
return {
aspectRatio,
items: image ? [toMedia(image, fallback)] : fallbacks.slice(0, 1),
};
}
if (apiImages.length === 0) {
return {
aspectRatio,
items: itemCount ? fallbacks.slice(0, itemCount) : fallbacks,
};
}
if (itemCount) {
return {
aspectRatio,
items: Array.from({ length: itemCount }, (_, index) => {
const image = apiImages[index];
const fallback = fallbacks[index] ?? fallbacks[fallbacks.length - 1];
return image ? toMedia(image, fallback) : fallback;
}).filter(Boolean),
};
}
return {
aspectRatio,
items: apiImages.map((image, index) => toMedia(image, fallbacks[index])),
};
}
function readItems(payload: unknown): StaticImageSlot[] {
if (Array.isArray(payload)) return payload as StaticImageSlot[];
if (payload && typeof payload === "object") {
const record = payload as { items?: StaticImageSlot[]; slots?: StaticImageSlot[] };
return record.items ?? record.slots ?? [];
}
return [];
}
export async function fetchStaticImageSlots(pageKey = "home"): Promise<StaticImageSlot[]> {
const url = `${meshkeeApiBase}/tenants/${encodeURIComponent(websiteDomain)}/website/static-images?pageKey=${encodeURIComponent(pageKey)}`;
try {
const response = await fetch(url, {
next: { revalidate: 60 },
});
if (!response.ok) return [];
return readItems(await response.json());
} catch {
return [];
}
}