Skip to content

Commit d78bc85

Browse files
authored
feat: Add items stock/backorder messages to cart page (#2758)
* feat: Add items stock/backorder messages to cart page * feat: Add items stock/backorder messages to cart page - Apply UI comments * feat: Add items stock/backorder messages to cart page - Apply review comments
1 parent 254dbc5 commit d78bc85

5 files changed

Lines changed: 174 additions & 56 deletions

File tree

.changeset/tender-toys-heal.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
"@bigcommerce/catalyst-core": minor
3+
---
4+
5+
Add the following messages to each line item on cart page based on store inventory settings:
6+
- Fully/partially out-of-stock message if enabled on the store and the line item is currently out of stock
7+
- Ready-to-ship quantity if enabled on the store
8+
- Backordered quantity if enabled on the store
9+
10+
## Migration
11+
For existing Catalyst stores, to get the newly added feature, simply rebase the existing code with the new release code. The files to be rebased for this change to be applied are:
12+
- core/app/[locale]/(default)/cart/page-data.ts
13+
- core/app/[locale]/(default)/cart/page.tsx
14+
- core/messages/en.json
15+
- core/vibes/soul/sections/cart/client.tsx

core/app/[locale]/(default)/cart/page-data.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,12 @@ export const PhysicalItemFragment = graphql(`
5656
}
5757
}
5858
url
59+
stockPosition {
60+
backorderMessage
61+
quantityOnHand
62+
quantityBackordered
63+
quantityOutOfStock
64+
}
5965
}
6066
`);
6167

@@ -206,6 +212,13 @@ const CartPageQuery = graphql(
206212
query CartPageQuery($cartId: String, $currencyCode: currencyCode) {
207213
site {
208214
settings {
215+
inventory {
216+
defaultOutOfStockMessage
217+
showOutOfStockMessage
218+
showBackorderMessage
219+
showQuantityOnBackorder
220+
showQuantityOnHand
221+
}
209222
url {
210223
checkoutUrl
211224
}

core/app/[locale]/(default)/cart/page.tsx

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,47 @@ export default async function Cart({ params }: Props) {
125125
};
126126
}
127127

128+
let inventoryMessages;
129+
130+
if (item.__typename === 'CartPhysicalItem') {
131+
if (item.stockPosition?.quantityOutOfStock === item.quantity) {
132+
inventoryMessages = {
133+
outOfStockMessage: data.site.settings?.inventory?.showOutOfStockMessage
134+
? data.site.settings.inventory.defaultOutOfStockMessage
135+
: undefined,
136+
};
137+
} else {
138+
inventoryMessages = {
139+
quantityReadyToShipMessage:
140+
data.site.settings?.inventory?.showQuantityOnHand &&
141+
!!item.stockPosition?.quantityOnHand
142+
? t('quantityReadyToShip', {
143+
quantity: Number(item.stockPosition.quantityOnHand),
144+
})
145+
: undefined,
146+
quantityBackorderedMessage:
147+
data.site.settings?.inventory?.showQuantityOnBackorder &&
148+
!!item.stockPosition?.quantityBackordered
149+
? t('quantityOnBackorder', {
150+
quantity: Number(item.stockPosition.quantityBackordered),
151+
})
152+
: undefined,
153+
quantityOutOfStockMessage:
154+
data.site.settings?.inventory?.showOutOfStockMessage &&
155+
!!item.stockPosition?.quantityOutOfStock
156+
? t('partiallyAvailable', {
157+
quantity: item.quantity - Number(item.stockPosition.quantityOutOfStock),
158+
})
159+
: undefined,
160+
backorderMessage:
161+
data.site.settings?.inventory?.showBackorderMessage &&
162+
!!item.stockPosition?.quantityBackordered
163+
? (item.stockPosition.backorderMessage ?? undefined)
164+
: undefined,
165+
};
166+
}
167+
}
168+
128169
return {
129170
typename: item.__typename,
130171
id: item.entityId,
@@ -165,6 +206,7 @@ export default async function Cart({ params }: Props) {
165206
selectedOptions: item.selectedOptions,
166207
productEntityId: item.productEntityId,
167208
variantEntityId: item.variantEntityId,
209+
inventoryMessages,
168210
};
169211
});
170212

core/messages/en.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -370,6 +370,9 @@
370370
"cartUpdateInProgress": "You have a cart update in progress. Are you sure you want to leave this page? Your changes may be lost.",
371371
"originalPrice": "Original price was {price}.",
372372
"currentPrice": "Current price is {price}.",
373+
"quantityReadyToShip": "{quantity, number} ready to ship",
374+
"quantityOnBackorder": "{quantity, number} will be backordered",
375+
"partiallyAvailable": "Only {quantity, number} available",
373376
"CheckoutSummary": {
374377
"title": "Summary",
375378
"subTotal": "Subtotal",

core/vibes/soul/sections/cart/client.tsx

Lines changed: 101 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,14 @@ import { CartEmptyState } from '.';
3434

3535
type Action<State, Payload> = (state: Awaited<State>, payload: Payload) => State | Promise<State>;
3636

37+
interface CartLineIteminventoryMessages {
38+
outOfStockMessage?: string;
39+
quantityReadyToShipMessage?: string;
40+
quantityBackorderedMessage?: string;
41+
quantityOutOfStockMessage?: string;
42+
backorderMessage?: string;
43+
}
44+
3745
export interface CartLineItem {
3846
typename: string;
3947
id: string;
@@ -44,6 +52,7 @@ export interface CartLineItem {
4452
price: string;
4553
salePrice?: string;
4654
href?: string;
55+
inventoryMessages?: CartLineIteminventoryMessages;
4756
}
4857

4958
export interface CartGiftCertificateLineItem extends CartLineItem {
@@ -563,7 +572,7 @@ function CounterForm({
563572
<input {...getInputProps(fields.id, { type: 'hidden' })} key={fields.id.id} />
564573
<div className="flex w-full flex-wrap items-center gap-x-5 gap-y-2">
565574
{lineItem.salePrice && lineItem.salePrice !== lineItem.price ? (
566-
<span className="font-medium @xl:ml-auto">
575+
<span className="mt-3 self-start font-medium @xl:ml-auto">
567576
<span className="sr-only">{t('originalPrice', { price: lineItem.price })}</span>
568577
<span aria-hidden="true" className="line-through">
569578
{lineItem.price}
@@ -572,65 +581,101 @@ function CounterForm({
572581
<span aria-hidden="true">{lineItem.salePrice}</span>
573582
</span>
574583
) : (
575-
<span className="font-medium @xl:ml-auto">{lineItem.price}</span>
584+
<span className="mt-3 self-start font-medium @xl:ml-auto">{lineItem.price}</span>
576585
)}
577-
{/* Counter */}
578-
<div className="flex items-center rounded-lg border border-[var(--cart-counter-border,hsl(var(--contrast-100)))]">
579-
<button
580-
aria-label={decrementLabel}
581-
className={clsx(
582-
'group rounded-l-lg bg-[var(--cart-counter-background,hsl(var(--background)))] p-3 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--cart-focus,hsl(var(--primary)))] disabled:cursor-not-allowed',
583-
lineItem.quantity === 1
584-
? 'opacity-50'
585-
: 'hover:bg-[var(--cart-counter-background-hover,hsl(var(--contrast-100)/50%))]',
586-
)}
587-
disabled={lineItem.quantity === 1}
588-
name="intent"
589-
type="submit"
590-
value="decrement"
591-
>
592-
<Minus
586+
<div className="flex size-min flex-col gap-y-0">
587+
<div className="mb-1 mt-1 flex items-center gap-x-5">
588+
{/* Counter */}
589+
<div
593590
className={clsx(
594-
'text-[var(--cart-counter-icon,hsl(var(--contrast-300)))] transition-colors duration-300',
595-
lineItem.quantity !== 1 &&
596-
'group-hover:text-[var(--cart-counter-icon-hover,hsl(var(--foreground)))]',
591+
'flex items-center rounded-lg border border-[var(--cart-counter-border,hsl(var(--contrast-100)))]',
592+
(lineItem.inventoryMessages?.outOfStockMessage != null ||
593+
lineItem.inventoryMessages?.quantityOutOfStockMessage != null) &&
594+
'border-red-500',
597595
)}
598-
size={18}
599-
strokeWidth={1.5}
600-
/>
601-
</button>
602-
<span className="flex w-8 select-none justify-center focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--cart-focus,hsl(var(--primary)))]">
603-
{lineItem.quantity}
604-
</span>
605-
<button
606-
aria-label={incrementLabel}
607-
className={clsx(
608-
'group rounded-r-lg bg-[var(--cart-counter-background,hsl(var(--background)))] p-3 transition-colors duration-300 hover:bg-[var(--cart-counter-background-hover,hsl(var(--contrast-100)/50%))] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--cart-focus,hsl(var(--primary)))] disabled:cursor-not-allowed',
609-
)}
610-
name="intent"
611-
type="submit"
612-
value="increment"
613-
>
614-
<Plus
615-
className="text-[var(--cart-counter-icon,hsl(var(--contrast-300)))] transition-colors duration-300 group-hover:text-[var(--cart-counter-icon-hover,hsl(var(--foreground)))]"
616-
size={18}
617-
strokeWidth={1.5}
618-
/>
619-
</button>
596+
>
597+
<button
598+
aria-label={decrementLabel}
599+
className={clsx(
600+
'group rounded-l-lg bg-[var(--cart-counter-background,hsl(var(--background)))] p-3 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--cart-focus,hsl(var(--primary)))] disabled:cursor-not-allowed',
601+
lineItem.quantity === 1
602+
? 'opacity-50'
603+
: 'hover:bg-[var(--cart-counter-background-hover,hsl(var(--contrast-100)/50%))]',
604+
)}
605+
disabled={lineItem.quantity === 1}
606+
name="intent"
607+
type="submit"
608+
value="decrement"
609+
>
610+
<Minus
611+
className={clsx(
612+
'text-[var(--cart-counter-icon,hsl(var(--contrast-300)))] transition-colors duration-300',
613+
lineItem.quantity !== 1 &&
614+
'group-hover:text-[var(--cart-counter-icon-hover,hsl(var(--foreground)))]',
615+
)}
616+
size={18}
617+
strokeWidth={1.5}
618+
/>
619+
</button>
620+
<span className="flex w-8 select-none justify-center focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--cart-focus,hsl(var(--primary)))]">
621+
{lineItem.quantity}
622+
</span>
623+
<button
624+
aria-label={incrementLabel}
625+
className={clsx(
626+
'group rounded-r-lg bg-[var(--cart-counter-background,hsl(var(--background)))] p-3 transition-colors duration-300 hover:bg-[var(--cart-counter-background-hover,hsl(var(--contrast-100)/50%))] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--cart-focus,hsl(var(--primary)))] disabled:cursor-not-allowed',
627+
)}
628+
name="intent"
629+
type="submit"
630+
value="increment"
631+
>
632+
<Plus
633+
className="text-[var(--cart-counter-icon,hsl(var(--contrast-300)))] transition-colors duration-300 group-hover:text-[var(--cart-counter-icon-hover,hsl(var(--foreground)))]"
634+
size={18}
635+
strokeWidth={1.5}
636+
/>
637+
</button>
638+
</div>
639+
<button
640+
aria-label={deleteLabel}
641+
className="group -ml-1 mt-1.5 flex h-8 w-8 shrink-0 items-center justify-center self-start rounded-full transition-colors duration-300 hover:bg-[var(--cart-button-background,hsl(var(--contrast-100)))] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--cart-focus,hsl(var(--primary)))] focus-visible:ring-offset-4"
642+
name="intent"
643+
type="submit"
644+
value="delete"
645+
>
646+
<Trash2
647+
className="text-[var(--cart-icon,hsl(var(--contrast-300)))] group-hover:text-[var(--cart-icon-hover,hsl(var(--foreground)))]"
648+
size={20}
649+
strokeWidth={1}
650+
/>
651+
</button>
652+
</div>
653+
{lineItem.inventoryMessages?.outOfStockMessage != null && (
654+
<span className="text-xs/5 font-light text-red-500">
655+
{lineItem.inventoryMessages.outOfStockMessage}
656+
</span>
657+
)}
658+
{lineItem.inventoryMessages?.quantityOutOfStockMessage != null && (
659+
<span className="mb-3 text-xs/5 font-light text-red-500">
660+
{lineItem.inventoryMessages.quantityOutOfStockMessage}
661+
</span>
662+
)}
663+
{lineItem.inventoryMessages?.quantityReadyToShipMessage != null && (
664+
<span className="text-xs/5 font-light">
665+
{lineItem.inventoryMessages.quantityReadyToShipMessage}
666+
</span>
667+
)}
668+
{lineItem.inventoryMessages?.quantityBackorderedMessage != null && (
669+
<span className="text-xs/5 font-light">
670+
{lineItem.inventoryMessages.quantityBackorderedMessage}
671+
</span>
672+
)}
673+
{lineItem.inventoryMessages?.backorderMessage != null && (
674+
<span className="text-xs/5 font-light">
675+
{lineItem.inventoryMessages.backorderMessage}
676+
</span>
677+
)}
620678
</div>
621-
<button
622-
aria-label={deleteLabel}
623-
className="group -ml-1 flex h-8 w-8 shrink-0 items-center justify-center rounded-full transition-colors duration-300 hover:bg-[var(--cart-button-background,hsl(var(--contrast-100)))] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--cart-focus,hsl(var(--primary)))] focus-visible:ring-offset-4"
624-
name="intent"
625-
type="submit"
626-
value="delete"
627-
>
628-
<Trash2
629-
className="text-[var(--cart-icon,hsl(var(--contrast-300)))] group-hover:text-[var(--cart-icon-hover,hsl(var(--foreground)))]"
630-
size={20}
631-
strokeWidth={1}
632-
/>
633-
</button>
634679
</div>
635680
</form>
636681
);

0 commit comments

Comments
 (0)