mirror of
https://github.com/vercel/commerce.git
synced 2025-05-15 05:56:59 +00:00
341 lines
11 KiB
TypeScript
341 lines
11 KiB
TypeScript
import {
|
|
requestCart,
|
|
requestCategory,
|
|
requestCategoryList,
|
|
requestCategoryProductsCollection,
|
|
requestCrossSell,
|
|
requestNavigation,
|
|
requestProductsCollection,
|
|
requestSearchCollectionProducts,
|
|
requestSeoUrl,
|
|
requestSeoUrls
|
|
} from './api';
|
|
import { ExtendedCategory, ExtendedProduct, ExtendedProductListingResult } from './api-extended';
|
|
import {
|
|
getDefaultCategoryCriteria,
|
|
getDefaultCategoryWithCmsCriteria,
|
|
getDefaultCrossSellingCriteria,
|
|
getDefaultProductCriteria,
|
|
getDefaultProductsCriteria,
|
|
getDefaultSearchProductsCriteria,
|
|
getDefaultSubCategoriesCriteria,
|
|
getSortingCriteria
|
|
} from './criteria';
|
|
import {
|
|
transformCollection,
|
|
transformHandle,
|
|
transformMenu,
|
|
transformPage,
|
|
transformProduct,
|
|
transformProducts,
|
|
transformSubCollection
|
|
} from './transform';
|
|
import {
|
|
ApiSchemas,
|
|
Cart,
|
|
CategoryListingResultSW,
|
|
Menu,
|
|
Page,
|
|
Product,
|
|
ProductListingCriteria,
|
|
StoreNavigationTypeSW
|
|
} from './types';
|
|
|
|
export async function getMenu(params?: {
|
|
type?: StoreNavigationTypeSW;
|
|
depth?: number;
|
|
}): Promise<Menu[]> {
|
|
const type = params?.type || 'main-navigation';
|
|
const depth = params?.depth || 1;
|
|
const res = await requestNavigation(type, depth);
|
|
|
|
return res ? transformMenu(res, type) : [];
|
|
}
|
|
|
|
export async function getPage(handle: string | []): Promise<Page | undefined> {
|
|
const pageHandle = decodeURIComponent(transformHandle(handle));
|
|
const seoUrlElement = await getFirstSeoUrlElement(pageHandle);
|
|
if (seoUrlElement) {
|
|
const category = await getCategory(seoUrlElement);
|
|
|
|
if (!category) {
|
|
console.log('[getPage] Did not found any category with page handle:', pageHandle);
|
|
}
|
|
|
|
return category ? transformPage(seoUrlElement, category) : undefined;
|
|
}
|
|
|
|
if (!seoUrlElement) {
|
|
console.log('[getPage] Did not found any seoUrl element with page handle:', pageHandle);
|
|
}
|
|
}
|
|
|
|
export async function getFirstSeoUrlElement(
|
|
handle: string
|
|
): Promise<ApiSchemas['SeoUrl'] | undefined> {
|
|
const seoURL = await requestSeoUrl(handle);
|
|
if (seoURL && seoURL.elements && seoURL.elements.length > 0 && seoURL.elements[0]) {
|
|
return seoURL.elements[0];
|
|
}
|
|
}
|
|
|
|
export async function getFirstProduct(productId: string): Promise<ExtendedProduct | undefined> {
|
|
const productCriteria = getDefaultProductCriteria(productId);
|
|
const listing: ExtendedProductListingResult | undefined = await requestProductsCollection(
|
|
productCriteria
|
|
);
|
|
if (listing && listing.elements && listing.elements.length > 0 && listing.elements[0]) {
|
|
return listing.elements[0];
|
|
}
|
|
}
|
|
|
|
// ToDo: should be more dynamic (depending on handle), should work with server and not client see generateStaticParams from next.js
|
|
export async function getSubCollections(collection: string) {
|
|
let res: CategoryListingResultSW | undefined = undefined;
|
|
const parentCollectionName =
|
|
Array.isArray(collection) && collection[0] ? collection[0] : undefined;
|
|
const collectionName = transformHandle(collection ?? '');
|
|
const seoUrlElement = await getFirstSeoUrlElement(collectionName);
|
|
if (seoUrlElement) {
|
|
const criteria = getDefaultSubCategoriesCriteria(seoUrlElement.foreignKey);
|
|
// @ts-ignore
|
|
res = await requestCategoryList(criteria);
|
|
}
|
|
|
|
return res ? transformSubCollection(res, parentCollectionName) : [];
|
|
}
|
|
|
|
export async function getSearchCollectionProducts(params?: {
|
|
query?: string;
|
|
reverse?: boolean;
|
|
sortKey?: string;
|
|
categoryId?: string;
|
|
defaultSearchCriteria?: Partial<ProductListingCriteria>;
|
|
}) {
|
|
const searchQuery = params?.query ?? '';
|
|
const criteria = getDefaultSearchProductsCriteria(searchQuery);
|
|
const sorting = getSortingCriteria(params?.sortKey, params?.reverse);
|
|
const searchCriteria = { ...criteria, ...sorting };
|
|
|
|
const search = await requestSearchCollectionProducts(searchCriteria);
|
|
if (search) {
|
|
search.elements = await changeVariantUrlToParentUrl(search);
|
|
}
|
|
|
|
return search ? transformProducts(search) : [];
|
|
}
|
|
|
|
export async function changeVariantUrlToParentUrl(
|
|
collection: ExtendedProductListingResult
|
|
): Promise<ExtendedProduct[]> {
|
|
const newElements: ExtendedProduct[] = [];
|
|
if (collection.elements && collection.elements.length > 0) {
|
|
await Promise.all(
|
|
collection.elements.map(async (item) => {
|
|
if (item.parentId && item.seoUrls && item.seoUrls[0]) {
|
|
const parentProduct = await getFirstProduct(item.parentId);
|
|
if (parentProduct && parentProduct.seoUrls && parentProduct.seoUrls[0]) {
|
|
item.seoUrls[0].seoPathInfo = parentProduct.seoUrls[0].seoPathInfo;
|
|
}
|
|
}
|
|
|
|
newElements.push(item);
|
|
})
|
|
);
|
|
}
|
|
|
|
return newElements;
|
|
}
|
|
|
|
export async function getCollectionProducts(params?: {
|
|
collection: string;
|
|
page?: number;
|
|
reverse?: boolean;
|
|
sortKey?: string;
|
|
categoryId?: string;
|
|
defaultSearchCriteria?: Partial<ProductListingCriteria>;
|
|
}): Promise<{ products: Product[]; total: number; limit: number }> {
|
|
let products;
|
|
let category = params?.categoryId;
|
|
const collectionName = decodeURIComponent(transformHandle(params?.collection ?? ''));
|
|
const sorting = getSortingCriteria(params?.sortKey, params?.reverse);
|
|
|
|
if (!category && collectionName !== '') {
|
|
const seoUrlElement = await getFirstSeoUrlElement(collectionName);
|
|
if (seoUrlElement) {
|
|
category = seoUrlElement.foreignKey;
|
|
}
|
|
if (!category) {
|
|
console.log(
|
|
'[useListing][search] Did not found any category with collection name:',
|
|
collectionName
|
|
);
|
|
}
|
|
}
|
|
|
|
if (category) {
|
|
const criteria = !params?.defaultSearchCriteria
|
|
? getDefaultProductsCriteria(params?.page)
|
|
: params?.defaultSearchCriteria;
|
|
const productsCriteria = { ...criteria, ...sorting };
|
|
products = await requestCategoryProductsCollection(category, productsCriteria);
|
|
if (products) {
|
|
products.elements = await changeVariantUrlToParentUrl(products);
|
|
}
|
|
}
|
|
|
|
return products
|
|
? {
|
|
products: transformProducts(products),
|
|
total: products.total ?? 0,
|
|
limit: products.limit ?? 0
|
|
}
|
|
: { products: [], total: 0, limit: 0 };
|
|
}
|
|
|
|
export async function getCategory(
|
|
seoUrl: ApiSchemas['SeoUrl'],
|
|
cms: boolean = false
|
|
): Promise<ExtendedCategory | undefined> {
|
|
const criteria = cms ? getDefaultCategoryWithCmsCriteria() : getDefaultCategoryCriteria();
|
|
return await requestCategory(seoUrl.foreignKey, criteria);
|
|
}
|
|
|
|
// This function is only used for generateMetadata at app/search/(collection)/[...collection]/page.tsx
|
|
export async function getCollection(handle: string | []) {
|
|
const collectionName = decodeURIComponent(transformHandle(handle));
|
|
const seoUrlElement = await getFirstSeoUrlElement(collectionName);
|
|
if (seoUrlElement) {
|
|
const category = await getCategory(seoUrlElement);
|
|
const path = seoUrlElement.seoPathInfo ?? '';
|
|
if (category) {
|
|
const collection = transformCollection(seoUrlElement, category);
|
|
|
|
return {
|
|
...collection,
|
|
path: `/search/${path}`
|
|
};
|
|
}
|
|
}
|
|
}
|
|
|
|
export async function getProductSeoUrls() {
|
|
const productSeoUrls: { path: string; updatedAt: string }[] = [];
|
|
const seoUrls = await requestSeoUrls('frontend.detail.page');
|
|
|
|
if (seoUrls && seoUrls.elements && seoUrls.elements.length > 0) {
|
|
seoUrls.elements.map((item) =>
|
|
productSeoUrls.push({ path: item.seoPathInfo, updatedAt: item.updatedAt ?? item.createdAt })
|
|
);
|
|
}
|
|
|
|
return productSeoUrls;
|
|
}
|
|
|
|
export async function getProduct(handle: string | []): Promise<Product | undefined> {
|
|
let productSW: ExtendedProduct | undefined;
|
|
let productId: string | undefined;
|
|
const productHandle = decodeURIComponent(transformHandle(handle));
|
|
|
|
const seoUrlElement = await getFirstSeoUrlElement(productHandle);
|
|
if (seoUrlElement) {
|
|
productId = seoUrlElement.foreignKey;
|
|
}
|
|
|
|
if (!productId) {
|
|
console.log('[getProduct][search] Did not found any product with handle:', productHandle);
|
|
}
|
|
if (productId) {
|
|
const firstProduct = await getFirstProduct(productId);
|
|
if (firstProduct) {
|
|
productSW = firstProduct;
|
|
}
|
|
}
|
|
|
|
return productSW ? transformProduct(productSW) : undefined;
|
|
}
|
|
|
|
export async function getProductRecommendations(productId: string): Promise<Product[]> {
|
|
const products: ExtendedProductListingResult = {};
|
|
|
|
const res = await requestCrossSell(productId, getDefaultCrossSellingCriteria());
|
|
// @ToDo: Make this more dynamic to merge multiple Cross-Sellings, at the moment we only get the first one
|
|
if (res && res[0] && res[0].products) {
|
|
products.elements = res[0].products;
|
|
}
|
|
|
|
return products ? transformProducts(products) : [];
|
|
}
|
|
|
|
export async function getCart(): Promise<Cart | undefined> {
|
|
const cartData = await requestCart();
|
|
if (cartData) {
|
|
// @ToDo: should be moved to transformCart function
|
|
const cart: Cart = {
|
|
checkoutUrl: 'https://frontends-demo.vercel.app',
|
|
cost: {
|
|
subtotalAmount: {
|
|
amount: cartData.price?.positionPrice?.toString() || '0',
|
|
currencyCode: 'EUR'
|
|
},
|
|
totalAmount: {
|
|
amount: cartData.price?.totalPrice?.toString() || '0',
|
|
currencyCode: 'EUR'
|
|
},
|
|
totalTaxAmount: {
|
|
amount: '0',
|
|
currencyCode: 'EUR'
|
|
}
|
|
},
|
|
id: cartData.token || '',
|
|
lines:
|
|
cartData.lineItems?.map((lineItem) => ({
|
|
id: lineItem.referencedId || '',
|
|
quantity: lineItem.quantity ?? 0,
|
|
cost: {
|
|
totalAmount: {
|
|
amount: (lineItem as any)?.price?.totalPrice || '',
|
|
currencyCode: 'EUR'
|
|
}
|
|
},
|
|
merchandise: {
|
|
id: lineItem.referencedId ?? '',
|
|
title: lineItem.label ?? '',
|
|
selectedOptions: [],
|
|
product: {
|
|
description: lineItem.description ?? '',
|
|
descriptionHtml: lineItem.description ?? '',
|
|
id: lineItem.referencedId ?? '',
|
|
images: [],
|
|
path: '',
|
|
seo: {
|
|
description: lineItem.description ?? '',
|
|
title: lineItem.label ?? ''
|
|
},
|
|
availableForSale: true,
|
|
featuredImage: (lineItem as any).cover?.url,
|
|
handle: '',
|
|
options: [],
|
|
variants: [],
|
|
priceRange: {
|
|
minVariantPrice: {
|
|
amount: '', // @ToDo: should be correct value
|
|
currencyCode: 'EUR'
|
|
},
|
|
maxVariantPrice: {
|
|
amount: '', // @ToDo: should be correct value
|
|
currencyCode: 'EUR'
|
|
}
|
|
},
|
|
tags: [],
|
|
title: lineItem.label ?? '',
|
|
updatedAt: (lineItem as any)?.payload?.updatedAt
|
|
}
|
|
}
|
|
})) || [],
|
|
totalQuantity: cartData.lineItems?.length || 0
|
|
};
|
|
return cart;
|
|
}
|
|
}
|