Initial commit: Hoomat storefront with Meshkee API and static-image slots.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,335 @@
|
||||
import {
|
||||
MESHKEE_API_BASE,
|
||||
MESHKEE_DOMAIN,
|
||||
} from "@/lib/meshkee/config";
|
||||
import type {
|
||||
AuthUser,
|
||||
BlogPost,
|
||||
Category,
|
||||
Comment,
|
||||
FaviconInfo,
|
||||
Paginated,
|
||||
Product,
|
||||
StaticImageSlot,
|
||||
StoreItemVariant,
|
||||
TechnicalInfo,
|
||||
Tenant,
|
||||
} from "@/lib/meshkee/types";
|
||||
|
||||
export class MeshkeeApiError extends Error {
|
||||
status: number;
|
||||
payload: unknown;
|
||||
|
||||
constructor(message: string, status: number, payload?: unknown) {
|
||||
super(message);
|
||||
this.name = "MeshkeeApiError";
|
||||
this.status = status;
|
||||
this.payload = payload;
|
||||
}
|
||||
}
|
||||
|
||||
type FetchOptions = {
|
||||
method?: string;
|
||||
body?: unknown;
|
||||
token?: string | null;
|
||||
signal?: AbortSignal;
|
||||
cache?: RequestCache;
|
||||
next?: NextFetchRequestConfig;
|
||||
};
|
||||
|
||||
async function request<T>(path: string, options: FetchOptions = {}): Promise<T> {
|
||||
const headers: Record<string, string> = {
|
||||
Accept: "application/json",
|
||||
};
|
||||
|
||||
if (options.body !== undefined) {
|
||||
headers["Content-Type"] = "application/json";
|
||||
}
|
||||
|
||||
if (options.token) {
|
||||
headers.Authorization = `Bearer ${options.token}`;
|
||||
}
|
||||
|
||||
const init: RequestInit = {
|
||||
method: options.method ?? "GET",
|
||||
headers,
|
||||
body: options.body !== undefined ? JSON.stringify(options.body) : undefined,
|
||||
signal: options.signal,
|
||||
};
|
||||
|
||||
// `next` / `cache` are server-only fetch extensions
|
||||
if (typeof window === "undefined") {
|
||||
if (options.cache) init.cache = options.cache;
|
||||
if (options.next) {
|
||||
(init as RequestInit & { next?: NextFetchRequestConfig }).next = options.next;
|
||||
}
|
||||
}
|
||||
|
||||
const response = await fetch(`${MESHKEE_API_BASE}${path}`, init);
|
||||
const payload = await response.json().catch(() => null);
|
||||
|
||||
if (!response.ok) {
|
||||
const record =
|
||||
payload && typeof payload === "object"
|
||||
? (payload as Record<string, unknown>)
|
||||
: null;
|
||||
const errorValue = record?.error;
|
||||
const messageValue = record?.message;
|
||||
const message =
|
||||
(typeof errorValue === "string" && errorValue) ||
|
||||
(typeof messageValue === "string" && messageValue) ||
|
||||
"خطا در ارتباط با سرور";
|
||||
throw new MeshkeeApiError(message, response.status, payload);
|
||||
}
|
||||
|
||||
return payload as T;
|
||||
}
|
||||
|
||||
function tenantPath(suffix = "") {
|
||||
return `/tenants/${encodeURIComponent(MESHKEE_DOMAIN)}${suffix}`;
|
||||
}
|
||||
|
||||
export async function getTenant(): Promise<Tenant> {
|
||||
return request<Tenant>(tenantPath(), { next: { revalidate: 300 } });
|
||||
}
|
||||
|
||||
export async function getFavicon(): Promise<FaviconInfo> {
|
||||
return request<FaviconInfo>(tenantPath("/website/favicon"), {
|
||||
next: { revalidate: 300 },
|
||||
});
|
||||
}
|
||||
|
||||
export async function getStaticImages(
|
||||
pageKey?: string,
|
||||
): Promise<StaticImageSlot[]> {
|
||||
const query = pageKey ? `?pageKey=${encodeURIComponent(pageKey)}` : "";
|
||||
const data = await request<{ items: StaticImageSlot[] }>(
|
||||
`${tenantPath("/website/static-images")}${query}`,
|
||||
{ next: { revalidate: 60 } },
|
||||
);
|
||||
return data.items ?? [];
|
||||
}
|
||||
|
||||
export async function getStaticImageSlot(
|
||||
key: string,
|
||||
): Promise<StaticImageSlot | null> {
|
||||
try {
|
||||
return await request<StaticImageSlot>(
|
||||
tenantPath(`/website/static-images/${encodeURIComponent(key)}`),
|
||||
{ next: { revalidate: 60 } },
|
||||
);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getCategories(
|
||||
entityType: "product" | "blog" = "product",
|
||||
): Promise<Category[]> {
|
||||
const data = await request<{ items: Category[] }>(
|
||||
`${tenantPath("/categories")}?entityType=${entityType}`,
|
||||
{ next: { revalidate: 120 } },
|
||||
);
|
||||
return data.items ?? [];
|
||||
}
|
||||
|
||||
export async function listStoreItems(params: {
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
categoryId?: string;
|
||||
name?: string;
|
||||
productId?: string;
|
||||
} = {}): Promise<Paginated<StoreItemVariant>> {
|
||||
const query = new URLSearchParams();
|
||||
if (params.page) query.set("page", String(params.page));
|
||||
if (params.pageSize) query.set("pageSize", String(params.pageSize));
|
||||
if (params.categoryId) query.set("categoryId", params.categoryId);
|
||||
if (params.name) query.set("name", params.name);
|
||||
if (params.productId) query.set("productId", params.productId);
|
||||
const qs = query.toString();
|
||||
return request<Paginated<StoreItemVariant>>(
|
||||
`${tenantPath("/store-items")}${qs ? `?${qs}` : ""}`,
|
||||
{ next: { revalidate: 60 } },
|
||||
);
|
||||
}
|
||||
|
||||
export async function getStoreItemVariant(
|
||||
variantId: string,
|
||||
): Promise<StoreItemVariant> {
|
||||
const data = await request<{ variant: StoreItemVariant }>(
|
||||
tenantPath(`/store-items/${encodeURIComponent(variantId)}`),
|
||||
{ next: { revalidate: 60 } },
|
||||
);
|
||||
return data.variant;
|
||||
}
|
||||
|
||||
export async function getProductById(productId: string): Promise<{
|
||||
product: Product;
|
||||
relatedProducts: Product[];
|
||||
}> {
|
||||
const data = await request<{
|
||||
product: Product;
|
||||
relatedProducts?: Product[] | null;
|
||||
}>(tenantPath(`/products/by-id/${encodeURIComponent(productId)}`), {
|
||||
next: { revalidate: 60 },
|
||||
});
|
||||
return {
|
||||
product: data.product,
|
||||
relatedProducts: data.relatedProducts ?? [],
|
||||
};
|
||||
}
|
||||
|
||||
export async function getProductTechnicalInfo(
|
||||
productId: string,
|
||||
): Promise<TechnicalInfo> {
|
||||
return request<TechnicalInfo>(
|
||||
tenantPath(`/products/by-id/${encodeURIComponent(productId)}/technical-info`),
|
||||
{ next: { revalidate: 120 } },
|
||||
);
|
||||
}
|
||||
|
||||
export async function listBlogs(params: {
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
} = {}): Promise<Paginated<BlogPost>> {
|
||||
const query = new URLSearchParams();
|
||||
if (params.page) query.set("page", String(params.page));
|
||||
if (params.pageSize) query.set("pageSize", String(params.pageSize));
|
||||
const qs = query.toString();
|
||||
return request<Paginated<BlogPost>>(
|
||||
`${tenantPath("/blogs")}${qs ? `?${qs}` : ""}`,
|
||||
{ next: { revalidate: 60 } },
|
||||
);
|
||||
}
|
||||
|
||||
export async function getBlogById(blogId: string): Promise<BlogPost> {
|
||||
const data = await request<{ blog?: BlogPost } & BlogPost>(
|
||||
tenantPath(`/blogs/by-id/${encodeURIComponent(blogId)}`),
|
||||
{ next: { revalidate: 60 } },
|
||||
);
|
||||
return data.blog ?? data;
|
||||
}
|
||||
|
||||
export async function listComments(params: {
|
||||
entityType: "product" | "blog";
|
||||
entityId: string;
|
||||
}): Promise<Comment[]> {
|
||||
const query = new URLSearchParams({
|
||||
entityType: params.entityType,
|
||||
entityId: params.entityId,
|
||||
});
|
||||
const data = await request<{ items: Comment[] }>(
|
||||
`${tenantPath("/comments")}?${query.toString()}`,
|
||||
{ cache: "no-store" },
|
||||
);
|
||||
return data.items ?? [];
|
||||
}
|
||||
|
||||
export async function submitComment(
|
||||
body: {
|
||||
entityType: "product" | "blog";
|
||||
entityId: string;
|
||||
authorName: string;
|
||||
authorEmail?: string;
|
||||
text: string;
|
||||
},
|
||||
token?: string | null,
|
||||
) {
|
||||
return request(tenantPath("/comments"), {
|
||||
method: "POST",
|
||||
body,
|
||||
token,
|
||||
cache: "no-store",
|
||||
});
|
||||
}
|
||||
|
||||
export async function submitContact(body: {
|
||||
title: string;
|
||||
name: string;
|
||||
email?: string;
|
||||
cellNumber?: string;
|
||||
text: string;
|
||||
}) {
|
||||
return request(tenantPath("/contact-submissions"), {
|
||||
method: "POST",
|
||||
body,
|
||||
cache: "no-store",
|
||||
});
|
||||
}
|
||||
|
||||
export async function getMe(token: string, signal?: AbortSignal): Promise<AuthUser> {
|
||||
const data = await request<{ user?: AuthUser } & AuthUser>("/auth/me", {
|
||||
token,
|
||||
signal,
|
||||
cache: "no-store",
|
||||
});
|
||||
return data.user ?? data;
|
||||
}
|
||||
|
||||
export async function refreshTokens(refreshToken: string) {
|
||||
return request<{ accessToken: string; refreshToken: string }>("/auth/refresh", {
|
||||
method: "POST",
|
||||
body: { refreshToken },
|
||||
cache: "no-store",
|
||||
});
|
||||
}
|
||||
|
||||
export async function createAuthHandoff(token: string) {
|
||||
return request<{ ticket?: string }>("/auth/handoff", {
|
||||
method: "POST",
|
||||
token,
|
||||
cache: "no-store",
|
||||
});
|
||||
}
|
||||
|
||||
export async function getCart(businessId: string, token: string) {
|
||||
return request(`/businesses/${encodeURIComponent(businessId)}/cart`, {
|
||||
token,
|
||||
cache: "no-store",
|
||||
});
|
||||
}
|
||||
|
||||
export async function addCartItem(
|
||||
businessId: string,
|
||||
token: string,
|
||||
body: { storeItemVariantId: string; quantity: number },
|
||||
) {
|
||||
return request(`/businesses/${encodeURIComponent(businessId)}/cart/items`, {
|
||||
method: "POST",
|
||||
token,
|
||||
body,
|
||||
cache: "no-store",
|
||||
});
|
||||
}
|
||||
|
||||
export async function updateCartItem(
|
||||
businessId: string,
|
||||
token: string,
|
||||
itemId: string,
|
||||
quantity: number,
|
||||
) {
|
||||
return request(
|
||||
`/businesses/${encodeURIComponent(businessId)}/cart/items/${encodeURIComponent(itemId)}`,
|
||||
{
|
||||
method: "PATCH",
|
||||
token,
|
||||
body: { quantity },
|
||||
cache: "no-store",
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export async function removeCartItem(
|
||||
businessId: string,
|
||||
token: string,
|
||||
itemId: string,
|
||||
) {
|
||||
return request(
|
||||
`/businesses/${encodeURIComponent(businessId)}/cart/items/${encodeURIComponent(itemId)}`,
|
||||
{
|
||||
method: "DELETE",
|
||||
token,
|
||||
cache: "no-store",
|
||||
},
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user