Implement product attributes (variants)

This commit is contained in:
tedraykov 2021-05-21 06:09:08 +03:00
parent 3112c7fc80
commit fb112385e6
3 changed files with 126 additions and 108 deletions

View File

@ -49,10 +49,10 @@ const ProductView: FC<Props> = ({ product }) => {
await addItem({ await addItem({
productId: String(product.id), productId: String(product.id),
variantId: String(selectedVariant.sku), variantId: String(selectedVariant.id),
pricing: { pricing: {
amount: selectedVariant.price, amount: selectedVariant.price,
currencyCode: product.price.currencyCode, currencyCode: product.price.currencyCode ?? 'USD',
}, },
}) })
openSidebar() openSidebar()

View File

@ -1,6 +1,6 @@
import { useCallback } from 'react' import { useCallback } from 'react'
import type { MutationHook } from '@commerce/utils/types' 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 useCustomer from '../customer/use-customer'
import authenticateMutation from '../utils/mutations/authenticate' import authenticateMutation from '../utils/mutations/authenticate'
import { import {

View File

@ -1,9 +1,8 @@
import { Product, Customer } from '@commerce/types' import {Product, Customer, ProductVariant, ProductOption, ProductOptionValues} from '@commerce/types'
import { import {
Account, Account,
Cart as ReactionCart, Cart as ReactionCart,
ProductPricingInfo,
CatalogProductVariant, CatalogProductVariant,
CartItemEdge, CartItemEdge,
CatalogItemProduct, CatalogItemProduct,
@ -12,20 +11,7 @@ import {
Maybe, Maybe,
} from '../schema' } from '../schema'
import type { Cart, LineItem } from '../types' 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<ImageInfo>[], name: string) => const normalizeProductImages = (images: Maybe<ImageInfo>[], name: string) =>
images.map((image) => ({ images.map((image) => ({
@ -33,83 +19,104 @@ const normalizeProductImages = (images: Maybe<ImageInfo>[], name: string) =>
alt: name, alt: name,
})) }))
const normalizeProductOption = ({ id, displayName, values }: ProductOption) => { const normalizeProductOption = (variant: CatalogProductVariant) => {
return { const option = <ProductOption>{
__typename: 'MultipleChoiceOption', __typename: 'MultipleChoiceOption',
id, id: variant._id,
displayName, displayName: variant.attributeLabel,
values: values.map((value) => { values: variant.optionTitle ? [{label: variant.optionTitle}] : []
let output: any = {
label: value,
}
if (displayName.toLowerCase() === 'color') {
output = {
...output,
hexColors: [value],
}
}
return output
}),
} }
option.values = option.values.map(value => colorizeProductOptionValue(value, option.displayName))
return option;
} }
const normalizeProductVariants = (variants: CatalogProductVariant[]) => { function colorizeProductOptionValue(value: ProductOptionValues, displayName: string): ProductOptionValues {
return variants.map((variant) => { if (displayName.toLowerCase() === 'color') {
const { _id, options, sku, title, pricing = [], variantId } = variant ?? {} 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 const variantPrice = pricing[0]?.price ?? pricing[0]?.minPrice ?? 0
return { productVariants.push(<ProductVariant>{
id: _id ?? '', id: variantId ?? '',
name: title, name: title,
sku: sku ?? variantId, sku: sku ?? variantId,
price: variantPrice, price: variantPrice,
listPrice: pricing[0]?.compareAtPrice?.amount ?? variantPrice, listPrice: pricing[0]?.compareAtPrice?.amount ?? variantPrice,
requiresShipping: true, requiresShipping: true,
options: options?.length options: [normalizeProductOption(variant)]
? options.map((option) => { });
return normalizeProductOption({
id: option?._id ?? '', return productVariants;
displayName: option?.attributeLabel ?? '', }, [])
values: [option?.optionTitle],
})
})
: [],
}
})
} }
export function groupProductOptionsByAttributeLabel( function groupProductOptionsByAttributeLabel(variants: CatalogProductVariant[]): ProductOption[] {
options: CatalogProductVariant[] return variants.reduce((groupedOptions: ProductOption[], currentVariant: CatalogProductVariant) => {
) {
return options.reduce((groupedOptions, currentOption) => {
const attributeLabelIndex = groupedOptions.findIndex((option) => {
return (
option.displayName.toLowerCase() ===
currentOption?.attributeLabel.toLowerCase()
)
})
if (attributeLabelIndex !== -1) { groupedOptions = mergeVariantOptionsWithExistingOptions(groupedOptions, currentVariant);
groupedOptions[attributeLabelIndex].values = [
...groupedOptions[attributeLabelIndex].values, if (variantHasOptions(currentVariant)) {
{ (<CatalogProductVariant[]>currentVariant.options).forEach(variantOption => {
label: currentOption?.optionTitle ?? '', groupedOptions = mergeVariantOptionsWithExistingOptions(groupedOptions, variantOption)
hexColors: [currentOption?.optionTitle] ?? '', })
},
]
} else {
groupedOptions = [
...groupedOptions,
normalizeProductOption({
id: currentOption?._id ?? '',
displayName: currentOption?.attributeLabel ?? '',
values: [currentOption?.optionTitle ?? ''],
}),
]
} }
return groupedOptions 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 { export function normalizeProduct(productNode: CatalogItemProduct): Product {
@ -124,9 +131,7 @@ export function normalizeProduct(productNode: CatalogItemProduct): Product {
sku, sku,
media, media,
pricing, pricing,
vendor, variants
variants,
...rest
} = product } = product
return { return {
@ -142,11 +147,8 @@ export function normalizeProduct(productNode: CatalogItemProduct): Product {
value: pricing[0]?.minPrice ?? 0, value: pricing[0]?.minPrice ?? 0,
currencyCode: pricing[0]?.currency.code, currencyCode: pricing[0]?.currency.code,
}, },
variants: variants?.length ? normalizeProductVariants(variants) : [], variants: variants !== null ? normalizeProductVariants(<CatalogProductVariant[]>variants) : [],
options: variants?.length options: variants !== null ? groupProductOptionsByAttributeLabel(<CatalogProductVariant[]>variants) : []
? groupProductOptionsByAttributeLabel(variants)
: [],
...rest,
} }
} }
@ -157,30 +159,32 @@ export function normalizeCart(cart: ReactionCart): Cart {
email: '', email: '',
createdAt: cart.createdAt, createdAt: cart.createdAt,
currency: { currency: {
code: cart.checkout?.summary?.total?.currency.code, code: cart.checkout?.summary?.total?.currency.code ?? '',
}, },
taxesIncluded: false, taxesIncluded: false,
lineItems: cart.items?.edges?.map(normalizeLineItem), lineItems: cart.items?.edges?.map(cartItem => normalizeLineItem(<CartItemEdge>cartItem)) ?? [],
lineItemsSubtotalPrice: +cart.checkout?.summary?.itemTotal?.amount, lineItemsSubtotalPrice: +(cart.checkout?.summary?.itemTotal?.amount ?? 0),
subtotalPrice: +cart.checkout?.summary?.itemTotal?.amount, subtotalPrice: +(cart.checkout?.summary?.itemTotal?.amount ?? 0),
totalPrice: cart.checkout?.summary?.total?.amount, totalPrice: cart.checkout?.summary?.total?.amount ?? 0,
discounts: [], discounts: [],
} }
} }
function normalizeLineItem({ function normalizeLineItem(cartItem: CartItemEdge): LineItem {
node: { const {
_id, node: {
compareAtPrice, _id,
imageURLs, compareAtPrice,
title, imageURLs,
productConfiguration, title,
priceWhenAdded, productConfiguration,
optionTitle, priceWhenAdded,
variantTitle, optionTitle,
quantity, variantTitle,
}, quantity,
}: CartItemEdge): LineItem { }
} = cartItem
console.log('imageURLs', imageURLs) console.log('imageURLs', imageURLs)
return { return {
id: _id, id: _id,
@ -193,7 +197,7 @@ function normalizeLineItem({
sku: String(productConfiguration?.productVariantId), sku: String(productConfiguration?.productVariantId),
name: String(optionTitle || variantTitle), name: String(optionTitle || variantTitle),
image: { image: {
url: imageURLs?.original ?? '/product-img-placeholder.svg', url: imageURLs?.thumbnail ?? '/product-img-placeholder.svg',
}, },
requiresShipping: true, requiresShipping: true,
price: priceWhenAdded?.amount, price: priceWhenAdded?.amount,
@ -211,12 +215,26 @@ function normalizeLineItem({
export function normalizeCustomer(viewer: Account): Customer { export function normalizeCustomer(viewer: Account): Customer {
if (!viewer) { if (!viewer) {
return {} return <Customer>{}
} }
return { return <Customer>{
firstName: viewer.firstName ?? '', firstName: viewer.firstName ?? '',
lastName: viewer.lastName ?? '', lastName: viewer.lastName ?? '',
email: viewer.primaryEmailAddress, email: viewer.primaryEmailAddress,
} }
} }
function flatVariantOptions(variant: CatalogProductVariant): ProductVariant[] {
const variantOptions = <CatalogProductVariant[]>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;
}