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>
This commit is contained in:
co-authored by
Cursor
parent
711f75eb93
commit
2ee76f4566
@@ -0,0 +1,181 @@
|
||||
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));
|
||||
}
|
||||
Reference in New Issue
Block a user