336 lines
8.4 KiB
TypeScript
336 lines
8.4 KiB
TypeScript
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",
|
|
},
|
|
);
|
|
}
|