Files
glassmond/lib/meshkee.ts
T
Alireza HassaniandCursor 98fde02322 Create Glassmond storefront with Meshkee integration
Build the RTL Next.js experience with live catalog, journal, customer interactions, and SEO-ready canonical routes.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-26 00:08:00 +03:30

298 lines
7.0 KiB
TypeScript

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<T> = {
items: T[];
total: number;
page: number;
pageSize: number;
};
async function apiFetch<T>(
path: string,
options: RequestInit & { revalidate?: number } = {},
): Promise<T> {
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<T>;
}
export function tenantPath(path = "") {
return `/tenants/${WEBSITE_DOMAIN}${path}`;
}
export async function getTenant() {
return apiFetch<Tenant>(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<ListResponse<Product>>(`${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<ListResponse<Blog>>(`${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<Product, "id" | "nameFa" | "title">) {
return `/products/${product.id}/${slugifyFa(product.nameFa || product.title)}`;
}
export function productCategoryUrl(
category: Pick<Category, "id" | "nameFa" | "name">,
) {
return `/products/category/${category.id}/${slugifyFa(category.nameFa || category.name)}`;
}
export function blogUrl(blog: Pick<Blog, "id" | "title">) {
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, "<br />");
}
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 "";
}
}