Replace the coming-soon page with products, mega menu, home slots, and catalog UI polish. Co-authored-by: Cursor <cursoragent@cursor.com>
182 lines
5.5 KiB
TypeScript
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, "&").replace(/</g, "<").replace(/>/g, ">")}</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));
|
|
}
|