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/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/commerce/types.ts b/framework/commerce/types.ts index 86361fd9f..b36a3c26c 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/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/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 296832e6e..8aec1717e 100644 --- a/framework/reactioncommerce/utils/normalize.ts +++ b/framework/reactioncommerce/utils/normalize.ts @@ -1,119 +1,168 @@ -import { Product, Customer } from '@commerce/types' +import { + Product, + Customer, + ProductVariant, + ProductOption, + ProductOptionValues, +} from '@commerce/types' import { Account, Cart as ReactionCart, - ProductPricingInfo, CatalogProductVariant, CartItemEdge, CatalogItemProduct, CatalogProduct, ImageInfo, - Maybe, + CartItem, } from '../schema' import type { Cart, LineItem } from '../types' -type ProductOption = { - __typename?: string - id: string - displayName: string - values: any[] -} - -const money = ({ displayPrice }: ProductPricingInfo) => { - return { - displayPrice, - } -} - -const normalizeProductImages = (images: Maybe[], name: string) => +const normalizeProductImages = (images: ImageInfo[], name: string) => images.map((image) => ({ url: image?.URLs?.original || image?.URLs?.medium || '', 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 ?? {} - const variantPrice = pricing[0]?.price ?? pricing[0]?.minPrice ?? 0 - - return { - id: _id ?? '', - 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], - }) - }) - : [], - } - }) +function colorizeProductOptionValue( + value: ProductOptionValues, + displayName: string +): ProductOptionValues { + if (displayName.toLowerCase() === 'color') { + value.hexColors = [value.label] + } + return value } -export function groupProductOptionsByAttributeLabel( - options: CatalogProductVariant[] -) { - return options.reduce((groupedOptions, currentOption) => { - const attributeLabelIndex = groupedOptions.findIndex((option) => { - return ( - option.displayName.toLowerCase() === - currentOption?.attributeLabel.toLowerCase() +const normalizeProductVariants = ( + variants: Array +): 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 + + 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 (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 ?? ''], - }), - ] - } + if (variantHasOptions(currentVariant)) { + (currentVariant.options).forEach( + (variantOption) => { + groupedOptions = mergeVariantOptionsWithExistingOptions( + groupedOptions, + variantOption + ) + } + ) + } - return groupedOptions - }, [] as ProductOption[]) + return groupedOptions + }, + [] + ) +} + +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 { - const product = productNode.product as CatalogProduct + const product = productNode.product + if (!product) { + return {} + } const { _id, @@ -124,10 +173,8 @@ export function normalizeProduct(productNode: CatalogItemProduct): Product { sku, media, pricing, - vendor, variants, - ...rest - } = product + } = product return { id: productId ?? _id, @@ -136,17 +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?.length ? normalizeProductVariants(variants) : [], - options: variants?.length - ? groupProductOptionsByAttributeLabel(variants) + variants: !!variants + ? normalizeProductVariants(variants) + : [], + options: !!variants + ? groupProductOptionsByAttributeLabel(variants) : [], - ...rest, } } @@ -157,19 +207,28 @@ 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: { +function normalizeLineItem(cartItemEdge: CartItemEdge): LineItem { + const cartItem = cartItemEdge.node + + if (!cartItem) { + return {} + } + + const { _id, compareAtPrice, imageURLs, @@ -179,9 +238,8 @@ function normalizeLineItem({ optionTitle, variantTitle, quantity, - }, -}: CartItemEdge): LineItem { - console.log('imageURLs', imageURLs) + } = cartItem + return { id: _id, variantId: String(productConfiguration?.productVariantId), @@ -193,11 +251,12 @@ 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, - listPrice: compareAtPrice?.amount, + listPrice: compareAtPrice?.amount ?? 0, + options: [], }, path: '', discounts: [], @@ -211,12 +270,25 @@ 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 +}