import { AjaxError } from 'lib/shopify/ajax'; import { Payload, Where } from 'lib/shopify/payload'; import { Cart as PayloadCart, Category as PayloadCategory, Media as PayloadMedia, Option as PayloadOption, Product as PayloadProduct } from 'lib/shopify/payload-types'; import { Cart, CartItem, Collection, Image, Menu, Money, Page, Product, ProductOption, ProductVariant } from './types'; const payload = new Payload({ baseUrl: process.env.CMS_URL }); const reshapeCartItems = (lines: PayloadCart['lines']): CartItem[] => { return (lines ?? []).map((item) => { const product = item.product as PayloadProduct; const variant = product.variants.find((v) => v.id === item.variant); return { id: item.id!, quantity: item.quantity, merchandise: { id: item.variant, title: product.title, selectedOptions: [], product: reshapeProduct(product) }, cost: { totalAmount: reshapePrice(variant?.price!) } }; }); }; const reshapeCart = (cart: PayloadCart): Cart => { const currencyCode = cart.currencyCode!; return { id: cart.id, checkoutUrl: '/api/checkout', cost: { totalAmount: { currencyCode, amount: cart.totalAmount?.toString()! }, totalTaxAmount: { currencyCode, amount: cart.totalTaxAmount?.toString()! }, subtotalAmount: { currencyCode, amount: '0.0' } }, lines: reshapeCartItems(cart.lines), totalQuantity: cart.totalQuantity ?? 0 }; }; export async function createCart(): Promise { const cart = await payload.create('carts', { lines: [] }); return reshapeCart(cart.doc); } export async function addToCart( cartId: string, lines: { merchandiseId: string; quantity: number }[] ): Promise { const prevCart = await payload.findByID('carts', cartId, { depth: 0 }); const cartItems = await mergeItems(prevCart.lines, lines, true); const cart = await payload.update('carts', cartId, { lines: cartItems }); return reshapeCart(cart.doc); } export async function removeFromCart(cartId: string, lineIds: string[]): Promise { const prevCart = await payload.findByID('carts', cartId, { depth: 0 }); const lines = prevCart?.lines?.filter((lineItem) => !lineIds.includes(lineItem.id!)) ?? []; const cart = await payload.update('carts', cartId, { lines }); return reshapeCart(cart.doc); } const mergeItems = async ( cartItems: PayloadCart['lines'], lines: { merchandiseId: string; quantity: number }[], add: boolean ): Promise => { const map = new Map((cartItems ?? []).map((item) => [item.variant, item])); const products = await payload.find('products', { where: { 'variants.id': { in: lines.map((line) => line.merchandiseId) } } }); lines.forEach((line) => { const product = products.docs.find((p) => p.variants.some((variant) => variant.id === line.merchandiseId) ); let item = { product: product?.id!, variant: line.merchandiseId, quantity: line.quantity }; if (add) { const added = map.get(line.merchandiseId); if (added) { item = { ...item, quantity: item.quantity + added.quantity }; } } map.set(line.merchandiseId, item); }); return Array.from(map).map(([, item]) => item); }; export async function updateCart( cartId: string, lines: { id: string; merchandiseId: string; quantity: number }[] ): Promise { const prevCart = await payload.findByID('carts', cartId, { depth: 0 }); const cartItems = await mergeItems(prevCart.lines, lines, false); const cart = await payload.update('carts', cartId, { lines: cartItems }); return reshapeCart(cart.doc); } export async function getCart(cartId: string): Promise { try { const cart = await payload.findByID('carts', cartId); return reshapeCart(cart); } catch (error: unknown) { if (error instanceof AjaxError) { if (error.statusCode === 404) { return undefined; } } throw error; } } export async function getCollection(handle: string): Promise { const category = await payload.findByID('categories', handle); return reshapeCategory(category); } const reshapeImage = (media: PayloadMedia): Image => { return { url: media.url!, altText: media.alt, width: media.width ?? 0, height: media.height ?? 0 }; }; type Price = { amount: number; currencyCode: string; }; const reshapePrice = (price: Price): Money => { return { amount: price.amount.toString(), currencyCode: price.currencyCode }; }; const reshapeOptions = (variants: PayloadProduct['variants']): ProductOption[] => { const options = new Map(); variants.forEach((variant) => { variant.selectedOptions?.forEach((selectedOption) => { const option = selectedOption.option as PayloadOption; options.set(option.id, option); }); }); return Array.from(options, ([id, option]) => ({ id, name: option.name, values: option.values.map((value) => value.label) })); }; const reshapeSelectedOption = ( selectedOptions: PayloadProduct['variants'][0]['selectedOptions'] ): Array<{ name: string; value: string }> => { return (selectedOptions ?? []).map((selectedOption) => { const option = selectedOption.option as PayloadOption; return { name: option.name, value: option.values.find(({ value }) => value === selectedOption.value)?.label! }; }); }; const reshapeVariants = (variants: PayloadProduct['variants']): ProductVariant[] => { return variants.map((variant) => ({ id: variant.id!, title: `${variant.price.amount} ${variant.price.currencyCode}`, availableForSale: true, selectedOptions: reshapeSelectedOption(variant.selectedOptions), price: reshapePrice(variant.price) })); }; const reshapeProduct = (product: PayloadProduct): Product => { return { id: product.id, handle: product.id, availableForSale: !product.disabled, title: product.title, description: product.description, descriptionHtml: product.description, options: reshapeOptions(product.variants), priceRange: { maxVariantPrice: reshapePrice(product.variants[0]?.price!), minVariantPrice: reshapePrice(product.variants[0]?.price!) }, featuredImage: reshapeImage(product.media as PayloadMedia), images: [reshapeImage(product.media as PayloadMedia)], seo: { title: product.title, description: product.description }, tags: product.tags ?? [], variants: reshapeVariants(product.variants), updatedAt: product.updatedAt }; }; export async function getCollectionProducts({ collection, tag, sortKey, reverse }: { collection?: string; tag?: string; reverse?: boolean; sortKey?: string; }): Promise { if (sortKey && reverse) { sortKey = '-' + sortKey; } const filters: Where[] = []; if (collection) { filters.push({ categories: { equals: collection } }); } if (tag) { filters.push({ tags: { equals: tag } }); } const products = await payload.find('products', { where: { and: filters }, sort: sortKey }); return products.docs.map(reshapeProduct); } const reshapeCategory = (category: PayloadCategory): Collection => { return { handle: category.id, title: category.title, description: category.description!, seo: { title: category.title, description: category.description! }, path: `/search/${category.id}`, updatedAt: category.updatedAt }; }; export async function getCollections(): Promise { const categories = await payload.find('categories', {}); return [ { handle: '', title: 'All', description: 'All products', seo: { title: 'All', description: 'All products' }, path: '/search', updatedAt: new Date().toISOString() }, ...categories.docs.map(reshapeCategory) ]; } export async function getMenu(handle: string): Promise { switch (handle) { case 'next-js-frontend-footer-menu': return [ { title: 'About Medusa', path: 'https://medusajs.com/' }, { title: 'Medusa Docs', path: 'https://docs.medusajs.com/' }, { title: 'Medusa Blog', path: 'https://medusajs.com/blog' } ]; case 'next-js-frontend-header-menu': return await getCollections(); default: return []; } } // eslint-disable-next-line no-unused-vars export async function getPage(handle: string): Promise { return undefined; } export async function getPages(): Promise { return []; } export async function getProduct(handle: string): Promise { const product = await payload.findByID('products', handle); return reshapeProduct(product); } // eslint-disable-next-line no-unused-vars export async function getProductRecommendations(productId: string): Promise { return []; } export async function getProducts({ query, sortKey, reverse }: { query?: string; reverse?: boolean; sortKey?: string; }): Promise { if (sortKey && reverse) { sortKey = '-' + sortKey; } let where: Where | undefined; if (query) { where = { or: [ { title: { contains: query } }, { description: { contains: query } } ] }; } const products = await payload.find('products', { where, sort: sortKey }); return products.docs.map(reshapeProduct); }