diff --git a/.changeset/tangy-facts-tell.md b/.changeset/tangy-facts-tell.md new file mode 100644 index 0000000000..de09bab188 --- /dev/null +++ b/.changeset/tangy-facts-tell.md @@ -0,0 +1,5 @@ +--- +"@bigcommerce/catalyst-core": minor +--- + +Adding picklist (bundle) support in cart display. Child items from picklist products are displayed nested under their parent product, matching Stencil behavior. diff --git a/core/app/[locale]/(default)/cart/page-data.ts b/core/app/[locale]/(default)/cart/page-data.ts index 0143a3de6d..2c541f69ab 100644 --- a/core/app/[locale]/(default)/cart/page-data.ts +++ b/core/app/[locale]/(default)/cart/page-data.ts @@ -16,6 +16,7 @@ export const PhysicalItemFragment = graphql(` url: urlTemplate(lossy: true) } entityId + parentEntityId quantity productEntityId variantEntityId @@ -76,6 +77,7 @@ export const DigitalItemFragment = graphql(` url: urlTemplate(lossy: true) } entityId + parentEntityId quantity productEntityId variantEntityId diff --git a/core/app/[locale]/(default)/cart/page.tsx b/core/app/[locale]/(default)/cart/page.tsx index 41f6c3a9ed..93d889b55e 100644 --- a/core/app/[locale]/(default)/cart/page.tsx +++ b/core/app/[locale]/(default)/cart/page.tsx @@ -43,7 +43,21 @@ const getAnalyticsData = async (cartId: string) => { const lineItems = [...cart.lineItems.physicalItems, ...cart.lineItems.digitalItems]; - return lineItems.map((item) => { + // Filter out picklist children to avoid double-counting in analytics + const itemMap = new Map(lineItems.map((item) => [item.entityId, item])); + const parentItems = lineItems.filter((item) => { + // Exclude items that are children (have parentEntityId and parent exists) + if ( + 'parentEntityId' in item && + item.parentEntityId != null && + itemMap.has(item.parentEntityId) + ) { + return false; + } + return true; + }); + + return parentItems.map((item) => { return { entityId: item.entityId, id: item.productEntityId, @@ -100,11 +114,44 @@ export default async function Cart({ params }: Props) { ...cart.lineItems.digitalItems, ]; - const formattedLineItems = lineItems.map((item) => { + // Group picklist children under their parents + // Create a map of all items by their entityId + const itemMap = new Map(lineItems.map((item) => [item.entityId, item])); + + // Separate parents from children + // Gift certificates are always parents (they don't have parentEntityId) + const parentItems: typeof lineItems = []; + for (const item of lineItems) { + // If this is a child item (has parentEntityId and parent exists), skip adding it as a root + if ( + 'parentEntityId' in item && + item.parentEntityId != null && + itemMap.has(item.parentEntityId) + ) { + continue; + } + // If this is a parent item, add it + parentItems.push(item); + } + + // Format parent items and attach children + const formattedLineItems = parentItems.map((item) => { + // Find children for this parent (only physical/digital items can have children) + const children = + item.__typename !== 'CartGiftCertificate' + ? lineItems.filter( + (child) => + child.__typename !== 'CartGiftCertificate' && + 'parentEntityId' in child && + child.parentEntityId === item.entityId && + child.entityId !== item.entityId, + ) + : []; + if (item.__typename === 'CartGiftCertificate') { return { typename: item.__typename, - id: item.entityId, + id: item.entityId.toString(), title: item.name, subtitle: `${t('GiftCertificate.to')}: ${item.recipient.name} (${item.recipient.email})${item.message ? `, ${t('GiftCertificate.message')}: ${item.message}` : ''}`, quantity: 1, @@ -122,9 +169,62 @@ export default async function Cart({ params }: Props) { }; } + // Format children for physical/digital items + // Children can only be physical or digital items (not gift certificates) + const formattedChildren = + children.length > 0 + ? children + .filter( + (child): child is + | typeof cart.lineItems.physicalItems[number] + | typeof cart.lineItems.digitalItems[number] => + child.__typename === 'CartPhysicalItem' || + child.__typename === 'CartDigitalItem', + ) + .map((child) => ({ + typename: child.__typename, + id: child.entityId.toString(), + quantity: child.quantity, + price: format.number(child.listPrice.value, { + style: 'currency', + currency: child.listPrice.currencyCode, + }), + subtitle: child.selectedOptions + .map((option) => { + switch (option.__typename) { + case 'CartSelectedMultipleChoiceOption': + case 'CartSelectedCheckboxOption': + return `${option.name}: ${option.value}`; + + case 'CartSelectedNumberFieldOption': + return `${option.name}: ${option.number}`; + + case 'CartSelectedMultiLineTextFieldOption': + case 'CartSelectedTextFieldOption': + return `${option.name}: ${option.text}`; + + case 'CartSelectedDateFieldOption': + return `${option.name}: ${format.dateTime(new Date(option.date.utc))}`; + + default: + return ''; + } + }) + .join(', '), + title: child.name, + image: child.image?.url + ? { src: child.image.url, alt: child.name } + : undefined, + href: new URL(child.url).pathname, + selectedOptions: child.selectedOptions, + productEntityId: child.productEntityId, + variantEntityId: child.variantEntityId, + })) + : []; + return { typename: item.__typename, - id: item.entityId, + id: item.entityId.toString(), quantity: item.quantity, price: format.number(item.listPrice.value, { style: 'currency', @@ -158,6 +258,8 @@ export default async function Cart({ params }: Props) { selectedOptions: item.selectedOptions, productEntityId: item.productEntityId, variantEntityId: item.variantEntityId, + children: + formattedChildren.length > 0 ? formattedChildren : undefined, }; }); @@ -347,7 +449,7 @@ export default async function Cart({ params }: Props) { diff --git a/core/vibes/soul/sections/cart/client.tsx b/core/vibes/soul/sections/cart/client.tsx index 10e7fd6966..8a7ce057c8 100644 --- a/core/vibes/soul/sections/cart/client.tsx +++ b/core/vibes/soul/sections/cart/client.tsx @@ -42,6 +42,7 @@ export interface CartLineItem { quantity: number; price: string; href?: string; + children?: CartLineItem[]; } export interface CartGiftCertificateLineItem extends CartLineItem { @@ -453,6 +454,25 @@ export function CartClient({ {lineItem.subtitle} + {lineItem.children && lineItem.children.length > 0 && ( + + )}