diff --git a/packages/document/src/stories/availability/001.availability.mdx b/packages/document/src/stories/availability/001.availability.mdx index a0e10d26..95dbac63 100644 --- a/packages/document/src/stories/availability/001.availability.mdx +++ b/packages/document/src/stories/availability/001.availability.mdx @@ -12,15 +12,13 @@ All Availability components must be nested inside the `` context. --- -## AvailabilityContainer +## Availability (standalone) -`AvailabilityContainer` is the root component of the Availability tree. -It fetches inventory data for a given SKU (by code or ID) and exposes the result -to its children through the Availability context. +The preferred way to display availability. `` fetches inventory data on its own — +no container wrapper needed. - + Must be a child of the `` component. -Can also be a child of `` inside a ``, in which case `skuCode` is inherited automatically. @@ -34,6 +32,7 @@ Can also be a child of `` inside a ``, in which case `skuCo | `skuCode` | `string` | — | The SKU code to fetch availability for | | `skuId` | `string` | — | The SKU ID (takes precedence over `skuCode`; improves performance) | | `getQuantity` | `(quantity: number) => void` | — | Callback fired whenever the available quantity changes | +| `loader` | `ReactNode` | — | Content shown while fetching (default: `"Loading..."`) | ` inside a ``, in which case `skuCo code={` import { CommerceLayer, - AvailabilityContainer, + Availability, AvailabilityTemplate, } from '@commercelayer/react-components' - - - + + + `} /> --- +## AvailabilityContainer + + +`AvailabilityContainer` is deprecated. Use the standalone `` component instead (see above). + + +**Migration guide** + +Before (deprecated): + + + + +`} +/> + +After (preferred): + + + + +`} +/> + +--- + ## AvailabilityTemplate -`AvailabilityTemplate` reads from the parent `AvailabilityContainer` context and renders +`AvailabilityTemplate` reads from the parent `Availability` (or `AvailabilityContainer`) context and renders a `` with availability text. You can customise the label shown for each state (`available`, `outOfStock`, `negativeStock`) and optionally include delivery lead time and shipping method details. -Must be a descendant of the `` component. +Must be a descendant of the `` component. **Props** @@ -81,7 +114,7 @@ Must be a descendant of the `` component. language="jsx" dark code={` - + ` component. showShippingMethodName showShippingMethodPrice /> - + `} /> @@ -105,7 +138,7 @@ The function receives the full availability context including `quantity`, `text` language="jsx" dark code={` - + {({ quantity, text, min, max }) => (
@@ -116,15 +149,15 @@ The function receives the full availability context including `quantity`, `text`
)}
-
+ `} /> --- -## Usage inside SkusContainer +## Usage inside Skus -When used inside a `` → `` tree, `AvailabilityContainer` +When used inside a `` or `` → `` tree, `Availability` automatically inherits the `skuCode` from the current SKU context — no need to pass `skuCode` explicitly. @@ -134,22 +167,19 @@ no need to pass `skuCode` explicitly. code={` import { CommerceLayer, - SkusContainer, - Skus, + Sku, SkuField, - AvailabilityContainer, + Availability, AvailabilityTemplate, } from '@commercelayer/react-components' - - - - - - - - + + + + + + `} /> diff --git a/packages/document/src/stories/availability/availability.stories.tsx b/packages/document/src/stories/availability/availability.stories.tsx index 1681f3c4..a9c3f00f 100644 --- a/packages/document/src/stories/availability/availability.stories.tsx +++ b/packages/document/src/stories/availability/availability.stories.tsx @@ -1,160 +1,170 @@ -import type { Meta, StoryObj } from '@storybook/react-vite' -import CommerceLayer from '../_internals/CommerceLayer' import { + Availability, AvailabilityContainer, AvailabilityTemplate, - SkusContainer, - Skus, + Sku, SkuField, -} from '@commercelayer/react-components' +} from "@commercelayer/react-components" +import type { Meta, StoryObj } from "@storybook/react-vite" +import CommerceLayer from "../_internals/CommerceLayer" const meta = { - title: 'Availability/Stories', + title: "Availability/Stories", parameters: { - layout: 'centered', + layout: "centered", }, } satisfies Meta export default meta type Story = StoryObj -export const BasicAvailability: Story = { - name: 'AvailabilityContainer — basic', +export const StandaloneAvailability: Story = { + name: "Availability — standalone (no container)", render: () => ( - - - + + + ), } export const CustomLabels: Story = { - name: 'AvailabilityTemplate — custom labels', + name: "AvailabilityTemplate — custom labels", render: () => ( - + - + ), } export const WithDeliveryLeadTimeDays: Story = { - name: 'AvailabilityTemplate — lead time in days', + name: "AvailabilityTemplate — lead time in days", render: () => ( - + - + ), } export const WithDeliveryLeadTimeHours: Story = { - name: 'AvailabilityTemplate — lead time in hours', + name: "AvailabilityTemplate — lead time in hours", render: () => ( - + - + ), } export const WithShippingMethodName: Story = { - name: 'AvailabilityTemplate — with shipping method name', + name: "AvailabilityTemplate — with shipping method name", render: () => ( - - - + + + ), } export const WithShippingMethodPrice: Story = { - name: 'AvailabilityTemplate — with shipping method price', + name: "AvailabilityTemplate — with shipping method price", render: () => ( - + - + ), } export const WithGetQuantityCallback: Story = { - name: 'AvailabilityContainer — getQuantity callback', + name: "Availability — getQuantity callback", render: () => ( - { - console.log('quantity updated:', quantity) + console.log("quantity updated:", quantity) }} > - + ), } export const WithChildrenRenderProp: Story = { - name: 'AvailabilityTemplate — children render prop', + name: "AvailabilityTemplate — children render prop", render: () => ( - + {({ quantity, text, min, max }) => ( -
+
{text} {quantity > 0 && min != null && ( -

+

Ships in {min.days}–{max?.days ?? min.days} day(s)

)}
)} - + + + ), +} + +export const InsideSku: Story = { + name: "Availability — inside Sku (inherits skuCode)", + render: () => ( + + + + + + + ), } -export const InsideSkusContainer: Story = { - name: 'AvailabilityContainer — inside SkusContainer', +export const DeprecatedContainer: Story = { + name: "AvailabilityContainer — deprecated (legacy)", render: () => ( - - -
- - - - -
-
-
+ + +
), } diff --git a/packages/react-components/src/components/skus/Availability.tsx b/packages/react-components/src/components/skus/Availability.tsx new file mode 100644 index 00000000..2ca0e093 --- /dev/null +++ b/packages/react-components/src/components/skus/Availability.tsx @@ -0,0 +1,89 @@ +import { useAvailability } from "@commercelayer/hooks" +import { type ReactNode, useContext, useEffect, useMemo } from "react" +import AvailabilityContext from "#context/AvailabilityContext" +import CommerceLayerContext from "#context/CommerceLayerContext" +import LineItemChildrenContext from "#context/LineItemChildrenContext" +import SkuChildrenContext from "#context/SkuChildrenContext" +import type { LoaderType } from "#typings/index" + +export interface AvailabilityProps { + children: ReactNode + /** The SKU code to fetch availability for. */ + skuCode?: string + /** + * The SKU id. Takes precedence over `skuCode` and improves performance + * by skipping the SKU lookup by code. + */ + skuId?: string + /** Callback called when the quantity is updated. */ + getQuantity?: (quantity: number) => void + /** Loader shown while fetching availability data. */ + loader?: LoaderType +} + +/** + * Displays availability data for a SKU. + * + * Can be used standalone — automatically fetches availability without an + * `AvailabilityContainer` parent. Also picks up `skuCode` from context when + * nested inside `` or a line item component. + * + * @example Standalone (preferred): + * ```tsx + * + * + * + * + * + * ``` + */ +export function Availability({ + children, + skuCode, + skuId, + getQuantity, + loader: propLoader = "Loading...", +}: AvailabilityProps): ReactNode { + const { lineItem } = useContext(LineItemChildrenContext) + const { sku } = useContext(SkuChildrenContext) + const { accessToken, interceptors } = useContext(CommerceLayerContext) + const { availability, fetchAvailability, clearAvailability, isLoading } = + useAvailability(accessToken ?? "", interceptors) + + const sCode = skuCode ?? lineItem?.sku_code ?? sku?.code + + useEffect(() => { + if ( + accessToken != null && + accessToken !== "" && + (sCode != null || skuId != null) + ) { + fetchAvailability({ skuCode: sCode, skuId }) + } + return () => { + clearAvailability() + } + }, [accessToken, sCode, skuId, clearAvailability, fetchAvailability]) + + useEffect(() => { + if (getQuantity != null && availability?.quantity != null) { + getQuantity(availability.quantity) + } + }, [availability?.quantity, getQuantity]) + + const contextValue = useMemo( + () => ({ ...availability, parent: true }), + [availability], + ) + + const hasFetchTarget = sCode != null || skuId != null + if (hasFetchTarget && isLoading) return propLoader + + return ( + + {children} + + ) +} + +export default Availability diff --git a/packages/react-components/src/components/skus/AvailabilityContainer.tsx b/packages/react-components/src/components/skus/AvailabilityContainer.tsx index 33e9102e..29d34039 100644 --- a/packages/react-components/src/components/skus/AvailabilityContainer.tsx +++ b/packages/react-components/src/components/skus/AvailabilityContainer.tsx @@ -37,6 +37,10 @@ interface Props { * * `` * + * + * @deprecated Use `` instead. It provides the same functionality + * without requiring a "container" wrapper. `AvailabilityContainer` remains + * functional for backwards compatibility. */ export function AvailabilityContainer({ children, diff --git a/packages/react-components/src/index.ts b/packages/react-components/src/index.ts index 4e2f7761..9b96c9ac 100644 --- a/packages/react-components/src/index.ts +++ b/packages/react-components/src/index.ts @@ -93,6 +93,7 @@ export * from "#components/shipping_methods/ShippingMethod" export * from "#components/shipping_methods/ShippingMethodName" export * from "#components/shipping_methods/ShippingMethodPrice" export * from "#components/shipping_methods/ShippingMethodRadioButton" +export * from "#components/skus/Availability" export * from "#components/skus/AvailabilityContainer" export * from "#components/skus/AvailabilityTemplate" export * from "#components/skus/DeliveryLeadTime"