Files
ayoubi-gallery/src/lib/catalog.ts
T
Alireza HassaniandCursor 2ee76f4566 Ship Meshkee-backed Ayoubi Gallery storefront.
Replace the coming-soon page with products, mega menu, home slots, and catalog UI polish.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-10 23:44:59 +03:30

182 lines
5.5 KiB
TypeScript

import { meshkeeApiBase, websiteDomain } from "@/lib/config";
import type {
ApiBlog,
ApiCategory,
ApiPortfolio,
ApiProduct,
AuthUser,
EntityComment,
PagedResult,
StoreItemVariants,
TechnicalInfoForm,
TechnicalInfoValue,
} from "@/lib/types";
type Query = Record<string, string | number | boolean | undefined>;
function tenant(path: string) {
return `/tenants/${encodeURIComponent(websiteDomain)}${path}`;
}
async function apiGet<T>(path: string, params?: Query, revalidate = 60): Promise<T> {
const url = new URL(`${meshkeeApiBase}${path}`);
if (params) {
for (const [key, value] of Object.entries(params)) {
if (value !== undefined && value !== null && value !== "") {
url.searchParams.set(key, String(value));
}
}
}
const response = await fetch(url.toString(), { next: { revalidate } });
if (!response.ok) throw new Error(`API ${response.status}: ${path}`);
return (await response.json()) as T;
}
async function apiPost<T>(path: string, body: unknown, token?: string): Promise<T> {
const response = await fetch(`${meshkeeApiBase}${path}`, {
method: "POST",
headers: {
"Content-Type": "application/json",
...(token ? { Authorization: `Bearer ${token}` } : {}),
},
body: JSON.stringify(body),
cache: "no-store",
});
if (!response.ok) throw new Error(`API ${response.status}: ${path}`);
return (await response.json()) as T;
}
async function apiGetAuthed<T>(path: string, token: string): Promise<T> {
const response = await fetch(`${meshkeeApiBase}${path}`, {
headers: { Authorization: `Bearer ${token}` },
cache: "no-store",
});
if (!response.ok) throw new Error(`API ${response.status}: ${path}`);
return (await response.json()) as T;
}
export async function safe<T>(promise: Promise<T>, fallback: T): Promise<T> {
try {
return await promise;
} catch {
return fallback;
}
}
export function getCategories(entityType: "product" | "blog" | "portfolio" = "product") {
return apiGet<{ items: ApiCategory[] }>(tenant("/categories"), { entityType }).then(
(data) => data.items ?? [],
);
}
export function getCategoryById(id: string) {
return apiGet<{ category: ApiCategory }>(tenant(`/categories/by-id/${id}`)).then(
(data) => data.category,
);
}
export function getProducts(
params: {
page?: number;
pageSize?: number;
name?: string;
categoryId?: string;
tag?: string;
} = {},
) {
return apiGet<PagedResult<ApiProduct>>(tenant("/products"), params);
}
export function getProductById(id: string) {
return apiGet<{ product: ApiProduct; relatedProducts: ApiProduct[] }>(
tenant(`/products/by-id/${id}`),
);
}
export function getProductTechnicalInfo(id: string) {
return apiGet<{ form: TechnicalInfoForm | null; values: TechnicalInfoValue[] }>(
tenant(`/products/by-id/${id}/technical-info`),
);
}
export function getStoreItemByProduct(productId: string) {
return apiGet<{ storeItem: StoreItemVariants | null }>(
tenant(`/store-items/by-product/${productId}`),
);
}
export function getBlogs(params: { page?: number; pageSize?: number; title?: string } = {}) {
return apiGet<PagedResult<ApiBlog>>(tenant("/blogs"), params);
}
export function getBlogBySlug(slug: string) {
return apiGet<{ blog: ApiBlog }>(tenant(`/blogs/${encodeURIComponent(slug)}`));
}
export function getPortfolios(params: { page?: number; pageSize?: number } = {}) {
return apiGet<PagedResult<ApiPortfolio>>(tenant("/portfolios"), params);
}
export function getPortfolioBySlug(slug: string) {
return apiGet<{ portfolio: ApiPortfolio }>(tenant(`/portfolios/${encodeURIComponent(slug)}`));
}
export function getComments(entityType: "product" | "blog" | "portfolio", entityId: string) {
return apiGet<{ items: EntityComment[] }>(tenant("/comments"), { entityType, entityId });
}
export function postComment(input: {
entityType: "product" | "blog" | "portfolio";
entityId: string;
authorName: string;
text: string;
authorEmail?: string;
}) {
return apiPost<{ comment: EntityComment }>(tenant("/comments"), input);
}
export function getMe(token: string) {
return apiGetAuthed<{ user: AuthUser }>("/auth/me", token);
}
/** Turn plain-text CMS bodies into safe HTML paragraphs when tags are absent. */
export function asRichHtml(input?: string | null): string | undefined {
if (!input?.trim()) return undefined;
if (/<[a-z][\s\S]*>/i.test(input)) return input;
return input
.split(/\r?\n+/)
.map((line) => line.trim())
.filter(Boolean)
.map((line) => `<p>${line.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;")}</p>`)
.join("");
}
export function productImages(product: ApiProduct): string[] {
const urls = [
product.thumbnailUrl,
product.image,
product.thumbnail,
...(product.images ?? []).map((image) => image.url),
].filter((url): url is string => Boolean(url));
return [...new Set(urls)];
}
export function portfolioImages(portfolio: ApiPortfolio): string[] {
const urls = [
portfolio.thumbnailUrl,
portfolio.image,
...(portfolio.images ?? []).map((image) => image.url),
].filter((url): url is string => Boolean(url));
return [...new Set(urls)];
}
export function jewelryCategories(categories: ApiCategory[]) {
const jewelryParent = categories.find((item) => item.slug === "gold-jewels-and-rhinestones");
if (!jewelryParent) {
return categories.filter((item) => item.parentId && item.isActive !== false);
}
return categories
.filter((item) => item.parentId === jewelryParent.id && item.isActive !== false)
.sort((a, b) => (a.sortOrder ?? 0) - (b.sortOrder ?? 0));
}