Files
saman-andish/app/lib/meshkee.ts
T

283 lines
7.0 KiB
TypeScript

export type StaticImage = {
id: number | 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 StaticImageSlot = {
key: string;
pageKey: string;
kind: "single" | "list";
label: string;
aspectRatio?: string | null;
recommendedWidth?: number | null;
itemCount?: number | null;
images: StaticImage[];
};
export type ProductCategory = {
id: string;
parentId?: string | null;
name: string;
nameFa?: string | null;
slug: string;
description?: string | null;
sortOrder?: number | null;
isActive?: boolean;
};
export type ProductMedia = {
mediaId?: string;
url: string;
};
export type Product = {
id: string;
title: string;
nameFa?: string | null;
summary?: string | null;
descriptionHtml?: string | null;
categoryId?: string | null;
categoryName?: string | null;
categoryNameFa?: string | null;
brand?: { name?: string | null; nameFa?: string | null } | string | null;
thumbnailUrl?: string | null;
image?: string | null;
thumbnail?: string | null;
images?: ProductMedia[];
commentCount?: number | null;
createdAt?: string | null;
updatedAt?: string | null;
store?: {
inStock?: boolean;
minPrice?: number | string | null;
maxPrice?: number | string | null;
} | null;
};
export type ProductDetailResponse = {
product: Product;
relatedProducts?: Product[];
};
type StaticImageSlotsResponse = {
items?: StaticImageSlot[];
};
type ProductCategoriesResponse = {
items?: ProductCategory[];
};
type ProductListResponse = {
items?: Product[];
total?: number;
page?: number;
pageSize?: number;
};
const API_BASE_URL =
process.env.NEXT_PUBLIC_MESHKEE_API_URL?.replace(/\/$/, "") ??
"https://api.meshkee.com/api/v1";
const WEBSITE_DOMAIN =
process.env.NEXT_PUBLIC_MESHKEE_DOMAIN ?? "samanandish.com";
function tenantEndpoint(path: string) {
return `${API_BASE_URL}/tenants/${encodeURIComponent(WEBSITE_DOMAIN)}${path}`;
}
async function getJson<T>(endpoint: string, signal?: AbortSignal) {
const response = await fetch(endpoint, {
headers: { Accept: "application/json" },
signal,
});
if (!response.ok) {
throw new Error(`Meshkee request failed with ${response.status}`);
}
return (await response.json()) as T;
}
export async function fetchHomeStaticImageSlots(signal?: AbortSignal) {
const payload = await getJson<StaticImageSlotsResponse>(
tenantEndpoint("/website/static-images?pageKey=home"),
signal,
);
return Array.isArray(payload.items) ? payload.items : [];
}
export async function fetchProductCategories(signal?: AbortSignal) {
if (typeof window !== "undefined") {
const localPayload = await getJson<ProductCategoriesResponse>(
"/api/meshkee/categories",
signal,
);
return Array.isArray(localPayload.items) ? localPayload.items : [];
}
const payload = await getJson<ProductCategoriesResponse>(
tenantEndpoint("/categories?entityType=product"),
signal,
);
return Array.isArray(payload.items) ? payload.items : [];
}
export function getLevelTwoProductCategories(categories: ProductCategory[]) {
const rootIds = new Set(
categories
.filter((category) => !category.parentId)
.map((category) => category.id),
);
return categories
.filter(
(category) =>
Boolean(category.parentId) &&
rootIds.has(category.parentId as string) &&
category.isActive !== false,
)
.sort(
(first, second) =>
(first.sortOrder ?? 0) - (second.sortOrder ?? 0) ||
(first.nameFa || first.name).localeCompare(second.nameFa || second.name),
);
}
async function fetchProductPage(
page: number,
pageSize: number,
signal?: AbortSignal,
categoryId?: string,
) {
const query = new URLSearchParams({
page: String(page),
pageSize: String(pageSize),
});
if (categoryId) query.set("categoryId", categoryId);
const payload = await getJson<ProductListResponse>(
tenantEndpoint(`/products?${query.toString()}`),
signal,
);
return {
items: Array.isArray(payload.items) ? payload.items : [],
total: Number(payload.total) || 0,
};
}
export async function fetchLatestProducts(
count = 4,
signal?: AbortSignal,
categoryId?: string,
) {
if (typeof window !== "undefined" && !categoryId) {
const localPayload = await getJson<ProductListResponse>(
`/api/meshkee/latest-products?count=${encodeURIComponent(count)}`,
signal,
);
return Array.isArray(localPayload.items) ? localPayload.items : [];
}
const firstPage = await fetchProductPage(1, count, signal, categoryId);
if (firstPage.total <= count) {
return [...firstPage.items]
.sort(
(first, second) =>
Date.parse(second.createdAt || "") - Date.parse(first.createdAt || ""),
)
.slice(0, count);
}
const firstDate = Date.parse(firstPage.items[0]?.createdAt || "");
const lastDate = Date.parse(
firstPage.items[firstPage.items.length - 1]?.createdAt || "",
);
if (Number.isFinite(firstDate) && firstDate >= lastDate) {
return firstPage.items.slice(0, count);
}
const lastPageNumber = Math.ceil(firstPage.total / count);
const lastPage = await fetchProductPage(
lastPageNumber,
count,
signal,
categoryId,
);
let latestItems = lastPage.items;
if (latestItems.length < count && lastPageNumber > 1) {
const previousPage = await fetchProductPage(
lastPageNumber - 1,
count,
signal,
categoryId,
);
latestItems = [...previousPage.items, ...latestItems];
}
return latestItems
.sort(
(first, second) =>
Date.parse(second.createdAt || "") - Date.parse(first.createdAt || ""),
)
.slice(0, count);
}
export async function fetchProductById(
productId: string,
signal?: AbortSignal,
) {
return getJson<ProductDetailResponse>(
tenantEndpoint(`/products/by-id/${encodeURIComponent(productId)}`),
signal,
);
}
export async function fetchCategoryById(
categoryId: string,
signal?: AbortSignal,
) {
const payload = await getJson<{ category?: ProductCategory }>(
tenantEndpoint(
`/categories/by-id/${encodeURIComponent(categoryId)}?entityType=product`,
),
signal,
);
return payload.category;
}
export function slugifyPersianTitle(value: string) {
return value
.normalize("NFKC")
.replace(/[^\p{L}\p{N}\s-]/gu, "")
.trim()
.replace(/\s+/g, "-")
.replace(/-+/g, "-");
}
export function productPath(product: Pick<Product, "id" | "nameFa" | "title">) {
return `/products/${product.id}/${slugifyPersianTitle(product.nameFa || product.title)}`;
}
export function categoryPath(
category: Pick<ProductCategory, "id" | "nameFa" | "name">,
) {
return `/products/category/${category.id}/${slugifyPersianTitle(category.nameFa || category.name)}`;
}
export function cssAspectRatio(value?: string | null) {
if (!value || !/^\d+(?:\.\d+)?:\d+(?:\.\d+)?$/.test(value)) {
return undefined;
}
return value.replace(":", " / ");
}