import sanitizeHtml from "sanitize-html"; export const MESHKEE_API_BASE = "https://api.meshkee.com/api/v1"; export const WEBSITE_DOMAIN = "glassmond.com"; export const CUSTOMER_URL = `https://customer.${WEBSITE_DOMAIN}`; export const BUSINESS_URL = `https://business.${WEBSITE_DOMAIN}`; export const BUSINESS_ID = "22"; export type Tenant = { id: number | string; name: string; nameFa: string; domain: string; logoUrl: string | null; faviconUrl: string | null; specialProductsSource: "product" | "store_item"; enabledModules: string[]; }; export type MediaImage = { mediaId?: string; url: string; }; export type Product = { id: string; title: string; nameFa: string; summary: string; descriptionHtml: string; slug: string; categoryId: string | null; categoryName?: string | null; categoryNameFa?: string | null; thumbnailUrl: string | null; image?: string; thumbnail?: string; images: MediaImage[]; commentCount: number; createdAt: string; updatedAt: string; store?: { variantCount: number; minPrice: number | null; maxPrice: number | null; inStock: boolean; }; }; export type Blog = { id: string; title: string; slug: string; type: "news" | "article" | "blog"; abstract: string; mainTextHtml: string; categoryId: string | null; categoryName?: string | null; author?: { firstName?: string; lastName?: string; } | null; titleImageUrl: string | null; commentCount: number; publishedAt: string; createdAt: string; }; export type Category = { id: string; name: string; nameFa: string; slug: string; parentId: string | null; description: string | null; }; export type PublicComment = { id: string; authorName: string; text: string; createdAt: string; }; type ListResponse = { items: T[]; total: number; page: number; pageSize: number; }; async function apiFetch( path: string, options: RequestInit & { revalidate?: number } = {}, ): Promise { const { revalidate = 300, ...requestOptions } = options; const response = await fetch(`${MESHKEE_API_BASE}${path}`, { ...requestOptions, headers: { Accept: "application/json", ...requestOptions.headers, }, next: { revalidate }, }); if (!response.ok) { throw new Error(`Meshkee API ${response.status}: ${path}`); } return response.json() as Promise; } export function tenantPath(path = "") { return `/tenants/${WEBSITE_DOMAIN}${path}`; } export async function getTenant() { return apiFetch(tenantPath(), { revalidate: 3600 }); } export async function getProducts({ page = 1, pageSize = 12, name, categoryId, }: { page?: number; pageSize?: number; name?: string; categoryId?: string; } = {}) { const query = new URLSearchParams({ page: String(page), pageSize: String(pageSize), }); if (name) query.set("name", name); if (categoryId) query.set("categoryId", categoryId); return apiFetch>(`${tenantPath("/products")}?${query}`); } export async function getProductById(productId: string) { return apiFetch<{ product: Product; relatedProducts: Product[] }>( tenantPath(`/products/by-id/${encodeURIComponent(productId)}`), ); } export async function getProductTechnicalInfo(productId: string) { return apiFetch<{ form?: unknown; values?: unknown }>( tenantPath(`/products/by-id/${encodeURIComponent(productId)}/technical-info`), ).catch(() => null); } export async function getBlogs({ page = 1, pageSize = 12, title, categoryId, }: { page?: number; pageSize?: number; title?: string; categoryId?: string; } = {}) { const query = new URLSearchParams({ page: String(page), pageSize: String(pageSize), }); if (title) query.set("title", title); if (categoryId) query.set("categoryId", categoryId); return apiFetch>(`${tenantPath("/blogs")}?${query}`); } export async function getBlogById(blogId: string) { return apiFetch<{ blog: Blog }>( tenantPath(`/blogs/by-id/${encodeURIComponent(blogId)}`), ); } export async function getCategories(entityType: "product" | "blog") { return apiFetch<{ items: Category[] }>( `${tenantPath("/categories")}?entityType=${entityType}`, { revalidate: 1800 }, ); } export async function getCategoryById( categoryId: string, entityType: "product" | "blog" = "product", ) { return apiFetch<{ category: Category }>( `${tenantPath(`/categories/by-id/${encodeURIComponent(categoryId)}`)}?entityType=${entityType}`, { revalidate: 1800 }, ); } export async function getComments( entityType: "product" | "blog", entityId: string, ) { const query = new URLSearchParams({ entityType, entityId }); return apiFetch<{ items: PublicComment[] }>( `${tenantPath("/comments")}?${query}`, ).catch(() => ({ items: [] })); } export function slugifyFa(value: string) { return value .normalize("NFKC") .trim() .replace(/[\u200c\u200f\u202a-\u202e]/g, " ") .replace(/[^\p{L}\p{N}]+/gu, "-") .replace(/^-+|-+$/g, "") .toLowerCase(); } export function productUrl(product: Pick) { return `/products/${product.id}/${slugifyFa(product.nameFa || product.title)}`; } export function productCategoryUrl( category: Pick, ) { return `/products/category/${category.id}/${slugifyFa(category.nameFa || category.name)}`; } export function blogUrl(blog: Pick) { return `/blog/${blog.id}/${slugifyFa(blog.title)}`; } export function productImage(product: Product) { return product.thumbnailUrl || product.image || product.thumbnail || null; } export function sanitizeCmsContent( content: string, imageAlt = "تصویر مرتبط با محتوای گلسموند", ) { const cleaned = sanitizeHtml(content || "", { allowedTags: [ "p", "br", "strong", "b", "em", "i", "ul", "ol", "li", "h2", "h3", "h4", "blockquote", "a", "img", ], allowedAttributes: { a: ["href", "target", "rel"], img: ["src", "alt", "width", "height", "loading"], }, allowedSchemes: ["http", "https", "mailto", "tel"], transformTags: { a: sanitizeHtml.simpleTransform("a", { rel: "noopener noreferrer", }), img: (tagName, attribs) => ({ tagName, attribs: { src: attribs.src || "", alt: attribs.alt?.trim() || imageAlt, loading: "lazy", ...(attribs.width ? { width: attribs.width } : {}), ...(attribs.height ? { height: attribs.height } : {}), }, }), }, }); return cleaned.includes("<") ? cleaned : cleaned.replace(/\n/g, "
"); } export function plainText(content: string) { return sanitizeHtml(content || "", { allowedTags: [], allowedAttributes: {} }) .replace(/\s+/g, " ") .trim(); } export function formatPersianDate(value: string) { try { return new Intl.DateTimeFormat("fa-IR", { year: "numeric", month: "long", day: "numeric", }).format(new Date(value)); } catch { return ""; } }