Update carousels, card hover, and IranBay branding.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,285 @@
|
||||
import { apiRequest, getAccessToken } from "@/lib/meshkee-api";
|
||||
|
||||
export interface MeshkeeProduct {
|
||||
id: string;
|
||||
title: string;
|
||||
nameFa?: string;
|
||||
summary?: string;
|
||||
slug: string;
|
||||
status?: string;
|
||||
categoryId?: string | null;
|
||||
categoryName?: string;
|
||||
categoryParentName?: string;
|
||||
thumbnailUrl?: string | null;
|
||||
image?: string;
|
||||
thumbnail?: string;
|
||||
}
|
||||
|
||||
export interface MeshkeeBlog {
|
||||
id: string;
|
||||
title: string;
|
||||
slug: string;
|
||||
abstract?: string | null;
|
||||
titleImageUrl?: string | null;
|
||||
createdAt?: string;
|
||||
}
|
||||
|
||||
export interface MeshkeePortfolio {
|
||||
id: string;
|
||||
title?: string;
|
||||
titleFa?: string | null;
|
||||
titleEn?: string | null;
|
||||
name?: string;
|
||||
nameFa?: string | null;
|
||||
nameEn?: string | null;
|
||||
slug: string;
|
||||
abstract?: string | null;
|
||||
titleImageUrl?: string | null;
|
||||
coverUrl?: string | null;
|
||||
image?: string | null;
|
||||
}
|
||||
|
||||
export interface MeshkeeMediaItem {
|
||||
id: string;
|
||||
mediaType: string;
|
||||
publicUrl: string;
|
||||
altText?: string | null;
|
||||
fileName?: string | null;
|
||||
width?: number | null;
|
||||
height?: number | null;
|
||||
}
|
||||
|
||||
export interface MeshkeeBusinessCategory {
|
||||
id: string;
|
||||
name: string;
|
||||
nameFa?: string | null;
|
||||
parentId: string | null;
|
||||
slug?: string;
|
||||
isActive?: boolean;
|
||||
}
|
||||
|
||||
export type ListResponse<T> = {
|
||||
items: T[];
|
||||
total: number;
|
||||
page: number;
|
||||
pageSize: number;
|
||||
};
|
||||
|
||||
type ListOptions = {
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
};
|
||||
|
||||
async function listResource<T>(
|
||||
path: string,
|
||||
options: ListOptions = {},
|
||||
): Promise<ListResponse<T>> {
|
||||
const page = options.page ?? 1;
|
||||
const pageSize = options.pageSize ?? 12;
|
||||
const params = new URLSearchParams({
|
||||
page: String(page),
|
||||
pageSize: String(pageSize),
|
||||
});
|
||||
const data = await apiRequest<ListResponse<T>>(`${path}?${params}`, {
|
||||
auth: true,
|
||||
});
|
||||
return {
|
||||
items: (data.items ?? []).map((item) => ({
|
||||
...item,
|
||||
id: String((item as { id: string | number }).id),
|
||||
})),
|
||||
total: data.total ?? 0,
|
||||
page: data.page ?? page,
|
||||
pageSize: data.pageSize ?? pageSize,
|
||||
};
|
||||
}
|
||||
|
||||
export function listMeshkeeProducts(
|
||||
businessId: string,
|
||||
options: ListOptions = {},
|
||||
) {
|
||||
return listResource<MeshkeeProduct>(
|
||||
`/businesses/${businessId}/products`,
|
||||
options,
|
||||
);
|
||||
}
|
||||
|
||||
export function listMeshkeeBlogs(
|
||||
businessId: string,
|
||||
options: ListOptions = {},
|
||||
) {
|
||||
return listResource<MeshkeeBlog>(`/businesses/${businessId}/blogs`, options);
|
||||
}
|
||||
|
||||
export function listMeshkeePortfolios(
|
||||
businessId: string,
|
||||
options: ListOptions = {},
|
||||
) {
|
||||
return listResource<MeshkeePortfolio>(
|
||||
`/businesses/${businessId}/portfolios`,
|
||||
options,
|
||||
);
|
||||
}
|
||||
|
||||
export async function listMeshkeeGallery(
|
||||
businessId: string,
|
||||
options: ListOptions = {},
|
||||
) {
|
||||
const page = options.page ?? 1;
|
||||
const pageSize = options.pageSize ?? 16;
|
||||
const params = new URLSearchParams({
|
||||
page: String(page),
|
||||
pageSize: String(pageSize),
|
||||
});
|
||||
const data = await apiRequest<ListResponse<MeshkeeMediaItem>>(
|
||||
`/businesses/${businessId}/gallery?${params}`,
|
||||
{ auth: true },
|
||||
);
|
||||
|
||||
return {
|
||||
items: (data.items ?? []).map((item) => ({
|
||||
...item,
|
||||
id: String(item.id),
|
||||
})),
|
||||
total: data.total ?? 0,
|
||||
page: data.page ?? page,
|
||||
pageSize: data.pageSize ?? pageSize,
|
||||
};
|
||||
}
|
||||
|
||||
export async function addMeshkeeGalleryItems(
|
||||
businessId: string,
|
||||
input: { files?: File[]; mediaIds?: string[] },
|
||||
) {
|
||||
const token = getAccessToken();
|
||||
const base =
|
||||
process.env.NEXT_PUBLIC_MESHKEE_API_URL?.replace(/\/$/, "") ||
|
||||
"http://localhost:3010/api/v1";
|
||||
|
||||
if (input.files?.length) {
|
||||
const form = new FormData();
|
||||
for (const file of input.files) {
|
||||
form.append("files", file);
|
||||
}
|
||||
if (input.mediaIds?.length) {
|
||||
form.append("mediaIds", JSON.stringify(input.mediaIds));
|
||||
}
|
||||
|
||||
const response = await fetch(`${base}/businesses/${businessId}/gallery`, {
|
||||
method: "POST",
|
||||
headers: token ? { Authorization: `Bearer ${token}` } : undefined,
|
||||
body: form,
|
||||
});
|
||||
const payload = await response.json().catch(() => null);
|
||||
if (!response.ok) {
|
||||
const message =
|
||||
typeof payload?.message === "string"
|
||||
? payload.message
|
||||
: Array.isArray(payload?.message)
|
||||
? payload.message.join(", ")
|
||||
: `Upload failed (${response.status})`;
|
||||
throw new Error(message);
|
||||
}
|
||||
return {
|
||||
items: ((payload?.items ?? []) as MeshkeeMediaItem[]).map((item) => ({
|
||||
...item,
|
||||
id: String(item.id),
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
const data = await apiRequest<{ items: MeshkeeMediaItem[] }>(
|
||||
`/businesses/${businessId}/gallery`,
|
||||
{
|
||||
method: "POST",
|
||||
auth: true,
|
||||
body: { mediaIds: input.mediaIds ?? [] },
|
||||
},
|
||||
);
|
||||
return {
|
||||
items: (data.items ?? []).map((item) => ({
|
||||
...item,
|
||||
id: String(item.id),
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
export async function deleteMeshkeeGalleryItem(
|
||||
businessId: string,
|
||||
mediaId: string,
|
||||
) {
|
||||
await apiRequest(`/businesses/${businessId}/gallery/${mediaId}`, {
|
||||
method: "DELETE",
|
||||
auth: true,
|
||||
});
|
||||
}
|
||||
|
||||
export async function listMeshkeeBusinessCategories(businessId: string) {
|
||||
const data = await apiRequest<{ items: MeshkeeBusinessCategory[] }>(
|
||||
`/businesses/${businessId}/categories`,
|
||||
{ auth: true },
|
||||
);
|
||||
|
||||
return (data.items ?? []).map((item) => ({
|
||||
...item,
|
||||
id: String(item.id),
|
||||
parentId: item.parentId == null ? null : String(item.parentId),
|
||||
}));
|
||||
}
|
||||
|
||||
export async function deleteMeshkeeProduct(
|
||||
businessId: string,
|
||||
productId: string,
|
||||
) {
|
||||
await apiRequest(`/businesses/${businessId}/products/${productId}`, {
|
||||
method: "DELETE",
|
||||
auth: true,
|
||||
});
|
||||
}
|
||||
|
||||
export async function deleteMeshkeeBlog(businessId: string, blogId: string) {
|
||||
await apiRequest(`/businesses/${businessId}/blogs/${blogId}`, {
|
||||
method: "DELETE",
|
||||
auth: true,
|
||||
});
|
||||
}
|
||||
|
||||
export async function deleteMeshkeeMedia(businessId: string, mediaId: string) {
|
||||
await apiRequest(`/businesses/${businessId}/media/${mediaId}`, {
|
||||
method: "DELETE",
|
||||
auth: true,
|
||||
});
|
||||
}
|
||||
|
||||
export async function deleteMeshkeePortfolio(
|
||||
businessId: string,
|
||||
portfolioId: string,
|
||||
) {
|
||||
await apiRequest(`/businesses/${businessId}/portfolios/${portfolioId}`, {
|
||||
method: "DELETE",
|
||||
auth: true,
|
||||
});
|
||||
}
|
||||
|
||||
/** Collect category id plus all descendant ids. */
|
||||
export function collectCategorySubtreeIds(
|
||||
categories: MeshkeeBusinessCategory[],
|
||||
rootId: string,
|
||||
): Set<string> {
|
||||
const ids = new Set<string>([rootId]);
|
||||
let grew = true;
|
||||
while (grew) {
|
||||
grew = false;
|
||||
for (const category of categories) {
|
||||
if (
|
||||
category.parentId &&
|
||||
ids.has(category.parentId) &&
|
||||
!ids.has(category.id)
|
||||
) {
|
||||
ids.add(category.id);
|
||||
grew = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return ids;
|
||||
}
|
||||
Reference in New Issue
Block a user