RTL carousels/FABs, special key+title fields, slider gallery fixes, and dashboard SSL sync agent for super-admin. Co-authored-by: Cursor <cursoragent@cursor.com>
210 lines
5.2 KiB
TypeScript
210 lines
5.2 KiB
TypeScript
import { apiRequest } from '../lib/api'
|
|
import { getActiveBusinessId } from '../lib/businessContext'
|
|
import type { Product } from '../types/product'
|
|
|
|
export const PRODUCTS_PER_PAGE = 24
|
|
|
|
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
|
|
categoryNameFa?: 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,
|
|
categoryFa: product.categoryNameFa || '',
|
|
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,
|
|
})
|
|
}
|