Skip to content

Commit e75db37

Browse files
Fix partial shop variant availability (#1127)
1 parent c1c213d commit e75db37

4 files changed

Lines changed: 79 additions & 31 deletions

File tree

src/components/shop/ProductDrawer.tsx

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@ import * as React from 'react'
22
import { useQuery, useQueryClient } from '@tanstack/react-query'
33
import { twMerge } from 'tailwind-merge'
44
import { getProduct } from '~/utils/shop.functions'
5-
import type {
6-
ProductDetail,
7-
ProductDetailVariant,
5+
import {
6+
hasAvailableVariant,
7+
type ProductDetail,
8+
type ProductDetailVariant,
89
} from '~/utils/shopify-queries'
910
import { formatMoney } from '~/utils/shopify-format'
1011
import { resolveShopProductColor, shopColorContrast } from '~/utils/shop-color'
@@ -474,15 +475,16 @@ function DrawerContent({
474475
Select {option.name}
475476
</option>
476477
{option.values.map((value) => {
477-
const match = findMatchingVariant(
478-
variants,
479-
getCandidate(value),
480-
)
481478
return (
482479
<option
483480
key={value}
484481
value={value}
485-
disabled={!match?.availableForSale}
482+
disabled={
483+
!hasAvailableVariant(
484+
variants,
485+
getCandidate(value),
486+
)
487+
}
486488
>
487489
{value}
488490
</option>
@@ -505,11 +507,10 @@ function DrawerContent({
505507
<div className="flex flex-wrap gap-1.5">
506508
{option.values.map((value) => {
507509
const isSelected = selected[option.name] === value
508-
const match = findMatchingVariant(
510+
const isUnavailable = !hasAvailableVariant(
509511
variants,
510512
getCandidate(value),
511513
)
512-
const isUnavailable = !match?.availableForSale
513514
return (
514515
<ShopSize
515516
key={value}
@@ -545,11 +546,10 @@ function DrawerContent({
545546
<div className="flex flex-wrap gap-1.5">
546547
{option.values.map((value) => {
547548
const isSelected = selected[option.name] === value
548-
const match = findMatchingVariant(
549+
const isUnavailable = !hasAvailableVariant(
549550
variants,
550551
getCandidate(value),
551552
)
552-
const isUnavailable = !match?.availableForSale
553553
const hex = resolveShopProductColor(value)
554554
return (
555555
<ShopChip

src/routes/shop.products.$handle.tsx

Lines changed: 5 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
import { useAddToCart } from '~/hooks/useCart'
2020
import { getProduct } from '~/utils/shop.functions'
2121
import {
22+
hasAvailableVariant,
2223
type ProductDetail,
2324
type ProductDetailVariant,
2425
} from '~/utils/shopify-queries'
@@ -336,15 +337,13 @@ function VariantSelector({
336337
Select {option.name}
337338
</option>
338339
{option.values.map((value) => {
339-
const match = findAvailableVariant(
340-
variants,
341-
getCandidate(value),
342-
)
343340
return (
344341
<option
345342
key={value}
346343
value={value}
347-
disabled={!match?.availableForSale}
344+
disabled={
345+
!hasAvailableVariant(variants, getCandidate(value))
346+
}
348347
>
349348
{value}
350349
</option>
@@ -361,11 +360,10 @@ function VariantSelector({
361360
>
362361
{option.values.map((value) => {
363362
const isSelected = selected[option.name] === value
364-
const match = findAvailableVariant(
363+
const isUnavailable = !hasAvailableVariant(
365364
variants,
366365
getCandidate(value),
367366
)
368-
const isUnavailable = !match?.availableForSale
369367
const handleClick = () => handleChange(value)
370368
if (isSizeOption) {
371369
return (
@@ -519,18 +517,6 @@ function findMatchingVariant(
519517
)
520518
}
521519

522-
function findAvailableVariant(
523-
variants: Array<ProductDetailVariant>,
524-
selected: Record<string, string>,
525-
): ProductDetailVariant | undefined {
526-
return variants.find((variant) =>
527-
variant.selectedOptions.every(
528-
(option) =>
529-
!selected[option.name] || selected[option.name] === option.value,
530-
),
531-
)
532-
}
533-
534520
function ProductJsonLd({
535521
product,
536522
selectedVariant,

src/utils/shopify-queries.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,22 @@ export type ProductDetailVariant = Pick<
255255
image: Pick<StorefrontImage, 'url' | 'altText' | 'width' | 'height'> | null
256256
}
257257

258+
export function hasAvailableVariant(
259+
variants: Array<
260+
Pick<ProductDetailVariant, 'availableForSale' | 'selectedOptions'>
261+
>,
262+
selected: Record<string, string>,
263+
): boolean {
264+
return variants.some(
265+
(variant) =>
266+
variant.availableForSale &&
267+
variant.selectedOptions.every(
268+
(option) =>
269+
!selected[option.name] || selected[option.name] === option.value,
270+
),
271+
)
272+
}
273+
258274
export type ProductDetail = Pick<
259275
Product,
260276
'id' | 'handle' | 'title' | 'descriptionHtml'

tests/shopify-variant.test.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import assert from 'node:assert/strict'
2+
import { test } from 'node:test'
3+
import { hasAvailableVariant } from '../src/utils/shopify-queries'
4+
5+
const variants = [
6+
{
7+
availableForSale: false,
8+
selectedOptions: [
9+
{ name: 'Color', value: 'Black' },
10+
{ name: 'Size', value: 'Small' },
11+
],
12+
},
13+
{
14+
availableForSale: true,
15+
selectedOptions: [
16+
{ name: 'Color', value: 'Black' },
17+
{ name: 'Size', value: 'Large' },
18+
],
19+
},
20+
{
21+
availableForSale: false,
22+
selectedOptions: [
23+
{ name: 'Color', value: 'Blue' },
24+
{ name: 'Size', value: 'Large' },
25+
],
26+
},
27+
]
28+
29+
test('partial selections stay available when any matching variant is in stock', () => {
30+
assert.equal(hasAvailableVariant(variants, { Color: 'Black' }), true)
31+
})
32+
33+
test('complete selections only match the selected variant', () => {
34+
assert.equal(
35+
hasAvailableVariant(variants, { Color: 'Black', Size: 'Small' }),
36+
false,
37+
)
38+
assert.equal(
39+
hasAvailableVariant(variants, { Color: 'Black', Size: 'Large' }),
40+
true,
41+
)
42+
})
43+
44+
test('partial selections are unavailable when all matches are sold out', () => {
45+
assert.equal(hasAvailableVariant(variants, { Color: 'Blue' }), false)
46+
})

0 commit comments

Comments
 (0)