From fb112385e6b270a6812362ebc6e6f44c0cb70d0b Mon Sep 17 00:00:00 2001 From: tedraykov Date: Fri, 21 May 2021 06:09:08 +0300 Subject: [PATCH 1/3] Implement product attributes (variants) --- .../product/ProductView/ProductView.tsx | 4 +- framework/reactioncommerce/auth/use-login.tsx | 2 +- framework/reactioncommerce/utils/normalize.ts | 228 ++++++++++-------- 3 files changed, 126 insertions(+), 108 deletions(-) diff --git a/components/product/ProductView/ProductView.tsx b/components/product/ProductView/ProductView.tsx index 16c02ca51..d38da3528 100644 --- a/components/product/ProductView/ProductView.tsx +++ b/components/product/ProductView/ProductView.tsx @@ -49,10 +49,10 @@ const ProductView: FC = ({ product }) => { await addItem({ productId: String(product.id), - variantId: String(selectedVariant.sku), + variantId: String(selectedVariant.id), pricing: { amount: selectedVariant.price, - currencyCode: product.price.currencyCode, + currencyCode: product.price.currencyCode ?? 'USD', }, }) openSidebar() diff --git a/framework/reactioncommerce/auth/use-login.tsx b/framework/reactioncommerce/auth/use-login.tsx index fb1fd22fe..a479d929f 100644 --- a/framework/reactioncommerce/auth/use-login.tsx +++ b/framework/reactioncommerce/auth/use-login.tsx @@ -1,6 +1,6 @@ import { useCallback } from 'react' import type { MutationHook } from '@commerce/utils/types' -import { CommerceError, ValidationError } from '@commerce/utils/errors' +import { CommerceError } from '@commerce/utils/errors' import useCustomer from '../customer/use-customer' import authenticateMutation from '../utils/mutations/authenticate' import { diff --git a/framework/reactioncommerce/utils/normalize.ts b/framework/reactioncommerce/utils/normalize.ts index 296832e6e..c97eb6185 100644 --- a/framework/reactioncommerce/utils/normalize.ts +++ b/framework/reactioncommerce/utils/normalize.ts @@ -1,9 +1,8 @@ -import { Product, Customer } from '@commerce/types' +import {Product, Customer, ProductVariant, ProductOption, ProductOptionValues} from '@commerce/types' import { Account, Cart as ReactionCart, - ProductPricingInfo, CatalogProductVariant, CartItemEdge, CatalogItemProduct, @@ -12,20 +11,7 @@ import { Maybe, } from '../schema' -import type { Cart, LineItem } from '../types' - -type ProductOption = { - __typename?: string - id: string - displayName: string - values: any[] -} - -const money = ({ displayPrice }: ProductPricingInfo) => { - return { - displayPrice, - } -} +import type {Cart, LineItem} from '../types' const normalizeProductImages = (images: Maybe[], name: string) => images.map((image) => ({ @@ -33,83 +19,104 @@ const normalizeProductImages = (images: Maybe[], name: string) => alt: name, })) -const normalizeProductOption = ({ id, displayName, values }: ProductOption) => { - return { +const normalizeProductOption = (variant: CatalogProductVariant) => { + const option = { __typename: 'MultipleChoiceOption', - id, - displayName, - values: values.map((value) => { - let output: any = { - label: value, - } - if (displayName.toLowerCase() === 'color') { - output = { - ...output, - hexColors: [value], - } - } - return output - }), + id: variant._id, + displayName: variant.attributeLabel, + values: variant.optionTitle ? [{label: variant.optionTitle}] : [] } + option.values = option.values.map(value => colorizeProductOptionValue(value, option.displayName)) + + return option; } -const normalizeProductVariants = (variants: CatalogProductVariant[]) => { - return variants.map((variant) => { - const { _id, options, sku, title, pricing = [], variantId } = variant ?? {} +function colorizeProductOptionValue(value: ProductOptionValues, displayName: string): ProductOptionValues { + if (displayName.toLowerCase() === 'color') { + value.hexColors = [value.label] + } + return value; +} + +const normalizeProductVariants = (variants: CatalogProductVariant[]): ProductVariant[] => { + return variants.reduce((productVariants: ProductVariant[], variant: CatalogProductVariant) => { + + if (variantHasOptions(variant)) { + productVariants.push(...flatVariantOptions(variant)) + return productVariants + } + + const {sku, title, pricing = [], variantId} = variant ?? {} const variantPrice = pricing[0]?.price ?? pricing[0]?.minPrice ?? 0 - return { - id: _id ?? '', + productVariants.push({ + id: variantId ?? '', name: title, sku: sku ?? variantId, price: variantPrice, listPrice: pricing[0]?.compareAtPrice?.amount ?? variantPrice, requiresShipping: true, - options: options?.length - ? options.map((option) => { - return normalizeProductOption({ - id: option?._id ?? '', - displayName: option?.attributeLabel ?? '', - values: [option?.optionTitle], - }) - }) - : [], - } - }) + options: [normalizeProductOption(variant)] + }); + + return productVariants; + }, []) } -export function groupProductOptionsByAttributeLabel( - options: CatalogProductVariant[] -) { - return options.reduce((groupedOptions, currentOption) => { - const attributeLabelIndex = groupedOptions.findIndex((option) => { - return ( - option.displayName.toLowerCase() === - currentOption?.attributeLabel.toLowerCase() - ) - }) +function groupProductOptionsByAttributeLabel(variants: CatalogProductVariant[]): ProductOption[] { + return variants.reduce((groupedOptions: ProductOption[], currentVariant: CatalogProductVariant) => { - if (attributeLabelIndex !== -1) { - groupedOptions[attributeLabelIndex].values = [ - ...groupedOptions[attributeLabelIndex].values, - { - label: currentOption?.optionTitle ?? '', - hexColors: [currentOption?.optionTitle] ?? '', - }, - ] - } else { - groupedOptions = [ - ...groupedOptions, - normalizeProductOption({ - id: currentOption?._id ?? '', - displayName: currentOption?.attributeLabel ?? '', - values: [currentOption?.optionTitle ?? ''], - }), - ] + groupedOptions = mergeVariantOptionsWithExistingOptions(groupedOptions, currentVariant); + + if (variantHasOptions(currentVariant)) { + (currentVariant.options).forEach(variantOption => { + groupedOptions = mergeVariantOptionsWithExistingOptions(groupedOptions, variantOption) + }) } return groupedOptions - }, [] as ProductOption[]) + }, []) + + function mergeVariantOptionsWithExistingOptions( + groupedOptions: ProductOption[], + currentVariant: CatalogProductVariant): ProductOption[] { + + const matchingOptionIndex = findCurrentVariantOptionsInGroupedOptions(groupedOptions, currentVariant) + + return matchingOptionIndex !== -1 ? + mergeWithExistingOptions(groupedOptions, currentVariant, matchingOptionIndex) : + addNewProductOption(groupedOptions, currentVariant) + } + + function findCurrentVariantOptionsInGroupedOptions( + groupedOptions: ProductOption[], + currentVariant: CatalogProductVariant): number { + + return groupedOptions.findIndex(option => + (option.displayName.toLowerCase() === currentVariant.attributeLabel.toLowerCase()) + ); + } + + function mergeWithExistingOptions( + groupedOptions: ProductOption[], + currentVariant: CatalogProductVariant, + matchingOptionIndex: number) { + + const currentVariantOption = normalizeProductOption(currentVariant); + groupedOptions[matchingOptionIndex].values = [ + ...groupedOptions[matchingOptionIndex].values, + ...currentVariantOption.values + ] + + return groupedOptions; + } + + function addNewProductOption(groupedOptions: ProductOption[], currentVariant: CatalogProductVariant) { + return [ + ...groupedOptions, + normalizeProductOption(currentVariant), + ]; + } } export function normalizeProduct(productNode: CatalogItemProduct): Product { @@ -124,9 +131,7 @@ export function normalizeProduct(productNode: CatalogItemProduct): Product { sku, media, pricing, - vendor, - variants, - ...rest + variants } = product return { @@ -142,11 +147,8 @@ export function normalizeProduct(productNode: CatalogItemProduct): Product { value: pricing[0]?.minPrice ?? 0, currencyCode: pricing[0]?.currency.code, }, - variants: variants?.length ? normalizeProductVariants(variants) : [], - options: variants?.length - ? groupProductOptionsByAttributeLabel(variants) - : [], - ...rest, + variants: variants !== null ? normalizeProductVariants(variants) : [], + options: variants !== null ? groupProductOptionsByAttributeLabel(variants) : [] } } @@ -157,30 +159,32 @@ export function normalizeCart(cart: ReactionCart): Cart { email: '', createdAt: cart.createdAt, currency: { - code: cart.checkout?.summary?.total?.currency.code, + code: cart.checkout?.summary?.total?.currency.code ?? '', }, taxesIncluded: false, - lineItems: cart.items?.edges?.map(normalizeLineItem), - lineItemsSubtotalPrice: +cart.checkout?.summary?.itemTotal?.amount, - subtotalPrice: +cart.checkout?.summary?.itemTotal?.amount, - totalPrice: cart.checkout?.summary?.total?.amount, + lineItems: cart.items?.edges?.map(cartItem => normalizeLineItem(cartItem)) ?? [], + lineItemsSubtotalPrice: +(cart.checkout?.summary?.itemTotal?.amount ?? 0), + subtotalPrice: +(cart.checkout?.summary?.itemTotal?.amount ?? 0), + totalPrice: cart.checkout?.summary?.total?.amount ?? 0, discounts: [], } } -function normalizeLineItem({ - node: { - _id, - compareAtPrice, - imageURLs, - title, - productConfiguration, - priceWhenAdded, - optionTitle, - variantTitle, - quantity, - }, -}: CartItemEdge): LineItem { +function normalizeLineItem(cartItem: CartItemEdge): LineItem { + const { + node: { + _id, + compareAtPrice, + imageURLs, + title, + productConfiguration, + priceWhenAdded, + optionTitle, + variantTitle, + quantity, + } + } = cartItem + console.log('imageURLs', imageURLs) return { id: _id, @@ -193,7 +197,7 @@ function normalizeLineItem({ sku: String(productConfiguration?.productVariantId), name: String(optionTitle || variantTitle), image: { - url: imageURLs?.original ?? '/product-img-placeholder.svg', + url: imageURLs?.thumbnail ?? '/product-img-placeholder.svg', }, requiresShipping: true, price: priceWhenAdded?.amount, @@ -211,12 +215,26 @@ function normalizeLineItem({ export function normalizeCustomer(viewer: Account): Customer { if (!viewer) { - return {} + return {} } - return { + return { firstName: viewer.firstName ?? '', lastName: viewer.lastName ?? '', email: viewer.primaryEmailAddress, } } + +function flatVariantOptions(variant: CatalogProductVariant): ProductVariant[] { + const variantOptions = variant.options; + + return normalizeProductVariants(variantOptions) + .map(variantOption => { + variantOption.options.push(normalizeProductOption(variant)) + return variantOption + }); +} + +function variantHasOptions(variant: CatalogProductVariant) { + return !!variant.options && variant.options.length != 0; +} From c6df70c34c90dde6bfc7e7297ccf8665ebf43643 Mon Sep 17 00:00:00 2001 From: tedraykov Date: Sun, 23 May 2021 19:15:01 +0300 Subject: [PATCH 2/3] Graphql codegen change When generating graphql types, the generation of optional types is redundant because the value is wrapped in Maybe type anyway. Removing the redundancy simplifies the type checking whenever generated types are used. --- framework/commerce/types.ts | 16 +++---- .../api/cart/handlers/get-cart.ts | 7 ++- framework/reactioncommerce/codegen.json | 5 ++- framework/reactioncommerce/utils/normalize.ts | 44 +++++++++---------- 4 files changed, 37 insertions(+), 35 deletions(-) diff --git a/framework/commerce/types.ts b/framework/commerce/types.ts index 86361fd9f..7bf44e774 100644 --- a/framework/commerce/types.ts +++ b/framework/commerce/types.ts @@ -63,6 +63,7 @@ export type ProductVariant = { // The variant's depth. If a depth was not explicitly specified on the // variant, this will be the product's depth. depth?: Measurement + options: ProductOption[] } // Shopping cart, a.k.a Checkout @@ -109,6 +110,10 @@ export type CartItemBody = { variantId: string productId?: string quantity?: number + pricing?: { + amount: number, + currencyCode: string, + } } // Body used by the `getCart` operation handler @@ -167,18 +172,18 @@ export interface Product extends Entity { slug?: string path?: string images: ProductImage[] - variants: ProductVariant2[] + variants: ProductVariant[] price: ProductPrice options: ProductOption[] sku?: string } -interface ProductOption extends Entity { +export interface ProductOption extends Entity { displayName: string values: ProductOptionValues[] } -interface ProductOptionValues { +export interface ProductOptionValues { label: string hexColors?: string[] } @@ -188,11 +193,6 @@ interface ProductImage { alt?: string } -interface ProductVariant2 { - id: string | number - options: ProductOption[] -} - interface ProductPrice { value: number currencyCode: 'USD' | 'ARS' | string | undefined diff --git a/framework/reactioncommerce/api/cart/handlers/get-cart.ts b/framework/reactioncommerce/api/cart/handlers/get-cart.ts index a6f2e58e7..f322bb669 100644 --- a/framework/reactioncommerce/api/cart/handlers/get-cart.ts +++ b/framework/reactioncommerce/api/cart/handlers/get-cart.ts @@ -1,6 +1,5 @@ -import type { Cart } from '../../../types' import type { CartHandlers } from '../' -import getAnomymousCartQuery from '@framework/utils/queries/get-anonymous-cart' +import getAnonymousCartQuery from '@framework/utils/queries/get-anonymous-cart' import accountCartByAccountIdQuery from '@framework/utils/queries/account-cart-by-account-id' import getCartCookie from '@framework/api/utils/get-cart-cookie' import reconcileCarts from '@framework/api/utils/reconcile-carts' @@ -9,7 +8,7 @@ import { REACTION_ANONYMOUS_CART_TOKEN_COOKIE, REACTION_CART_ID_COOKIE, REACTION_CUSTOMER_TOKEN_COOKIE, -} from '@framework/const.ts' +} from '@framework/const' import { normalizeCart } from '@framework/utils' // Return current cart info @@ -42,7 +41,7 @@ const getCart: CartHandlers['getCart'] = async ({ req, res, config }) => { } else if (cartId && anonymousCartToken) { const { data: { cart: rawAnonymousCart }, - } = await config.fetch(getAnomymousCartQuery, { + } = await config.fetch(getAnonymousCartQuery, { variables: { cartId, cartToken: anonymousCartToken, diff --git a/framework/reactioncommerce/codegen.json b/framework/reactioncommerce/codegen.json index 56167f954..5679e67fd 100644 --- a/framework/reactioncommerce/codegen.json +++ b/framework/reactioncommerce/codegen.json @@ -6,7 +6,10 @@ }, "generates": { "./framework/reactioncommerce/schema.d.ts": { - "plugins": ["typescript", "typescript-operations"] + "plugins": ["typescript", "typescript-operations"], + "config": { + "avoidOptionals": true + } }, "./framework/reactioncommerce/schema.graphql": { "plugins": ["schema-ast"] diff --git a/framework/reactioncommerce/utils/normalize.ts b/framework/reactioncommerce/utils/normalize.ts index c97eb6185..982444f08 100644 --- a/framework/reactioncommerce/utils/normalize.ts +++ b/framework/reactioncommerce/utils/normalize.ts @@ -8,15 +8,15 @@ import { CatalogItemProduct, CatalogProduct, ImageInfo, - Maybe, + CartItem, } from '../schema' import type {Cart, LineItem} from '../types' -const normalizeProductImages = (images: Maybe[], name: string) => +const normalizeProductImages = (images: ImageInfo[], name: string) => images.map((image) => ({ url: image?.URLs?.original || image?.URLs?.medium || '', - alt: name, + alt: name })) const normalizeProductOption = (variant: CatalogProductVariant) => { @@ -38,7 +38,8 @@ function colorizeProductOptionValue(value: ProductOptionValues, displayName: str return value; } -const normalizeProductVariants = (variants: CatalogProductVariant[]): ProductVariant[] => { +const normalizeProductVariants = (variants: Array): ProductVariant[] => { + console.log(variants); return variants.reduce((productVariants: ProductVariant[], variant: CatalogProductVariant) => { if (variantHasOptions(variant)) { @@ -141,14 +142,14 @@ export function normalizeProduct(productNode: CatalogItemProduct): Product { slug: slug?.replace(/^\/+|\/+$/g, '') ?? '', path: slug ?? '', sku: sku ?? '', - images: media?.length ? normalizeProductImages(media, title ?? '') : [], + images: media?.length ? normalizeProductImages(media, title ?? '') : [], vendor: product.vendor, price: { value: pricing[0]?.minPrice ?? 0, currencyCode: pricing[0]?.currency.code, }, - variants: variants !== null ? normalizeProductVariants(variants) : [], - options: variants !== null ? groupProductOptionsByAttributeLabel(variants) : [] + variants: !!variants ? normalizeProductVariants(variants) : [], + options: !!variants ? groupProductOptionsByAttributeLabel(variants) : [] } } @@ -172,20 +173,18 @@ export function normalizeCart(cart: ReactionCart): Cart { function normalizeLineItem(cartItem: CartItemEdge): LineItem { const { - node: { - _id, - compareAtPrice, - imageURLs, - title, - productConfiguration, - priceWhenAdded, - optionTitle, - variantTitle, - quantity, - } - } = cartItem + _id, + compareAtPrice, + imageURLs, + title, + productConfiguration, + priceWhenAdded, + optionTitle, + variantTitle, + quantity + } = cartItem.node - console.log('imageURLs', imageURLs) + console.log('imageURLs', cartItem) return { id: _id, variantId: String(productConfiguration?.productVariantId), @@ -197,11 +196,12 @@ function normalizeLineItem(cartItem: CartItemEdge): LineItem { sku: String(productConfiguration?.productVariantId), name: String(optionTitle || variantTitle), image: { - url: imageURLs?.thumbnail ?? '/product-img-placeholder.svg', + url: imageURLs?.thumbnail ?? '/product-img-placeholder.svg', }, requiresShipping: true, price: priceWhenAdded?.amount, - listPrice: compareAtPrice?.amount, + listPrice: compareAtPrice?.amount ?? 0, + options: [] }, path: '', discounts: [], From 081a9a54a7eb30fe35daa47cc8323553b5718706 Mon Sep 17 00:00:00 2001 From: tedraykov Date: Tue, 25 May 2021 12:57:11 +0300 Subject: [PATCH 3/3] Trailing commas, null checks, formatting Added trailing commas object literals in normalize util. Added null checks for product and cart item normalization methods. Formatted the code with prettier config. Separated the nested functions in groupProductOptionsByAttributeLabel --- assets/base.css | 1 - assets/components.css | 2 +- framework/commerce/types.ts | 4 +- framework/reactioncommerce/utils/normalize.ts | 242 +++++++++++------- 4 files changed, 151 insertions(+), 98 deletions(-) diff --git a/assets/base.css b/assets/base.css index dfdaf1475..e63ea1aa4 100644 --- a/assets/base.css +++ b/assets/base.css @@ -127,4 +127,3 @@ a { opacity: 1; } } - diff --git a/assets/components.css b/assets/components.css index 8c4c5a357..ebebcc238 100644 --- a/assets/components.css +++ b/assets/components.css @@ -1,3 +1,3 @@ .fit { min-height: calc(100vh - 88px); -} \ No newline at end of file +} diff --git a/framework/commerce/types.ts b/framework/commerce/types.ts index 7bf44e774..b36a3c26c 100644 --- a/framework/commerce/types.ts +++ b/framework/commerce/types.ts @@ -111,8 +111,8 @@ export type CartItemBody = { productId?: string quantity?: number pricing?: { - amount: number, - currencyCode: string, + amount: number + currencyCode: string } } diff --git a/framework/reactioncommerce/utils/normalize.ts b/framework/reactioncommerce/utils/normalize.ts index 982444f08..8aec1717e 100644 --- a/framework/reactioncommerce/utils/normalize.ts +++ b/framework/reactioncommerce/utils/normalize.ts @@ -1,4 +1,10 @@ -import {Product, Customer, ProductVariant, ProductOption, ProductOptionValues} from '@commerce/types' +import { + Product, + Customer, + ProductVariant, + ProductOption, + ProductOptionValues, +} from '@commerce/types' import { Account, @@ -11,12 +17,12 @@ import { CartItem, } from '../schema' -import type {Cart, LineItem} from '../types' +import type { Cart, LineItem } from '../types' const normalizeProductImages = (images: ImageInfo[], name: string) => images.map((image) => ({ url: image?.URLs?.original || image?.URLs?.medium || '', - alt: name + alt: name, })) const normalizeProductOption = (variant: CatalogProductVariant) => { @@ -24,104 +30,139 @@ const normalizeProductOption = (variant: CatalogProductVariant) => { __typename: 'MultipleChoiceOption', id: variant._id, displayName: variant.attributeLabel, - values: variant.optionTitle ? [{label: variant.optionTitle}] : [] + values: variant.optionTitle ? [{ label: variant.optionTitle }] : [], } - option.values = option.values.map(value => colorizeProductOptionValue(value, option.displayName)) + option.values = option.values.map((value) => + colorizeProductOptionValue(value, option.displayName) + ) - return option; + return option } -function colorizeProductOptionValue(value: ProductOptionValues, displayName: string): ProductOptionValues { +function colorizeProductOptionValue( + value: ProductOptionValues, + displayName: string +): ProductOptionValues { if (displayName.toLowerCase() === 'color') { value.hexColors = [value.label] } - return value; + return value } -const normalizeProductVariants = (variants: Array): ProductVariant[] => { - console.log(variants); - return variants.reduce((productVariants: ProductVariant[], variant: CatalogProductVariant) => { +const normalizeProductVariants = ( + variants: Array +): ProductVariant[] => { + return variants.reduce( + (productVariants: ProductVariant[], variant: CatalogProductVariant) => { + if (variantHasOptions(variant)) { + productVariants.push(...flatVariantOptions(variant)) + return productVariants + } - if (variantHasOptions(variant)) { - productVariants.push(...flatVariantOptions(variant)) - return productVariants - } + const { sku, title, pricing = [], variantId } = variant ?? {} + const variantPrice = pricing[0]?.price ?? pricing[0]?.minPrice ?? 0 - const {sku, title, pricing = [], variantId} = variant ?? {} - const variantPrice = pricing[0]?.price ?? pricing[0]?.minPrice ?? 0 - - productVariants.push({ - id: variantId ?? '', - name: title, - sku: sku ?? variantId, - price: variantPrice, - listPrice: pricing[0]?.compareAtPrice?.amount ?? variantPrice, - requiresShipping: true, - options: [normalizeProductOption(variant)] - }); - - return productVariants; - }, []) -} - -function groupProductOptionsByAttributeLabel(variants: CatalogProductVariant[]): ProductOption[] { - return variants.reduce((groupedOptions: ProductOption[], currentVariant: CatalogProductVariant) => { - - groupedOptions = mergeVariantOptionsWithExistingOptions(groupedOptions, currentVariant); - - if (variantHasOptions(currentVariant)) { - (currentVariant.options).forEach(variantOption => { - groupedOptions = mergeVariantOptionsWithExistingOptions(groupedOptions, variantOption) + productVariants.push({ + id: variantId ?? '', + name: title, + sku: sku ?? variantId, + price: variantPrice, + listPrice: pricing[0]?.compareAtPrice?.amount ?? variantPrice, + requiresShipping: true, + options: [normalizeProductOption(variant)], }) - } - return groupedOptions - }, []) + return productVariants + }, + [] + ) +} - function mergeVariantOptionsWithExistingOptions( - groupedOptions: ProductOption[], - currentVariant: CatalogProductVariant): ProductOption[] { +function groupProductOptionsByAttributeLabel( + variants: CatalogProductVariant[] +): ProductOption[] { + return variants.reduce( + ( + groupedOptions: ProductOption[], + currentVariant: CatalogProductVariant + ) => { + groupedOptions = mergeVariantOptionsWithExistingOptions( + groupedOptions, + currentVariant + ) - const matchingOptionIndex = findCurrentVariantOptionsInGroupedOptions(groupedOptions, currentVariant) + if (variantHasOptions(currentVariant)) { + (currentVariant.options).forEach( + (variantOption) => { + groupedOptions = mergeVariantOptionsWithExistingOptions( + groupedOptions, + variantOption + ) + } + ) + } - return matchingOptionIndex !== -1 ? - mergeWithExistingOptions(groupedOptions, currentVariant, matchingOptionIndex) : - addNewProductOption(groupedOptions, currentVariant) - } + return groupedOptions + }, + [] + ) +} - function findCurrentVariantOptionsInGroupedOptions( - groupedOptions: ProductOption[], - currentVariant: CatalogProductVariant): number { +function mergeVariantOptionsWithExistingOptions( + groupedOptions: ProductOption[], + currentVariant: CatalogProductVariant +): ProductOption[] { + const matchingOptionIndex = findCurrentVariantOptionsInGroupedOptions( + groupedOptions, + currentVariant + ) - return groupedOptions.findIndex(option => - (option.displayName.toLowerCase() === currentVariant.attributeLabel.toLowerCase()) - ); - } + return matchingOptionIndex !== -1 + ? mergeWithExistingOptions( + groupedOptions, + currentVariant, + matchingOptionIndex + ) + : addNewProductOption(groupedOptions, currentVariant) +} - function mergeWithExistingOptions( - groupedOptions: ProductOption[], - currentVariant: CatalogProductVariant, - matchingOptionIndex: number) { +function findCurrentVariantOptionsInGroupedOptions( + groupedOptions: ProductOption[], + currentVariant: CatalogProductVariant +): number { + return groupedOptions.findIndex( + (option) => + option.displayName.toLowerCase() === + currentVariant.attributeLabel.toLowerCase() + ) +} - const currentVariantOption = normalizeProductOption(currentVariant); - groupedOptions[matchingOptionIndex].values = [ - ...groupedOptions[matchingOptionIndex].values, - ...currentVariantOption.values - ] +function mergeWithExistingOptions( + groupedOptions: ProductOption[], + currentVariant: CatalogProductVariant, + matchingOptionIndex: number +) { + const currentVariantOption = normalizeProductOption(currentVariant) + groupedOptions[matchingOptionIndex].values = [ + ...groupedOptions[matchingOptionIndex].values, + ...currentVariantOption.values, + ] - return groupedOptions; - } + return groupedOptions +} - function addNewProductOption(groupedOptions: ProductOption[], currentVariant: CatalogProductVariant) { - return [ - ...groupedOptions, - normalizeProductOption(currentVariant), - ]; - } +function addNewProductOption( + groupedOptions: ProductOption[], + currentVariant: CatalogProductVariant +) { + return [...groupedOptions, normalizeProductOption(currentVariant)] } export function normalizeProduct(productNode: CatalogItemProduct): Product { - const product = productNode.product as CatalogProduct + const product = productNode.product + if (!product) { + return {} + } const { _id, @@ -132,8 +173,8 @@ export function normalizeProduct(productNode: CatalogItemProduct): Product { sku, media, pricing, - variants - } = product + variants, + } = product return { id: productId ?? _id, @@ -142,14 +183,20 @@ export function normalizeProduct(productNode: CatalogItemProduct): Product { slug: slug?.replace(/^\/+|\/+$/g, '') ?? '', path: slug ?? '', sku: sku ?? '', - images: media?.length ? normalizeProductImages(media, title ?? '') : [], + images: media?.length + ? normalizeProductImages(media, title ?? '') + : [], vendor: product.vendor, price: { value: pricing[0]?.minPrice ?? 0, currencyCode: pricing[0]?.currency.code, }, - variants: !!variants ? normalizeProductVariants(variants) : [], - options: !!variants ? groupProductOptionsByAttributeLabel(variants) : [] + variants: !!variants + ? normalizeProductVariants(variants) + : [], + options: !!variants + ? groupProductOptionsByAttributeLabel(variants) + : [], } } @@ -163,7 +210,10 @@ export function normalizeCart(cart: ReactionCart): Cart { code: cart.checkout?.summary?.total?.currency.code ?? '', }, taxesIncluded: false, - lineItems: cart.items?.edges?.map(cartItem => normalizeLineItem(cartItem)) ?? [], + lineItems: + cart.items?.edges?.map((cartItem) => + normalizeLineItem(cartItem) + ) ?? [], lineItemsSubtotalPrice: +(cart.checkout?.summary?.itemTotal?.amount ?? 0), subtotalPrice: +(cart.checkout?.summary?.itemTotal?.amount ?? 0), totalPrice: cart.checkout?.summary?.total?.amount ?? 0, @@ -171,7 +221,13 @@ export function normalizeCart(cart: ReactionCart): Cart { } } -function normalizeLineItem(cartItem: CartItemEdge): LineItem { +function normalizeLineItem(cartItemEdge: CartItemEdge): LineItem { + const cartItem = cartItemEdge.node + + if (!cartItem) { + return {} + } + const { _id, compareAtPrice, @@ -181,10 +237,9 @@ function normalizeLineItem(cartItem: CartItemEdge): LineItem { priceWhenAdded, optionTitle, variantTitle, - quantity - } = cartItem.node + quantity, + } = cartItem - console.log('imageURLs', cartItem) return { id: _id, variantId: String(productConfiguration?.productVariantId), @@ -201,7 +256,7 @@ function normalizeLineItem(cartItem: CartItemEdge): LineItem { requiresShipping: true, price: priceWhenAdded?.amount, listPrice: compareAtPrice?.amount ?? 0, - options: [] + options: [], }, path: '', discounts: [], @@ -226,15 +281,14 @@ export function normalizeCustomer(viewer: Account): Customer { } function flatVariantOptions(variant: CatalogProductVariant): ProductVariant[] { - const variantOptions = variant.options; + const variantOptions = variant.options - return normalizeProductVariants(variantOptions) - .map(variantOption => { - variantOption.options.push(normalizeProductOption(variant)) - return variantOption - }); + return normalizeProductVariants(variantOptions).map((variantOption) => { + variantOption.options.push(normalizeProductOption(variant)) + return variantOption + }) } function variantHasOptions(variant: CatalogProductVariant) { - return !!variant.options && variant.options.length != 0; + return !!variant.options && variant.options.length != 0 }