Update carousels, card hover, and IranBay branding.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,312 @@
|
||||
import { apiRequest, getAccessToken } from "@/lib/meshkee-api";
|
||||
|
||||
export interface MeshkeeBusinessSearchItem {
|
||||
id: string;
|
||||
name: string;
|
||||
nameFa: string | null;
|
||||
slug: string;
|
||||
label: string;
|
||||
ownerName?: string | null;
|
||||
ownerCellNumber?: string | null;
|
||||
}
|
||||
|
||||
export interface MeshkeeBusinessCategory {
|
||||
id: string;
|
||||
name: string;
|
||||
slug: string;
|
||||
parentId: string | null;
|
||||
}
|
||||
|
||||
export interface MeshkeeBusinessOwner {
|
||||
id: string;
|
||||
cellNumber: string;
|
||||
firstName: string | null;
|
||||
lastName: string | null;
|
||||
firstNameEn: string | null;
|
||||
lastNameEn: string | null;
|
||||
email: string | null;
|
||||
}
|
||||
|
||||
export interface MeshkeeBusinessDetail {
|
||||
id: string;
|
||||
name: string;
|
||||
nameFa: string | null;
|
||||
slug: string;
|
||||
categories: MeshkeeBusinessCategory[];
|
||||
categoryIds: string[];
|
||||
owner: MeshkeeBusinessOwner | null;
|
||||
logoUrl: string | null;
|
||||
logoMediaId: string | null;
|
||||
bannerUrl: string | null;
|
||||
bannerMediaId: string | null;
|
||||
createdAt?: string;
|
||||
}
|
||||
|
||||
function mapSearchItem(
|
||||
item: MeshkeeBusinessSearchItem,
|
||||
): MeshkeeBusinessSearchItem {
|
||||
const name = item.name ?? "";
|
||||
const nameFa = item.nameFa ?? null;
|
||||
return {
|
||||
...item,
|
||||
id: String(item.id),
|
||||
name,
|
||||
nameFa,
|
||||
slug: item.slug ?? "",
|
||||
label: item.label || name || item.slug || String(item.id),
|
||||
ownerName: item.ownerName ?? null,
|
||||
ownerCellNumber: item.ownerCellNumber ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
export async function searchMeshkeeBusinesses(
|
||||
q: string,
|
||||
signal?: AbortSignal,
|
||||
) {
|
||||
const params = new URLSearchParams({
|
||||
q,
|
||||
limit: "20",
|
||||
});
|
||||
|
||||
const data = await apiRequest<{ items: MeshkeeBusinessSearchItem[] }>(
|
||||
`/businesses/search?${params}`,
|
||||
{ auth: true, signal },
|
||||
);
|
||||
|
||||
return {
|
||||
items: (data.items ?? []).map(mapSearchItem),
|
||||
};
|
||||
}
|
||||
|
||||
/** Newest Meshkee businesses (super-admin list, created_at desc). */
|
||||
export async function listLatestMeshkeeBusinesses(
|
||||
limit = 5,
|
||||
signal?: AbortSignal,
|
||||
) {
|
||||
const pageSize = Math.min(Math.max(limit, 5), 50);
|
||||
const params = new URLSearchParams({
|
||||
page: "1",
|
||||
pageSize: String(pageSize),
|
||||
sort: "created",
|
||||
});
|
||||
|
||||
const data = await apiRequest<{ items: MeshkeeBusinessSearchItem[] }>(
|
||||
`/businesses?${params}`,
|
||||
{ auth: true, signal },
|
||||
);
|
||||
|
||||
return {
|
||||
items: (data.items ?? []).slice(0, limit).map(mapSearchItem),
|
||||
};
|
||||
}
|
||||
|
||||
export async function getMeshkeeBusiness(
|
||||
businessId: string,
|
||||
signal?: AbortSignal,
|
||||
) {
|
||||
const data = await apiRequest<MeshkeeBusinessDetail>(
|
||||
`/businesses/${businessId}`,
|
||||
{ auth: true, signal },
|
||||
);
|
||||
|
||||
return {
|
||||
...data,
|
||||
id: String(data.id),
|
||||
createdAt: data.createdAt,
|
||||
categories: (data.categories ?? []).map((category) => ({
|
||||
...category,
|
||||
id: String(category.id),
|
||||
parentId:
|
||||
category.parentId == null ? null : String(category.parentId),
|
||||
})),
|
||||
categoryIds: (data.categoryIds ?? []).map(String),
|
||||
owner: data.owner
|
||||
? {
|
||||
...data.owner,
|
||||
id: String(data.owner.id),
|
||||
}
|
||||
: null,
|
||||
};
|
||||
}
|
||||
|
||||
export async function createMeshkeeBusiness(body: {
|
||||
name: string;
|
||||
nameFa: string;
|
||||
slug?: string;
|
||||
categoryIds: number[];
|
||||
ownerFirstName: string;
|
||||
ownerLastName: string;
|
||||
ownerFirstNameEn: string;
|
||||
ownerLastNameEn: string;
|
||||
ownerCellNumber: string;
|
||||
ownerPassword: string;
|
||||
}) {
|
||||
const data = await apiRequest<MeshkeeBusinessDetail>(`/businesses`, {
|
||||
method: "POST",
|
||||
auth: true,
|
||||
body,
|
||||
});
|
||||
return { ...data, id: String(data.id) };
|
||||
}
|
||||
|
||||
export async function uploadBusinessMedia(
|
||||
businessId: string,
|
||||
file: File,
|
||||
): Promise<{ id: string; publicUrl: string }> {
|
||||
const token = getAccessToken();
|
||||
const form = new FormData();
|
||||
form.append("files", file);
|
||||
|
||||
const base =
|
||||
process.env.NEXT_PUBLIC_MESHKEE_API_URL?.replace(/\/$/, "") ||
|
||||
"http://localhost:3010/api/v1";
|
||||
|
||||
const response = await fetch(`${base}/businesses/${businessId}/media`, {
|
||||
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);
|
||||
}
|
||||
|
||||
const item = Array.isArray(payload?.items)
|
||||
? payload.items[0]
|
||||
: Array.isArray(payload)
|
||||
? payload[0]
|
||||
: payload;
|
||||
|
||||
return {
|
||||
id: String(item.id),
|
||||
publicUrl: String(item.publicUrl ?? item.url ?? ""),
|
||||
};
|
||||
}
|
||||
|
||||
export async function updateBusinessBanner(
|
||||
businessId: string,
|
||||
bannerMediaId: string | null,
|
||||
) {
|
||||
return updateBusinessProfile(businessId, {
|
||||
bannerMediaId: bannerMediaId == null ? null : Number(bannerMediaId),
|
||||
});
|
||||
}
|
||||
|
||||
export type BusinessPhoneType = "landline" | "cell";
|
||||
|
||||
export interface BusinessPhoneNumber {
|
||||
type: BusinessPhoneType;
|
||||
number: string;
|
||||
/** Optional; supported after Meshkee deploy with label field */
|
||||
label?: string;
|
||||
}
|
||||
|
||||
export interface BusinessAddress {
|
||||
id?: string;
|
||||
/** Display name / label for the address */
|
||||
name: string;
|
||||
province: string;
|
||||
city: string;
|
||||
address: string;
|
||||
postalCode: string;
|
||||
landline?: string | null;
|
||||
}
|
||||
|
||||
export interface BusinessProfile {
|
||||
businessId: string;
|
||||
profile: {
|
||||
nameEn: string;
|
||||
nameFa: string;
|
||||
title: string;
|
||||
about: string;
|
||||
vision: string;
|
||||
emails: string[];
|
||||
phoneNumbers: BusinessPhoneNumber[];
|
||||
socialMedia: Record<string, string>;
|
||||
logoMediaId: string | null;
|
||||
logoUrl: string | null;
|
||||
bannerMediaId: string | null;
|
||||
bannerUrl: string | null;
|
||||
categoryIds: string[];
|
||||
};
|
||||
addresses: BusinessAddress[];
|
||||
}
|
||||
|
||||
export type UpdateBusinessProfileInput = {
|
||||
nameEn?: string;
|
||||
nameFa?: string;
|
||||
title?: string;
|
||||
about?: string;
|
||||
vision?: string;
|
||||
phoneNumbers?: BusinessPhoneNumber[];
|
||||
logoMediaId?: number | null;
|
||||
bannerMediaId?: number | null;
|
||||
categoryIds?: number[];
|
||||
addresses?: BusinessAddress[];
|
||||
};
|
||||
|
||||
export async function getBusinessProfile(businessId: string) {
|
||||
const data = await apiRequest<BusinessProfile>(
|
||||
`/businesses/${businessId}/profile`,
|
||||
{ auth: true },
|
||||
);
|
||||
|
||||
return {
|
||||
businessId: String(data.businessId),
|
||||
profile: {
|
||||
...data.profile,
|
||||
logoMediaId:
|
||||
data.profile.logoMediaId == null
|
||||
? null
|
||||
: String(data.profile.logoMediaId),
|
||||
bannerMediaId:
|
||||
data.profile.bannerMediaId == null
|
||||
? null
|
||||
: String(data.profile.bannerMediaId),
|
||||
categoryIds: (data.profile.categoryIds ?? []).map(String),
|
||||
phoneNumbers: (data.profile.phoneNumbers ?? []).map((phone) => ({
|
||||
type: phone.type === "landline" ? "landline" : "cell",
|
||||
number: phone.number ?? "",
|
||||
...(phone.label?.trim() ? { label: phone.label.trim() } : {}),
|
||||
})),
|
||||
emails: data.profile.emails ?? [],
|
||||
about: data.profile.about ?? "",
|
||||
vision: data.profile.vision ?? "",
|
||||
nameEn: data.profile.nameEn ?? "",
|
||||
nameFa: data.profile.nameFa ?? "",
|
||||
title: data.profile.title ?? "",
|
||||
logoUrl: data.profile.logoUrl ?? null,
|
||||
bannerUrl: data.profile.bannerUrl ?? null,
|
||||
socialMedia: data.profile.socialMedia ?? {},
|
||||
},
|
||||
addresses: (data.addresses ?? []).map((address) => ({
|
||||
...address,
|
||||
id: address.id == null ? undefined : String(address.id),
|
||||
name: String(
|
||||
(address as BusinessAddress & { label?: string | null }).name ??
|
||||
(address as { label?: string | null }).label ??
|
||||
"",
|
||||
),
|
||||
landline: address.landline ?? "",
|
||||
postalCode: address.postalCode ?? "",
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
export async function updateBusinessProfile(
|
||||
businessId: string,
|
||||
body: UpdateBusinessProfileInput,
|
||||
) {
|
||||
return apiRequest<BusinessProfile>(`/businesses/${businessId}/profile`, {
|
||||
method: "PATCH",
|
||||
auth: true,
|
||||
body,
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user