Connect the storefront to Meshkee catalog, blogs, and static image slots.

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>
This commit is contained in:
Alireza Hassani
2026-08-19 23:51:41 +03:30
co-authored by Cursor
parent cd2f9dfc9d
commit 66e604c426
39 changed files with 1900 additions and 188 deletions
+205
View File
@@ -0,0 +1,205 @@
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 [];
}
}