Initial commit: Meshkee dashboards monorepo.
Includes business, customer, and super-admin apps with shared packages and production deploy scripts. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,207 @@
|
||||
import { apiRequest } from '../lib/api'
|
||||
import { getActiveBusinessId } from '../lib/businessContext'
|
||||
import type { Product } from '../types/product'
|
||||
|
||||
export const PRODUCTS_PER_PAGE = 12
|
||||
|
||||
export interface ProductApi {
|
||||
id: string
|
||||
businessId: string
|
||||
title: string
|
||||
nameFa: string
|
||||
summary: string
|
||||
descriptionHtml: string
|
||||
slug: string
|
||||
status: 'draft' | 'published' | 'archived'
|
||||
categoryId: string | null
|
||||
categoryName: string
|
||||
brandId: string | null
|
||||
brand: {
|
||||
id: string
|
||||
nameEn: string
|
||||
nameFa: string | null
|
||||
slug: string
|
||||
imageUrl: string | null
|
||||
} | null
|
||||
tags: string[]
|
||||
thumbnailUrl: string | null
|
||||
thumbnailMediaId: string | null
|
||||
image: string
|
||||
thumbnail: string
|
||||
images: { mediaId: string; url: string }[]
|
||||
galleryMediaIds: string[]
|
||||
commentCount: number
|
||||
variantCount: number
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
}
|
||||
|
||||
export interface ProductsListResponse {
|
||||
items: ProductApi[]
|
||||
total: number
|
||||
page: number
|
||||
pageSize: number
|
||||
}
|
||||
|
||||
export interface CreateProductPayload {
|
||||
title: string
|
||||
nameFa?: string
|
||||
summary?: string
|
||||
descriptionHtml?: string
|
||||
categoryId?: string
|
||||
brandId?: string
|
||||
status?: 'draft' | 'published' | 'archived'
|
||||
featuredMediaId?: string
|
||||
galleryMediaIds?: string[]
|
||||
tags?: string[]
|
||||
}
|
||||
|
||||
export interface UpdateProductPayload {
|
||||
title?: string
|
||||
nameFa?: string | null
|
||||
summary?: string | null
|
||||
descriptionHtml?: string | null
|
||||
categoryId?: string | null
|
||||
brandId?: string | null
|
||||
status?: 'draft' | 'published' | 'archived'
|
||||
featuredMediaId?: string | null
|
||||
galleryMediaIds?: string[]
|
||||
tags?: string[]
|
||||
}
|
||||
|
||||
function businessPath(suffix = '') {
|
||||
const businessId = getActiveBusinessId()
|
||||
if (!businessId) {
|
||||
throw new Error('No active business selected. Please sign in again.')
|
||||
}
|
||||
return `/businesses/${businessId}/products${suffix}`
|
||||
}
|
||||
|
||||
export function mapProductApiToUi(product: ProductApi): Product {
|
||||
return {
|
||||
id: product.id,
|
||||
nameEn: product.title,
|
||||
nameFa: product.nameFa,
|
||||
categoryId: product.categoryId ?? '',
|
||||
category: product.categoryName,
|
||||
summary: product.summary,
|
||||
description: product.descriptionHtml,
|
||||
image: product.image || product.thumbnail || '',
|
||||
thumbnail: product.thumbnail || product.thumbnailUrl || '',
|
||||
images: getProductGalleryUrls(product),
|
||||
tags: product.tags,
|
||||
status: product.status === 'published' ? 'active' : 'draft',
|
||||
commentCount: product.commentCount,
|
||||
variantCount: product.variantCount ?? 0,
|
||||
}
|
||||
}
|
||||
|
||||
export function getProductGalleryUrls(product: ProductApi): string[] {
|
||||
const urls: string[] = []
|
||||
const featured = product.thumbnailUrl || product.thumbnail || product.image
|
||||
|
||||
if (featured) {
|
||||
urls.push(featured)
|
||||
}
|
||||
|
||||
for (const item of product.images) {
|
||||
if (item.url && !urls.includes(item.url)) {
|
||||
urls.push(item.url)
|
||||
}
|
||||
}
|
||||
|
||||
return urls
|
||||
}
|
||||
|
||||
export function mapProductApiToFormState(product: ProductApi) {
|
||||
return {
|
||||
categoryId: product.categoryId ?? '',
|
||||
brandId: product.brandId ?? '',
|
||||
nameFa: product.nameFa,
|
||||
nameEn: product.title,
|
||||
summary: product.summary,
|
||||
description: product.descriptionHtml,
|
||||
thumbnail: product.thumbnailUrl ?? product.thumbnail ?? null,
|
||||
images: product.images.map((item) => item.url),
|
||||
tags: product.tags,
|
||||
thumbnailMediaId: product.thumbnailMediaId,
|
||||
galleryMediaIds: product.galleryMediaIds,
|
||||
}
|
||||
}
|
||||
|
||||
export async function listProducts(
|
||||
page = 1,
|
||||
pageSize = PRODUCTS_PER_PAGE,
|
||||
signal?: AbortSignal,
|
||||
filters?: { name?: string },
|
||||
) {
|
||||
const params = new URLSearchParams({
|
||||
page: String(page),
|
||||
pageSize: String(pageSize),
|
||||
})
|
||||
if (filters?.name?.trim()) {
|
||||
params.set('name', filters.name.trim())
|
||||
}
|
||||
|
||||
const data = await apiRequest<ProductsListResponse>(
|
||||
`${businessPath()}?${params.toString()}`,
|
||||
{ auth: true, signal },
|
||||
)
|
||||
return {
|
||||
...data,
|
||||
items: data.items.map(mapProductApiToUi),
|
||||
}
|
||||
}
|
||||
|
||||
export async function listAllProducts(signal?: AbortSignal) {
|
||||
const pageSize = 100
|
||||
let page = 1
|
||||
let items: ProductApi[] = []
|
||||
let total = Infinity
|
||||
|
||||
while (items.length < total) {
|
||||
const data = await apiRequest<ProductsListResponse>(
|
||||
`${businessPath()}?page=${page}&pageSize=${pageSize}`,
|
||||
{ auth: true, signal },
|
||||
)
|
||||
items = items.concat(data.items)
|
||||
total = data.total
|
||||
page += 1
|
||||
}
|
||||
|
||||
return items
|
||||
}
|
||||
|
||||
export async function getProduct(productId: string, signal?: AbortSignal) {
|
||||
const data = await apiRequest<{ product: ProductApi }>(
|
||||
businessPath(`/${productId}`),
|
||||
{ auth: true, signal },
|
||||
)
|
||||
return data.product
|
||||
}
|
||||
|
||||
export async function createProduct(payload: CreateProductPayload) {
|
||||
return apiRequest<{ message: string; product: ProductApi }>(businessPath(), {
|
||||
method: 'POST',
|
||||
auth: true,
|
||||
body: payload,
|
||||
})
|
||||
}
|
||||
|
||||
export async function updateProduct(productId: string, payload: UpdateProductPayload) {
|
||||
return apiRequest<{ message: string; product: ProductApi }>(
|
||||
businessPath(`/${productId}`),
|
||||
{
|
||||
method: 'PATCH',
|
||||
auth: true,
|
||||
body: payload,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
export async function deleteProduct(productId: string) {
|
||||
return apiRequest<{ message: string }>(businessPath(`/${productId}`), {
|
||||
method: 'DELETE',
|
||||
auth: true,
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user