import { apiFetch } from './client'; import type { BrandFilter, CategoryApiData, CategoryData, ComboSet, PagingCollection, PriceFilter, Product, ProductImageDetail, ProductImageGallery, ProductDetailData, } from '@/types'; import type { TypeListProduct } from '@/types/global/TypeListProduct'; import { normalizeAssetUrl, normalizeHtmlAssetUrls } from '@/lib/normalizeAssetUrl'; function normalizeSlug(slug: string) { return slug.replace(/^\/+/, ''); } function normalizeSearch(search?: string) { if (!search) { return ''; } return search.startsWith('?') ? search : `?${search}`; } function normalizeProductList(productList: Product[] | Record | undefined) { if (!productList) { return []; } return Array.isArray(productList) ? productList : Object.values(productList); } function normalizeProductImage(image: ProductImageDetail): ProductImageDetail { return { small: normalizeAssetUrl(image.small), large: normalizeAssetUrl(image.large), original: normalizeAssetUrl(image.original), }; } function normalizeProductGallery(images: ProductImageGallery[] | undefined) { if (!images) { return []; } return images.map((image) => ({ ...image, size: normalizeProductImage(image.size), })); } function normalizeComboSet(comboSet: ComboSet[] | undefined) { if (!comboSet) { return []; } return comboSet.map((set) => ({ ...set, group_list: set.group_list.map((group) => ({ ...group, product_list: group.product_list.map((product) => ({ ...product, images: { small: normalizeAssetUrl(product.images.small), large: normalizeAssetUrl(product.images.large), }, })), })), })); } function normalizeProductDetail(data: ProductDetailData): ProductDetailData { return { ...data, image: normalizeAssetUrl(data.image), combo_set: normalizeComboSet(data.combo_set), product_info: { ...data.product_info, productImage: normalizeProductImage(data.product_info.productImage), productImageGallery: normalizeProductGallery(data.product_info.productImageGallery), productDescription: normalizeHtmlAssetUrls(data.product_info.productDescription), productSpec: normalizeHtmlAssetUrls(data.product_info.productSpec), multipartSpec: normalizeHtmlAssetUrls(data.product_info.multipartSpec), productSummary: normalizeHtmlAssetUrls(data.product_info.productSummary), thum_poster: normalizeAssetUrl(data.product_info.thum_poster), }, }; } function toNumber(value: string | number | null | undefined) { const normalizedValue = typeof value === 'string' ? value.replace(/[^\d.-]/g, '') : (value ?? 0).toString(); const parsedValue = Number(normalizedValue); return Number.isFinite(parsedValue) ? parsedValue : 0; } function normalizeText(value: string | number | undefined) { return String(value ?? '') .normalize('NFD') .replace(/[\u0300-\u036f]/g, '') .toLowerCase(); } function isCurrentFilterUrl(searchParams: URLSearchParams, targetUrl: string) { const target = new URL(targetUrl, 'http://local'); const targetParams = new URLSearchParams(target.search); if ([...targetParams.keys()].length !== [...searchParams.keys()].length) { return false; } return [...targetParams.entries()].every( ([key, value]) => (searchParams.get(key) ?? '') === value, ); } function filterByBrand(products: Product[], brandFilter: BrandFilter | undefined) { if (!brandFilter) { return products; } const expectedBrand = normalizeText(brandFilter.brand_index || brandFilter.name); return products.filter((product) => { const brandCandidates = [ product.brand?.brand_index, product.brand?.name, product.brand?.url, product.categories?.[0]?.name, ]; return brandCandidates.some((candidate) => normalizeText(candidate).includes(expectedBrand)); }); } function filterByPrice(products: Product[], priceFilter: PriceFilter | undefined) { if (!priceFilter || (!priceFilter.min && !priceFilter.max)) { return products; } const min = toNumber(priceFilter.min); const max = toNumber(priceFilter.max); return products.filter((product) => { const price = toNumber(product.price); if (min > 0 && price < min) { return false; } if (max > 0 && price > max) { return false; } return true; }); } function sortProducts(products: Product[], sortKey: string | null) { const sortedProducts = [...products]; switch (sortKey) { case 'price-desc': sortedProducts.sort((left, right) => toNumber(right.price) - toNumber(left.price)); break; case 'price-asc': sortedProducts.sort((left, right) => toNumber(left.price) - toNumber(right.price)); break; case 'view': sortedProducts.sort((left, right) => toNumber(right.visit) - toNumber(left.visit)); break; case 'comment': sortedProducts.sort( (left, right) => toNumber(right.comment?.total) - toNumber(left.comment?.total), ); break; case 'rating': sortedProducts.sort((left, right) => toNumber(right.rating) - toNumber(left.rating)); break; case 'name': sortedProducts.sort((left, right) => left.productName.localeCompare(right.productName, 'vi')); break; case 'new': default: sortedProducts.sort( (left, right) => new Date(right.lastUpdate).getTime() - new Date(left.lastUpdate).getTime(), ); break; } return sortedProducts; } function buildPagingCollection( category: CategoryApiData, searchParams: URLSearchParams, totalPages: number, currentPage: number, ): PagingCollection[] { return Array.from({ length: totalPages }, (_, index) => { const page = index + 1; const nextSearchParams = new URLSearchParams(searchParams); if (page === 1) { nextSearchParams.delete('page'); } else { nextSearchParams.set('page', String(page)); } const queryString = nextSearchParams.toString(); return { name: String(page), url: queryString ? `${category.current_category.url}?${queryString}` : category.current_category.url, is_active: page === currentPage ? '1' : '0', }; }); } function applyCategorySearchParams(category: CategoryApiData, search?: string): CategoryData { const searchParams = new URLSearchParams(normalizeSearch(search)); let productList = normalizeProductList(category.product_list); if (searchParams.get('other_filter') === 'in-stock') { productList = productList.filter((product) => toNumber(product.quantity) > 0); } const selectedBrand = category.brand_filter_list?.find((item) => isCurrentFilterUrl(searchParams, item.url), ); productList = filterByBrand(productList, selectedBrand); const selectedPrice = category.price_filter_list?.find((item) => isCurrentFilterUrl(searchParams, item.url), ); productList = filterByPrice(productList, selectedPrice); productList = sortProducts(productList, searchParams.get('sort')); const totalProducts = productList.length; const configuredPageCount = toNumber(category.paging_count); const perPage = configuredPageCount > 0 ? Math.max(1, Math.ceil(totalProducts / configuredPageCount)) : totalProducts; const totalPages = perPage > 0 ? Math.max(1, Math.ceil(totalProducts / perPage)) : Math.max(1, configuredPageCount); const requestedPage = toNumber(searchParams.get('page')); const currentPage = Math.min(Math.max(requestedPage || 1, 1), totalPages); const pagedProducts = perPage > 0 ? productList.slice((currentPage - 1) * perPage, currentPage * perPage) : productList; return { ...category, product_count: String(totalProducts), paging_count: String(totalPages), paging_collection: buildPagingCollection(category, searchParams, totalPages, currentPage), product_list: pagedProducts, }; } export function getProductHot() { return apiFetch('/products/hot'); } export function getProductDetail(slug: string) { return apiFetch(`/products/${normalizeSlug(slug)}`).then((product) => normalizeProductDetail(product), ); } export function getProductComments(slug: string) { return apiFetch( `/products/${normalizeSlug(slug)}/comments`, ); } export function getProductReviews(slug: string) { return apiFetch( `/products/${normalizeSlug(slug)}/reviews`, ); } export function getProductCategory(slug: string, search?: string) { const normalizedSearch = normalizeSearch(search); return apiFetch(`/products/category/${normalizeSlug(slug)}${normalizedSearch}`).then( (category) => applyCategorySearchParams(category, normalizedSearch), ); } export function getProductHotPage(slug: string) { return apiFetch( `/products/hot-page/${normalizeSlug(slug)}`, ); } export function getProductSearch(q: string) { return apiFetch(`/products/search?q=${encodeURIComponent(q)}`); }