diff --git a/src/components/shop/ProductDrawer.tsx b/src/components/shop/ProductDrawer.tsx index e20e5467e..8e64bce23 100644 --- a/src/components/shop/ProductDrawer.tsx +++ b/src/components/shop/ProductDrawer.tsx @@ -2,9 +2,10 @@ import * as React from 'react' import { useQuery, useQueryClient } from '@tanstack/react-query' import { twMerge } from 'tailwind-merge' import { getProduct } from '~/utils/shop.functions' -import type { - ProductDetail, - ProductDetailVariant, +import { + hasAvailableVariant, + type ProductDetail, + type ProductDetailVariant, } from '~/utils/shopify-queries' import { formatMoney } from '~/utils/shopify-format' import { resolveShopProductColor, shopColorContrast } from '~/utils/shop-color' @@ -474,15 +475,16 @@ function DrawerContent({ Select {option.name} {option.values.map((value) => { - const match = findMatchingVariant( - variants, - getCandidate(value), - ) return ( @@ -505,11 +507,10 @@ function DrawerContent({
{option.values.map((value) => { const isSelected = selected[option.name] === value - const match = findMatchingVariant( + const isUnavailable = !hasAvailableVariant( variants, getCandidate(value), ) - const isUnavailable = !match?.availableForSale return ( {option.values.map((value) => { const isSelected = selected[option.name] === value - const match = findMatchingVariant( + const isUnavailable = !hasAvailableVariant( variants, getCandidate(value), ) - const isUnavailable = !match?.availableForSale const hex = resolveShopProductColor(value) return ( {option.values.map((value) => { - const match = findAvailableVariant( - variants, - getCandidate(value), - ) return ( @@ -361,11 +360,10 @@ function VariantSelector({ > {option.values.map((value) => { const isSelected = selected[option.name] === value - const match = findAvailableVariant( + const isUnavailable = !hasAvailableVariant( variants, getCandidate(value), ) - const isUnavailable = !match?.availableForSale const handleClick = () => handleChange(value) if (isSizeOption) { return ( @@ -519,18 +517,6 @@ function findMatchingVariant( ) } -function findAvailableVariant( - variants: Array, - selected: Record, -): ProductDetailVariant | undefined { - return variants.find((variant) => - variant.selectedOptions.every( - (option) => - !selected[option.name] || selected[option.name] === option.value, - ), - ) -} - function ProductJsonLd({ product, selectedVariant, diff --git a/src/utils/shopify-queries.ts b/src/utils/shopify-queries.ts index 844227da5..b7a4b4359 100644 --- a/src/utils/shopify-queries.ts +++ b/src/utils/shopify-queries.ts @@ -255,6 +255,22 @@ export type ProductDetailVariant = Pick< image: Pick | null } +export function hasAvailableVariant( + variants: Array< + Pick + >, + selected: Record, +): boolean { + return variants.some( + (variant) => + variant.availableForSale && + variant.selectedOptions.every( + (option) => + !selected[option.name] || selected[option.name] === option.value, + ), + ) +} + export type ProductDetail = Pick< Product, 'id' | 'handle' | 'title' | 'descriptionHtml' diff --git a/tests/shopify-variant.test.ts b/tests/shopify-variant.test.ts new file mode 100644 index 000000000..a94bae49a --- /dev/null +++ b/tests/shopify-variant.test.ts @@ -0,0 +1,46 @@ +import assert from 'node:assert/strict' +import { test } from 'node:test' +import { hasAvailableVariant } from '../src/utils/shopify-queries' + +const variants = [ + { + availableForSale: false, + selectedOptions: [ + { name: 'Color', value: 'Black' }, + { name: 'Size', value: 'Small' }, + ], + }, + { + availableForSale: true, + selectedOptions: [ + { name: 'Color', value: 'Black' }, + { name: 'Size', value: 'Large' }, + ], + }, + { + availableForSale: false, + selectedOptions: [ + { name: 'Color', value: 'Blue' }, + { name: 'Size', value: 'Large' }, + ], + }, +] + +test('partial selections stay available when any matching variant is in stock', () => { + assert.equal(hasAvailableVariant(variants, { Color: 'Black' }), true) +}) + +test('complete selections only match the selected variant', () => { + assert.equal( + hasAvailableVariant(variants, { Color: 'Black', Size: 'Small' }), + false, + ) + assert.equal( + hasAvailableVariant(variants, { Color: 'Black', Size: 'Large' }), + true, + ) +}) + +test('partial selections are unavailable when all matches are sold out', () => { + assert.equal(hasAvailableVariant(variants, { Color: 'Blue' }), false) +})