Skip to content

Commit f1bc0a2

Browse files
authored
refactor(availability): remove dead reducer, export SkuAvailability, add hook tests & stories
* refactor(availability): remove dead reducer, export SkuAvailability, add hook tests - Delete AvailabilityReducer.ts (dead code, zero references) - Re-export SkuAvailability from @commercelayer/core in public index - Add useAvailability.test.ts with 8 test cases covering initial state, fetch by skuCode, clear, error handling, isLoading state, and null return Closes #744 * fix(document): add Vite path aliases for react-components source Storybook stories in the document package use internal helpers (CommerceLayer, OrderStorage) that import via '#components/*' and other path aliases defined in the react-components tsconfig. Because those files live in the document package, vite-tsconfig-paths could not match them to the react-components project scope and failed to resolve the imports at pre-bundling time. Fix by adding explicit 'resolve.alias' entries in viteFinal so all '#components/*', '#context/*', '#hooks/*', '#typings/*', '#utils/*', '#config/*', '#reducers/*' and '#components-utils/*' paths are reliably resolved to the react-components source tree regardless of which file triggers the import. Also includes: - Fix stories glob path (../stories → ../src/stories) - New availability MDX docs and 9 interactive stories - Add missing npm dependencies (@commercelayer/react-components, @commercelayer/js-auth, js-cookie, jwt-decode, @types/js-cookie)
1 parent e25391e commit f1bc0a2

9 files changed

Lines changed: 666 additions & 202 deletions

File tree

packages/document/.storybook/main.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,22 @@ import remarkGfm from "remark-gfm"
44
import { type UserConfig, mergeConfig } from "vite"
55
import tsconfigPaths from "vite-tsconfig-paths"
66

7+
const rcSrc = resolve(import.meta.dirname, "../../react-components/src")
8+
79
const viteOverrides: UserConfig = {
810
base: process.env.VITE_BASE_URL,
11+
resolve: {
12+
alias: {
13+
"#components": `${rcSrc}/components`,
14+
"#components-utils": `${rcSrc}/components/utils`,
15+
"#context": `${rcSrc}/context`,
16+
"#hooks": `${rcSrc}/hooks`,
17+
"#typings": `${rcSrc}/typings`,
18+
"#utils": `${rcSrc}/utils`,
19+
"#config": `${rcSrc}/config`,
20+
"#reducers": `${rcSrc}/reducers`,
21+
},
22+
},
923
plugins: [
1024
tsconfigPaths({
1125
projects: [
@@ -20,7 +34,7 @@ const storybookConfig: StorybookConfig = {
2034
async viteFinal(config) {
2135
return mergeConfig(config, viteOverrides)
2236
},
23-
stories: ["../stories/**/*.mdx", "../stories/**/*.stories.@(js|jsx|ts|tsx)"],
37+
stories: ["../src/stories/**/*.mdx", "../src/stories/**/*.stories.@(js|jsx|ts|tsx)"],
2438
addons: ["@storybook/addon-links", {
2539
name: "@storybook/addon-docs",
2640
options: {

packages/document/package.json

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,30 +12,34 @@
1212
"mcp": "storybook mcp"
1313
},
1414
"dependencies": {
15+
"@commercelayer/js-auth": "^7.1.2",
16+
"@commercelayer/react-components": "workspace:*",
17+
"js-cookie": "^3.0.5",
18+
"jwt-decode": "^4.0.0",
1519
"react": "^19.2.3",
1620
"react-dom": "^19.2.3"
1721
},
1822
"devDependencies": {
23+
"@types/js-cookie": "^3.0.6",
1924
"@chromatic-com/storybook": "^5.1.1",
2025
"@eslint/js": "^9.39.2",
21-
"@storybook/addon-docs": "^10.3.3",
22-
"@storybook/addon-links": "^10.3.3",
26+
"@storybook/addon-docs": "^10.3.5",
27+
"@storybook/addon-links": "^10.3.5",
2328
"@storybook/addon-mcp": "^0.4.2",
24-
"@storybook/addon-onboarding": "^10.3.3",
29+
"@storybook/addon-onboarding": "^10.3.5",
2530
"@storybook/icons": "^2.0.1",
26-
"@storybook/react": "^10.3.3",
27-
"@storybook/react-vite": "^10.3.3",
31+
"@storybook/react-vite": "^10.3.5",
2832
"@types/react": "^19.2.8",
2933
"@types/react-dom": "^19.2.3",
3034
"@vitejs/plugin-react": "^5.1.2",
3135
"eslint": "^9.39.2",
3236
"eslint-plugin-react-hooks": "^7.0.1",
3337
"eslint-plugin-react-refresh": "^0.4.26",
34-
"eslint-plugin-storybook": "^10.3.3",
38+
"eslint-plugin-storybook": "^10.3.5",
3539
"globals": "^17.0.0",
3640
"msw": "^2.12.7",
3741
"remark-gfm": "^4.0.1",
38-
"storybook": "^10.3.3",
42+
"storybook": "^10.3.5",
3943
"typescript": "~5.9.3",
4044
"typescript-eslint": "^8.53.0",
4145
"vite": "^7.3.1",
@@ -46,4 +50,4 @@
4650
"plugin:storybook/recommended"
4751
]
4852
}
49-
}
53+
}
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
import { Meta, Source } from '@storybook/addon-docs';
2+
3+
<Meta title="Availability/Overview" />
4+
5+
# Availability
6+
7+
The Availability components let you display real-time stock quantity and delivery lead times
8+
for any SKU. They are powered by the Commerce Layer inventory model and work by fetching
9+
availability data through the `useAvailability` hook from `@commercelayer/hooks`.
10+
11+
All Availability components must be nested inside the `<CommerceLayer>` context.
12+
13+
---
14+
15+
## AvailabilityContainer
16+
17+
`AvailabilityContainer` is the root component of the Availability tree.
18+
It fetches inventory data for a given SKU (by code or ID) and exposes the result
19+
to its children through the Availability context.
20+
21+
<span title='Requirements' type='warning'>
22+
Must be a child of the `<CommerceLayer>` component.
23+
Can also be a child of `<Skus>` inside a `<SkusContainer>`, in which case `skuCode` is inherited automatically.
24+
</span>
25+
26+
<span title='Children' type='info'>
27+
`<AvailabilityTemplate>`
28+
</span>
29+
30+
**Props**
31+
32+
| Prop | Type | Required | Description |
33+
|------|------|----------|-------------|
34+
| `skuCode` | `string` || The SKU code to fetch availability for |
35+
| `skuId` | `string` || The SKU ID (takes precedence over `skuCode`; improves performance) |
36+
| `getQuantity` | `(quantity: number) => void` || Callback fired whenever the available quantity changes |
37+
38+
<Source
39+
language="jsx"
40+
dark
41+
code={`
42+
import {
43+
CommerceLayer,
44+
AvailabilityContainer,
45+
AvailabilityTemplate,
46+
} from '@commercelayer/react-components'
47+
48+
<CommerceLayer accessToken="..." endpoint="https://yourdomain.commercelayer.io">
49+
<AvailabilityContainer skuCode="TSHIRTMM000000FFFFFFXLXX">
50+
<AvailabilityTemplate />
51+
</AvailabilityContainer>
52+
</CommerceLayer>
53+
`}
54+
/>
55+
56+
---
57+
58+
## AvailabilityTemplate
59+
60+
`AvailabilityTemplate` reads from the parent `AvailabilityContainer` context and renders
61+
a `<span>` with availability text. You can customise the label shown for each state
62+
(`available`, `outOfStock`, `negativeStock`) and optionally include delivery lead time
63+
and shipping method details.
64+
65+
<span title='Requirements' type='warning'>
66+
Must be a descendant of the `<AvailabilityContainer>` component.
67+
</span>
68+
69+
**Props**
70+
71+
| Prop | Type | Default | Description |
72+
|------|------|---------|-------------|
73+
| `labels.available` | `string` | `"Available"` | Text shown when quantity > 0 |
74+
| `labels.outOfStock` | `string` | `"Out of stock"` | Text shown when quantity is 0 |
75+
| `labels.negativeStock` | `string` | `"Not available"` | Text shown when quantity is negative |
76+
| `timeFormat` | `"days" \| "hours"` || When set, delivery lead time is appended to the label |
77+
| `showShippingMethodName` | `boolean` | `false` | Requires `timeFormat`. Appends the shipping method name |
78+
| `showShippingMethodPrice` | `boolean` | `false` | Requires `timeFormat`. Appends the formatted shipping price |
79+
80+
<Source
81+
language="jsx"
82+
dark
83+
code={`
84+
<AvailabilityContainer skuCode="TSHIRTMM000000FFFFFFXLXX">
85+
<AvailabilityTemplate
86+
labels={{
87+
available: 'In stock',
88+
outOfStock: 'Sold out',
89+
}}
90+
timeFormat="days"
91+
showShippingMethodName
92+
showShippingMethodPrice
93+
/>
94+
</AvailabilityContainer>
95+
`}
96+
/>
97+
98+
### Custom render via children
99+
100+
You can fully control the rendered output by passing a function as `children`.
101+
The function receives the full availability context including `quantity`, `text`,
102+
`min`, `max`, and `shipping_method`.
103+
104+
<Source
105+
language="jsx"
106+
dark
107+
code={`
108+
<AvailabilityContainer skuCode="TSHIRTMM000000FFFFFFXLXX">
109+
<AvailabilityTemplate>
110+
{({ quantity, text, min, max }) => (
111+
<div>
112+
<strong>{text}</strong>
113+
{quantity > 0 && min != null && (
114+
<p>Ships in {min.days}–{max?.days ?? min.days} days</p>
115+
)}
116+
</div>
117+
)}
118+
</AvailabilityTemplate>
119+
</AvailabilityContainer>
120+
`}
121+
/>
122+
123+
---
124+
125+
## Usage inside SkusContainer
126+
127+
When used inside a `<SkusContainer>``<Skus>` tree, `AvailabilityContainer`
128+
automatically inherits the `skuCode` from the current SKU context —
129+
no need to pass `skuCode` explicitly.
130+
131+
<Source
132+
language="jsx"
133+
dark
134+
code={`
135+
import {
136+
CommerceLayer,
137+
SkusContainer,
138+
Skus,
139+
SkuField,
140+
AvailabilityContainer,
141+
AvailabilityTemplate,
142+
} from '@commercelayer/react-components'
143+
144+
<CommerceLayer accessToken="..." endpoint="https://yourdomain.commercelayer.io">
145+
<SkusContainer skus={['TSHIRTMM000000FFFFFFXLXX', 'PANTSMM000000FFFFFFXXXX']}>
146+
<Skus>
147+
<SkuField attribute="name" tagElement="h3" />
148+
<AvailabilityContainer>
149+
<AvailabilityTemplate />
150+
</AvailabilityContainer>
151+
</Skus>
152+
</SkusContainer>
153+
</CommerceLayer>
154+
`}
155+
/>
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
import type { Meta, StoryObj } from '@storybook/react-vite'
2+
import CommerceLayer from '../_internals/CommerceLayer'
3+
import {
4+
AvailabilityContainer,
5+
AvailabilityTemplate,
6+
SkusContainer,
7+
Skus,
8+
SkuField,
9+
} from '@commercelayer/react-components'
10+
11+
const meta = {
12+
title: 'Availability/Stories',
13+
parameters: {
14+
layout: 'centered',
15+
},
16+
} satisfies Meta
17+
18+
export default meta
19+
type Story = StoryObj<typeof meta>
20+
21+
export const BasicAvailability: Story = {
22+
name: 'AvailabilityContainer — basic',
23+
render: () => (
24+
<CommerceLayer accessToken="my-access-token">
25+
<AvailabilityContainer skuCode="POLOMXXX000000FFFFFFLXXX">
26+
<AvailabilityTemplate />
27+
</AvailabilityContainer>
28+
</CommerceLayer>
29+
),
30+
}
31+
32+
export const CustomLabels: Story = {
33+
name: 'AvailabilityTemplate — custom labels',
34+
render: () => (
35+
<CommerceLayer accessToken="my-access-token">
36+
<AvailabilityContainer skuCode="POLOMXXX000000FFFFFFLXXX">
37+
<AvailabilityTemplate
38+
labels={{
39+
available: '✅ In stock',
40+
outOfStock: '❌ Sold out',
41+
negativeStock: '⚠️ Not available',
42+
}}
43+
/>
44+
</AvailabilityContainer>
45+
</CommerceLayer>
46+
),
47+
}
48+
49+
export const WithDeliveryLeadTimeDays: Story = {
50+
name: 'AvailabilityTemplate — lead time in days',
51+
render: () => (
52+
<CommerceLayer accessToken="my-access-token">
53+
<AvailabilityContainer skuCode="POLOMXXX000000FFFFFFLXXX">
54+
<AvailabilityTemplate
55+
labels={{ available: 'Available' }}
56+
timeFormat="days"
57+
/>
58+
</AvailabilityContainer>
59+
</CommerceLayer>
60+
),
61+
}
62+
63+
export const WithDeliveryLeadTimeHours: Story = {
64+
name: 'AvailabilityTemplate — lead time in hours',
65+
render: () => (
66+
<CommerceLayer accessToken="my-access-token">
67+
<AvailabilityContainer skuCode="POLOMXXX000000FFFFFFLXXX">
68+
<AvailabilityTemplate
69+
labels={{ available: 'Available' }}
70+
timeFormat="hours"
71+
/>
72+
</AvailabilityContainer>
73+
</CommerceLayer>
74+
),
75+
}
76+
77+
export const WithShippingMethodName: Story = {
78+
name: 'AvailabilityTemplate — with shipping method name',
79+
render: () => (
80+
<CommerceLayer accessToken="my-access-token">
81+
<AvailabilityContainer skuCode="POLOMXXX000000FFFFFFLXXX">
82+
<AvailabilityTemplate
83+
timeFormat="days"
84+
showShippingMethodName
85+
/>
86+
</AvailabilityContainer>
87+
</CommerceLayer>
88+
),
89+
}
90+
91+
export const WithShippingMethodPrice: Story = {
92+
name: 'AvailabilityTemplate — with shipping method price',
93+
render: () => (
94+
<CommerceLayer accessToken="my-access-token">
95+
<AvailabilityContainer skuCode="POLOMXXX000000FFFFFFLXXX">
96+
<AvailabilityTemplate
97+
timeFormat="days"
98+
showShippingMethodName
99+
showShippingMethodPrice
100+
/>
101+
</AvailabilityContainer>
102+
</CommerceLayer>
103+
),
104+
}
105+
106+
export const WithGetQuantityCallback: Story = {
107+
name: 'AvailabilityContainer — getQuantity callback',
108+
render: () => (
109+
<CommerceLayer accessToken="my-access-token">
110+
<AvailabilityContainer
111+
skuCode="POLOMXXX000000FFFFFFLXXX"
112+
getQuantity={(quantity) => {
113+
console.log('quantity updated:', quantity)
114+
}}
115+
>
116+
<AvailabilityTemplate />
117+
</AvailabilityContainer>
118+
</CommerceLayer>
119+
),
120+
}
121+
122+
export const WithChildrenRenderProp: Story = {
123+
name: 'AvailabilityTemplate — children render prop',
124+
render: () => (
125+
<CommerceLayer accessToken="my-access-token">
126+
<AvailabilityContainer skuCode="POLOMXXX000000FFFFFFLXXX">
127+
<AvailabilityTemplate>
128+
{({ quantity, text, min, max }) => (
129+
<div style={{ fontFamily: 'monospace', fontSize: 14 }}>
130+
<strong>{text}</strong>
131+
{quantity > 0 && min != null && (
132+
<p style={{ marginTop: 4, color: '#666' }}>
133+
Ships in {min.days}{max?.days ?? min.days} day(s)
134+
</p>
135+
)}
136+
</div>
137+
)}
138+
</AvailabilityTemplate>
139+
</AvailabilityContainer>
140+
</CommerceLayer>
141+
),
142+
}
143+
144+
export const InsideSkusContainer: Story = {
145+
name: 'AvailabilityContainer — inside SkusContainer',
146+
render: () => (
147+
<CommerceLayer accessToken="my-access-token">
148+
<SkusContainer skus={['POLOMXXX000000FFFFFFLXXX', 'TSHIRTMM000000FFFFFFXLXX']}>
149+
<Skus>
150+
<div style={{ marginBottom: 16 }}>
151+
<SkuField attribute="name" tagElement="h3" style={{ marginBottom: 4 }} />
152+
<AvailabilityContainer>
153+
<AvailabilityTemplate />
154+
</AvailabilityContainer>
155+
</div>
156+
</Skus>
157+
</SkusContainer>
158+
</CommerceLayer>
159+
),
160+
}

0 commit comments

Comments
 (0)