Wire homepage specials, product/blog pages, instant search, and the customer portal so the live pharmacy site can be managed from the dashboard. Co-authored-by: Cursor <cursoragent@cursor.com>
206 lines
5.3 KiB
TypeScript
206 lines
5.3 KiB
TypeScript
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;
|
||
fallbackSrc?: string;
|
||
};
|
||
|
||
/**
|
||
* Homepage slot catalog published at GET /meshkee/static-image-slots.
|
||
* Ratios match the live homepage layout at max-w-[1280px] (inner 1232px).
|
||
* If a section’s size or aspect ratio changes, update this catalog — the
|
||
* Meshkee dashboard 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: "40:13",
|
||
itemCount: null,
|
||
recommendedWidth: 1280,
|
||
},
|
||
{
|
||
key: "two-banner-below-slider",
|
||
label: "Two banners below slider",
|
||
kind: "list",
|
||
pageKey: "home",
|
||
aspectRatio: "61:11",
|
||
itemCount: 2,
|
||
recommendedWidth: 610,
|
||
},
|
||
{
|
||
key: "categories",
|
||
label: "Featured categories",
|
||
kind: "list",
|
||
pageKey: "home",
|
||
aspectRatio: "1:1",
|
||
itemCount: null,
|
||
recommendedWidth: 168,
|
||
},
|
||
{
|
||
key: "two-banner-row",
|
||
label: "Two banner row",
|
||
kind: "list",
|
||
pageKey: "home",
|
||
aspectRatio: "305:59",
|
||
itemCount: 2,
|
||
recommendedWidth: 610,
|
||
},
|
||
{
|
||
key: "three-banner-row",
|
||
label: "Three banner row",
|
||
kind: "list",
|
||
pageKey: "home",
|
||
aspectRatio: "403:118",
|
||
itemCount: 3,
|
||
recommendedWidth: 403,
|
||
},
|
||
{
|
||
key: "about-us-image",
|
||
label: "About us image",
|
||
kind: "single",
|
||
pageKey: "home",
|
||
aspectRatio: "10:11",
|
||
itemCount: 1,
|
||
recommendedWidth: 560,
|
||
},
|
||
];
|
||
|
||
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, fallbackSrc?: string): SlotMediaItem {
|
||
return {
|
||
id: String(image.id),
|
||
image: image.url,
|
||
href: image.linkUrl?.trim() || undefined,
|
||
title: image.titleFa?.trim() || image.titleEn?.trim() || undefined,
|
||
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?.image)] : 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?.image) : fallback;
|
||
}).filter(Boolean),
|
||
};
|
||
}
|
||
|
||
return {
|
||
aspectRatio,
|
||
items: apiImages.map((image, index) => toMedia(image, fallbacks[index]?.image)),
|
||
};
|
||
}
|
||
|
||
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 [];
|
||
}
|
||
}
|