Publish a slot catalog so the dashboard can import keys, and keep local placeholders when a slot is empty. Co-authored-by: Cursor <cursoragent@cursor.com>
172 lines
4.5 KiB
TypeScript
172 lines
4.5 KiB
TypeScript
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<StaticImageSlot[]> {
|
|
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<StaticImageSlot | null> {
|
|
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;
|
|
}
|
|
}
|