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(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( 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( "/api/meshkee/categories", signal, ); return Array.isArray(localPayload.items) ? localPayload.items : []; } const payload = await getJson( 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( 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( `/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( 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) { return `/products/${product.id}/${slugifyPersianTitle(product.nameFa || product.title)}`; } export function categoryPath( category: Pick, ) { 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(":", " / "); }