mirror of
https://github.com/vercel/commerce.git
synced 2025-05-18 15:36:58 +00:00
Implement product attributes (variants)
This commit is contained in:
parent
3112c7fc80
commit
fb112385e6
@ -49,10 +49,10 @@ const ProductView: FC<Props> = ({ 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()
|
||||
|
@ -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 {
|
||||
|
@ -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<ImageInfo>[], name: string) =>
|
||||
images.map((image) => ({
|
||||
@ -33,83 +19,104 @@ const normalizeProductImages = (images: Maybe<ImageInfo>[], name: string) =>
|
||||
alt: name,
|
||||
}))
|
||||
|
||||
const normalizeProductOption = ({ id, displayName, values }: ProductOption) => {
|
||||
return {
|
||||
const normalizeProductOption = (variant: CatalogProductVariant) => {
|
||||
const option = <ProductOption>{
|
||||
__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(<ProductVariant>{
|
||||
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)) {
|
||||
(<CatalogProductVariant[]>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(<CatalogProductVariant[]>variants) : [],
|
||||
options: variants !== null ? groupProductOptionsByAttributeLabel(<CatalogProductVariant[]>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(<CartItemEdge>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 <Customer>{}
|
||||
}
|
||||
|
||||
return {
|
||||
return <Customer>{
|
||||
firstName: viewer.firstName ?? '',
|
||||
lastName: viewer.lastName ?? '',
|
||||
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;
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user