@develemit/storefront 0.0.0-stage → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +265 -2
- package/dist/cart/cart-provider.d.ts +24 -0
- package/dist/cart/cart-reducer.d.ts +31 -0
- package/dist/cart/cart-storage.d.ts +6 -0
- package/dist/client/commerce-client.d.ts +13 -0
- package/dist/client/errors.d.ts +12 -0
- package/dist/client/index.d.ts +2 -0
- package/dist/client/index.js +64 -0
- package/dist/client/index.js.map +1 -0
- package/dist/components/add-to-cart-button/add-to-cart-button.d.ts +14 -0
- package/dist/components/availability-badge/availability-badge.d.ts +15 -0
- package/dist/components/cart-button/cart-button.d.ts +9 -0
- package/dist/components/cart-drawer/cart-drawer.d.ts +18 -0
- package/dist/components/cart-drawer/use-drawer-presence.d.ts +5 -0
- package/dist/components/cart-line-item/cart-line-item.d.ts +11 -0
- package/dist/components/cart-summary/cart-summary.d.ts +10 -0
- package/dist/components/category-filter/category-filter.d.ts +11 -0
- package/dist/components/fixtures.d.ts +2 -0
- package/dist/components/price-tag/price-tag.d.ts +7 -0
- package/dist/components/product-card/product-card.d.ts +9 -0
- package/dist/components/product-gallery/product-gallery.d.ts +6 -0
- package/dist/components/product-grid/product-grid.d.ts +10 -0
- package/dist/components/render-with-cart.d.ts +3 -0
- package/dist/format/index.d.ts +2 -0
- package/dist/format/index.js +1 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.js +686 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/format-price.d.ts +1 -0
- package/dist/lib/parse-duration.d.ts +1 -0
- package/dist/lib/provider.d.ts +28 -0
- package/dist/lib/theme.d.ts +15 -0
- package/dist/sort-for-display-Cj6GMeCY.js +15 -0
- package/dist/sort-for-display-Cj6GMeCY.js.map +1 -0
- package/dist/styles.css +476 -0
- package/dist/views/featured-products.d.ts +8 -0
- package/dist/views/product-detail-view.d.ts +13 -0
- package/dist/views/shop-view.d.ts +21 -0
- package/dist/views/sort-for-display.d.ts +2 -0
- package/package.json +46 -3
package/README.md
CHANGED
|
@@ -1,3 +1,266 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @develemit/storefront
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Brand-agnostic storefront UI and commerce logic, built so it can be extracted
|
|
4
|
+
and published as a standalone package.
|
|
5
|
+
|
|
6
|
+
## Boundary rules
|
|
7
|
+
|
|
8
|
+
- No client branding, copy, colors, or product data in this package.
|
|
9
|
+
The consuming app supplies those through props, theme tokens, and data adapters.
|
|
10
|
+
- No imports from `apps/*` or other app-specific packages.
|
|
11
|
+
- React, React DOM, and Next are peer dependencies, never bundled.
|
|
12
|
+
|
|
13
|
+
## Commands
|
|
14
|
+
|
|
15
|
+
- `pnpm exec nx build storefront` — library build to `packages/storefront/dist`
|
|
16
|
+
- `pnpm exec nx test storefront`
|
|
17
|
+
|
|
18
|
+
## Theme contract
|
|
19
|
+
|
|
20
|
+
Import `@develemit/storefront/styles.css` for neutral defaults, then override
|
|
21
|
+
the variables in the host app (or pass a `StorefrontTheme` through
|
|
22
|
+
`themeToCssVars()`).
|
|
23
|
+
|
|
24
|
+
| Variable | Purpose |
|
|
25
|
+
| ----------------------------- | ------------------------------------------------- |
|
|
26
|
+
| `--sf-color-bg` | Page background |
|
|
27
|
+
| `--sf-color-surface` | Cards and raised surfaces |
|
|
28
|
+
| `--sf-color-fg` | Primary text |
|
|
29
|
+
| `--sf-color-muted` | Secondary text |
|
|
30
|
+
| `--sf-color-accent` | Buttons and links |
|
|
31
|
+
| `--sf-color-accent-fg` | Text on accent backgrounds |
|
|
32
|
+
| `--sf-color-border` | Borders and dividers |
|
|
33
|
+
| `--sf-color-sold` | Sold / unavailable treatment |
|
|
34
|
+
| `--sf-radius` | Corner radius |
|
|
35
|
+
| `--sf-font-display` | Headings font stack |
|
|
36
|
+
| `--sf-font-body` | Body font stack |
|
|
37
|
+
| `--sf-space-unit` | Base spacing unit |
|
|
38
|
+
| `--sf-motion-duration` | Drawer motion (default `0ms`) |
|
|
39
|
+
| `--sf-color-focus` | Gallery focus ring (default `currentColor`) |
|
|
40
|
+
| `--sf-card-hover-lift` | Card hover lift (default `0px`) |
|
|
41
|
+
| `--sf-card-hover-shadow` | Card hover shadow (default `none`) |
|
|
42
|
+
| `--sf-card-image-hover-scale` | Card image hover zoom (default `1`) |
|
|
43
|
+
| `--sf-sold-opacity` | Sold card image opacity (default `0.55`) |
|
|
44
|
+
| `--sf-sold-filter` | Sold card image filter (default `grayscale(0.6)`) |
|
|
45
|
+
| `--sf-grid-columns-mobile` | Grid columns below 40rem (default `1`) |
|
|
46
|
+
|
|
47
|
+
## Components
|
|
48
|
+
|
|
49
|
+
`PriceTag`, `AvailabilityBadge`, `ProductCard`, `ProductGrid`,
|
|
50
|
+
`CategoryFilter`, and `ProductGallery` are styled only through the `--sf-*`
|
|
51
|
+
variables. Wrap them in `StorefrontProvider` to supply framework adapters;
|
|
52
|
+
without it they fall back to plain `<a>` and `<img>`.
|
|
53
|
+
|
|
54
|
+
Example Next.js host setup (host code, not part of this package):
|
|
55
|
+
|
|
56
|
+
```tsx
|
|
57
|
+
import Link from 'next/link';
|
|
58
|
+
import Image from 'next/image';
|
|
59
|
+
import { StorefrontProvider } from '@develemit/storefront';
|
|
60
|
+
import '@develemit/storefront/styles.css';
|
|
61
|
+
|
|
62
|
+
export function Providers({ children }: { children: React.ReactNode }) {
|
|
63
|
+
return (
|
|
64
|
+
<StorefrontProvider
|
|
65
|
+
locale="en-US"
|
|
66
|
+
renderLink={({ href, children, className }) => (
|
|
67
|
+
<Link href={href} className={className}>
|
|
68
|
+
{children}
|
|
69
|
+
</Link>
|
|
70
|
+
)}
|
|
71
|
+
renderImage={({ src, alt, sizes, className }) => (
|
|
72
|
+
<Image src={src} alt={alt} sizes={sizes} className={className} fill />
|
|
73
|
+
)}
|
|
74
|
+
hrefs={{
|
|
75
|
+
product: (slug) => `/shop/${slug}`,
|
|
76
|
+
category: (slug) => (slug ? `/shop?category=${slug}` : '/shop'),
|
|
77
|
+
}}
|
|
78
|
+
>
|
|
79
|
+
{children}
|
|
80
|
+
</StorefrontProvider>
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
`AvailabilityBadge` and the components that embed it take a `labels` prop to
|
|
86
|
+
override the default copy ("One of a kind", "Sold", "On hold", "Available").
|
|
87
|
+
`CategoryFilter` renders buttons when `onSelect` is passed (controlled mode) and
|
|
88
|
+
links otherwise.
|
|
89
|
+
|
|
90
|
+
## Server-safe helpers
|
|
91
|
+
|
|
92
|
+
The main entry carries a `"use client"` directive, so in React Server
|
|
93
|
+
Components its exports are client references and cannot be called. Pure helpers
|
|
94
|
+
live in a directive-free entry:
|
|
95
|
+
|
|
96
|
+
```ts
|
|
97
|
+
import { formatPrice, sortForDisplay } from '@develemit/storefront/format';
|
|
98
|
+
|
|
99
|
+
formatPrice(2500, 'USD'); // "$25.00"
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Both are still exported from the main entry for client components.
|
|
103
|
+
|
|
104
|
+
## API client
|
|
105
|
+
|
|
106
|
+
`createCommerceClient` lives in a separate, React-free entry so server code can
|
|
107
|
+
use it without pulling in the UI:
|
|
108
|
+
|
|
109
|
+
```ts
|
|
110
|
+
import { createCommerceClient } from '@develemit/storefront/client';
|
|
111
|
+
|
|
112
|
+
const client = createCommerceClient({ baseUrl: 'https://example.com/api' });
|
|
113
|
+
const { items, nextCursor } = await client.listProducts({
|
|
114
|
+
category: 'rings',
|
|
115
|
+
limit: 24, // default 48, max 100
|
|
116
|
+
// cursor: nextCursor from the previous page
|
|
117
|
+
});
|
|
118
|
+
await client.getProduct('sample-piece'); // throws NotFoundError on 404
|
|
119
|
+
await client.listCategories();
|
|
120
|
+
await client.createCheckout([{ productId: 'p1', quantity: 1 }]); // { url }
|
|
121
|
+
await client.getOrderBySession('cs_test_123'); // order summary
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
`baseUrl` is where the commerce server plugins are mounted; the client appends
|
|
125
|
+
`/catalog/...`, `/checkout`, and `/orders/...`. Responses are parsed with the
|
|
126
|
+
contract schemas, so a drifting server fails loudly. Other non-2xx responses
|
|
127
|
+
throw `CommerceApiError`.
|
|
128
|
+
|
|
129
|
+
`createCheckout` throws `CheckoutConflictError` (a 409) when some items are
|
|
130
|
+
sold or held by another buyer; its `productIds` lists them so the cart can drop
|
|
131
|
+
them. Send only ids and quantities: the server prices every line itself.
|
|
132
|
+
Redirect the shopper to the returned `url`.
|
|
133
|
+
|
|
134
|
+
## Images in production
|
|
135
|
+
|
|
136
|
+
The default image adapter is a plain `<img loading="lazy" decoding="async">`
|
|
137
|
+
(`priority` switches to `loading="eager"` and `fetchpriority="high"`). It does
|
|
138
|
+
not resize. Production hosts **must** supply a resizing `renderImage` (such as
|
|
139
|
+
`next/image`) or serve through a resizing CDN; otherwise every card downloads
|
|
140
|
+
the full-size original, and admin uploads can be up to 8MB.
|
|
141
|
+
|
|
142
|
+
Pass accurate `sizes` so the adapter picks a small variant. For `ProductGrid`,
|
|
143
|
+
the sizes should match your column layout, for example
|
|
144
|
+
`(min-width: 1024px) 25vw, (min-width: 640px) 50vw, 100vw`.
|
|
145
|
+
|
|
146
|
+
## Views
|
|
147
|
+
|
|
148
|
+
`ShopView`, `ProductDetailView`, and `FeaturedProducts` take plain data plus
|
|
149
|
+
slots (`header`, `intro`, `emptyState`, `actions`, `afterDetails`).
|
|
150
|
+
`ShopView` lists sold items last (`sortForDisplay`).
|
|
151
|
+
|
|
152
|
+
### Render a shop in a Next app
|
|
153
|
+
|
|
154
|
+
```tsx
|
|
155
|
+
// app/shop/page.tsx (server component)
|
|
156
|
+
import { createCommerceClient } from '@develemit/storefront/client';
|
|
157
|
+
import { ShopView } from '@develemit/storefront';
|
|
158
|
+
|
|
159
|
+
const client = createCommerceClient({ baseUrl: process.env.COMMERCE_API_URL! });
|
|
160
|
+
|
|
161
|
+
export default async function ShopPage({
|
|
162
|
+
searchParams,
|
|
163
|
+
}: {
|
|
164
|
+
searchParams: Promise<{ category?: string; cursor?: string }>;
|
|
165
|
+
}) {
|
|
166
|
+
const { category, cursor } = await searchParams;
|
|
167
|
+
const [page, categories] = await Promise.all([
|
|
168
|
+
client.listProducts({ category, cursor }),
|
|
169
|
+
client.listCategories(),
|
|
170
|
+
]);
|
|
171
|
+
return (
|
|
172
|
+
<ShopView
|
|
173
|
+
products={page.items}
|
|
174
|
+
nextHref={
|
|
175
|
+
page.nextCursor
|
|
176
|
+
? `/shop?cursor=${encodeURIComponent(page.nextCursor)}`
|
|
177
|
+
: undefined
|
|
178
|
+
}
|
|
179
|
+
categories={categories}
|
|
180
|
+
activeCategory={category}
|
|
181
|
+
slots={{ header: <h1>Shop</h1> }}
|
|
182
|
+
/>
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
```tsx
|
|
188
|
+
// app/shop/[slug]/page.tsx
|
|
189
|
+
import { notFound } from 'next/navigation';
|
|
190
|
+
import {
|
|
191
|
+
createCommerceClient,
|
|
192
|
+
NotFoundError,
|
|
193
|
+
} from '@develemit/storefront/client';
|
|
194
|
+
import { ProductDetailView } from '@develemit/storefront';
|
|
195
|
+
|
|
196
|
+
export default async function ProductPage({
|
|
197
|
+
params,
|
|
198
|
+
}: {
|
|
199
|
+
params: Promise<{ slug: string }>;
|
|
200
|
+
}) {
|
|
201
|
+
const { slug } = await params;
|
|
202
|
+
try {
|
|
203
|
+
const product = await client.getProduct(slug);
|
|
204
|
+
return <ProductDetailView product={product} />;
|
|
205
|
+
} catch (e) {
|
|
206
|
+
if (e instanceof NotFoundError) notFound();
|
|
207
|
+
throw e;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
## Cart
|
|
213
|
+
|
|
214
|
+
Wrap the app in `CartProvider` (inside `StorefrontProvider`). State is a
|
|
215
|
+
snapshot of `CartLine`s; prices and availability are re-verified server-side
|
|
216
|
+
at checkout. Persistence goes through a `CartStorage` (`load`/`save`); the
|
|
217
|
+
default is `localStorageCartStorage(key)`, which hydrates after mount and never
|
|
218
|
+
touches `window` during render.
|
|
219
|
+
|
|
220
|
+
```tsx
|
|
221
|
+
<CartProvider>
|
|
222
|
+
<CartButton />
|
|
223
|
+
<CartDrawer summary={{ onCheckout, checkoutLabel: 'Checkout' }} />
|
|
224
|
+
</CartProvider>
|
|
225
|
+
|
|
226
|
+
<ProductDetailView
|
|
227
|
+
product={product}
|
|
228
|
+
slots={{ actions: <AddToCartButton product={product} /> }}
|
|
229
|
+
/>
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
`useCart()` exposes `add` (returns `{ added, capped, reason? }`), `remove`,
|
|
233
|
+
`setQuantity`, `clear`, `open`/`close`, and the derived `itemCount` and
|
|
234
|
+
`subtotalCents`. A product's `quantity` is the line's `maxQuantity`, so
|
|
235
|
+
one-of-a-kind items can only be added once. Every label is overridable
|
|
236
|
+
through a `labels` prop.
|
|
237
|
+
|
|
238
|
+
### Polish hooks
|
|
239
|
+
|
|
240
|
+
- `CartDrawer` `slots={{ empty, summaryNote }}`: `empty` replaces the
|
|
241
|
+
`labels.empty` text (for example with a link back to the shop); `summaryNote`
|
|
242
|
+
renders under the subtotal and Checkout button. An empty cart shows no
|
|
243
|
+
summary or Checkout button. Close and Remove are at least 44×44px.
|
|
244
|
+
- `CartButton` `icon` (rendered before the label), `labelHidden` (visually
|
|
245
|
+
hides the label, keeping an accessible name like "Cart, 2 items") and
|
|
246
|
+
`hideZeroCount` (omits the count at 0; the accessible name becomes
|
|
247
|
+
"Cart, empty").
|
|
248
|
+
- `--sf-motion-duration` (default `0ms`, no motion) drives the drawer's
|
|
249
|
+
slide-in and the backdrop's fade, in and out. It is forced to `0ms` under
|
|
250
|
+
`prefers-reduced-motion: reduce`. Example: `--sf-motion-duration: 200ms`.
|
|
251
|
+
|
|
252
|
+
### Catalog presentation hooks
|
|
253
|
+
|
|
254
|
+
- Card hover: `--sf-card-hover-lift`, `--sf-card-hover-shadow` and
|
|
255
|
+
`--sf-card-image-hover-scale` apply on `:hover` and `:focus-within`, animated
|
|
256
|
+
with `--sf-motion-duration` (off under `prefers-reduced-motion: reduce`).
|
|
257
|
+
- Sold treatment: `--sf-sold-opacity` and `--sf-sold-filter` style the sold
|
|
258
|
+
card image.
|
|
259
|
+
- `--sf-grid-columns-mobile` sets the column count below 40rem.
|
|
260
|
+
- `CategoryFilter` `formatLabel(category)` and `sortCategories(a, b)` change
|
|
261
|
+
the label text and order (the input array is not mutated); `allLabel` renames
|
|
262
|
+
the All item. `ShopView` forwards them as `formatCategoryLabel`,
|
|
263
|
+
`sortCategories` and `allLabel`.
|
|
264
|
+
- `ShopView`, `FeaturedProducts` and `ProductDetailView` accept
|
|
265
|
+
`labels?: Partial<AvailabilityLabels>` for the availability badge.
|
|
266
|
+
- The gallery focus ring uses `--sf-color-focus`.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { ReactNode } from 'react';
|
|
2
|
+
import { CartLine } from '@develemit/commerce-contracts';
|
|
3
|
+
import { AddResult } from './cart-reducer';
|
|
4
|
+
import { CartStorage } from './cart-storage';
|
|
5
|
+
export interface CartContextValue {
|
|
6
|
+
lines: CartLine[];
|
|
7
|
+
itemCount: number;
|
|
8
|
+
subtotalCents: number;
|
|
9
|
+
currency: string | null;
|
|
10
|
+
hydrated: boolean;
|
|
11
|
+
isOpen: boolean;
|
|
12
|
+
open: () => void;
|
|
13
|
+
close: () => void;
|
|
14
|
+
add: (line: CartLine) => AddResult;
|
|
15
|
+
remove: (productId: string) => void;
|
|
16
|
+
setQuantity: (productId: string, quantity: number) => void;
|
|
17
|
+
clear: () => void;
|
|
18
|
+
}
|
|
19
|
+
export interface CartProviderProps {
|
|
20
|
+
storage?: CartStorage;
|
|
21
|
+
children: ReactNode;
|
|
22
|
+
}
|
|
23
|
+
export declare function CartProvider({ storage, children, }: CartProviderProps): import('react').JSX.Element;
|
|
24
|
+
export declare function useCart(): CartContextValue;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { CartLine } from '@develemit/commerce-contracts';
|
|
2
|
+
export interface CartState {
|
|
3
|
+
lines: CartLine[];
|
|
4
|
+
}
|
|
5
|
+
export type CartAction = {
|
|
6
|
+
type: 'add';
|
|
7
|
+
line: CartLine;
|
|
8
|
+
} | {
|
|
9
|
+
type: 'remove';
|
|
10
|
+
productId: string;
|
|
11
|
+
} | {
|
|
12
|
+
type: 'setQuantity';
|
|
13
|
+
productId: string;
|
|
14
|
+
quantity: number;
|
|
15
|
+
} | {
|
|
16
|
+
type: 'clear';
|
|
17
|
+
} | {
|
|
18
|
+
type: 'hydrate';
|
|
19
|
+
state: CartState;
|
|
20
|
+
};
|
|
21
|
+
export interface AddResult {
|
|
22
|
+
state: CartState;
|
|
23
|
+
added: boolean;
|
|
24
|
+
capped: boolean;
|
|
25
|
+
reason?: 'currency-mismatch';
|
|
26
|
+
}
|
|
27
|
+
export declare const emptyCart: CartState;
|
|
28
|
+
export declare function addLine(state: CartState, line: CartLine): AddResult;
|
|
29
|
+
export declare function cartReducer(state: CartState, action: CartAction): CartState;
|
|
30
|
+
export declare function subtotalCents(state: CartState): number;
|
|
31
|
+
export declare function itemCount(state: CartState): number;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Category, CheckoutRequest, CheckoutResponse, OrderSummary, ProductCardPage, Product, ProductListQuery } from '@develemit/commerce-contracts';
|
|
2
|
+
export interface CommerceClientOptions {
|
|
3
|
+
baseUrl: string;
|
|
4
|
+
fetch?: typeof fetch;
|
|
5
|
+
}
|
|
6
|
+
export interface CommerceClient {
|
|
7
|
+
listProducts(query?: ProductListQuery): Promise<ProductCardPage>;
|
|
8
|
+
getProduct(slug: string): Promise<Product>;
|
|
9
|
+
listCategories(): Promise<Category[]>;
|
|
10
|
+
createCheckout(items: CheckoutRequest['items']): Promise<CheckoutResponse>;
|
|
11
|
+
getOrderBySession(sessionId: string): Promise<OrderSummary>;
|
|
12
|
+
}
|
|
13
|
+
export declare function createCommerceClient({ baseUrl, fetch: fetchImpl, }: CommerceClientOptions): CommerceClient;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export declare class CommerceApiError extends Error {
|
|
2
|
+
readonly status: number;
|
|
3
|
+
readonly url: string;
|
|
4
|
+
constructor(status: number, url: string, message?: string);
|
|
5
|
+
}
|
|
6
|
+
export declare class NotFoundError extends CommerceApiError {
|
|
7
|
+
constructor(url: string);
|
|
8
|
+
}
|
|
9
|
+
export declare class CheckoutConflictError extends CommerceApiError {
|
|
10
|
+
readonly productIds: string[];
|
|
11
|
+
constructor(url: string, productIds: string[]);
|
|
12
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { categorySchema as e, checkoutConflictSchema as t, checkoutResponseSchema as n, orderSummarySchema as r, productCardPageSchema as i, productSchema as a } from "@develemit/commerce-contracts";
|
|
2
|
+
// src/client/errors.ts
|
|
3
|
+
var o = class extends Error {
|
|
4
|
+
status;
|
|
5
|
+
url;
|
|
6
|
+
constructor(e, t, n) {
|
|
7
|
+
super(n ?? `Commerce API request failed (${e}): ${t}`), this.status = e, this.url = t, this.name = "CommerceApiError";
|
|
8
|
+
}
|
|
9
|
+
}, s = class extends o {
|
|
10
|
+
constructor(e) {
|
|
11
|
+
super(404, e, `Not found: ${e}`), this.name = "NotFoundError";
|
|
12
|
+
}
|
|
13
|
+
}, c = class extends o {
|
|
14
|
+
productIds;
|
|
15
|
+
constructor(e, t) {
|
|
16
|
+
super(409, e, `Some items are no longer available: ${t.join(", ")}`), this.productIds = t, this.name = "CheckoutConflictError";
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
// src/client/commerce-client.ts
|
|
20
|
+
function l({ baseUrl: l, fetch: u = (...e) => globalThis.fetch(...e) }) {
|
|
21
|
+
let d = l.replace(/\/+$/, "");
|
|
22
|
+
async function f(e, t, n) {
|
|
23
|
+
if (t.status === 404) throw new s(e);
|
|
24
|
+
if (!t.ok) throw new o(t.status, e);
|
|
25
|
+
return n.parse(await t.json());
|
|
26
|
+
}
|
|
27
|
+
async function p(e, t) {
|
|
28
|
+
let n = `${d}${e}`;
|
|
29
|
+
return f(n, await u(n, { headers: { accept: "application/json" } }), t);
|
|
30
|
+
}
|
|
31
|
+
async function m(e) {
|
|
32
|
+
let r = `${d}/checkout`, i = await u(r, {
|
|
33
|
+
method: "POST",
|
|
34
|
+
headers: {
|
|
35
|
+
accept: "application/json",
|
|
36
|
+
"content-type": "application/json"
|
|
37
|
+
},
|
|
38
|
+
body: JSON.stringify({ items: e })
|
|
39
|
+
});
|
|
40
|
+
if (i.status === 409) throw new c(r, t.parse(await i.json()).unavailableProductIds);
|
|
41
|
+
return f(r, i, n);
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
async listProducts(e = {}) {
|
|
45
|
+
let t = new URLSearchParams();
|
|
46
|
+
e.category && t.set("category", e.category), e.featured !== void 0 && t.set("featured", String(e.featured)), e.limit !== void 0 && t.set("limit", String(e.limit)), e.cursor && t.set("cursor", e.cursor);
|
|
47
|
+
let n = t.toString();
|
|
48
|
+
return p(`/catalog/products${n ? `?${n}` : ""}`, i);
|
|
49
|
+
},
|
|
50
|
+
getProduct(e) {
|
|
51
|
+
return p(`/catalog/products/${encodeURIComponent(e)}`, a);
|
|
52
|
+
},
|
|
53
|
+
listCategories() {
|
|
54
|
+
return p("/catalog/categories", e.array());
|
|
55
|
+
},
|
|
56
|
+
createCheckout: m,
|
|
57
|
+
getOrderBySession(e) {
|
|
58
|
+
return p(`/orders/session/${encodeURIComponent(e)}`, r);
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
export { c as CheckoutConflictError, o as CommerceApiError, s as NotFoundError, l as createCommerceClient };
|
|
63
|
+
|
|
64
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":["CommerceApiError","message","status","url","NotFoundError","CheckoutConflictError","productIds","createCommerceClient","fetchImpl","args","root","baseUrl","readJson","res","schema","get","path","createCheckout","checkoutConflictSchema","checkoutResponseSchema","params","query","qs","productCardPageSchema","slug","productSchema","categorySchema","sessionId","orderSummarySchema"],"sources":["../../src/client/errors.ts","../../src/client/commerce-client.ts"],"sourcesContent":["export class CommerceApiError extends Error {\n constructor(\n readonly status: number,\n readonly url: string,\n message?: string,\n ) {\n super(message ?? `Commerce API request failed (${status}): ${url}`);\n this.name = 'CommerceApiError';\n }\n}\n\nexport class NotFoundError extends CommerceApiError {\n constructor(url: string) {\n super(404, url, `Not found: ${url}`);\n this.name = 'NotFoundError';\n }\n}\n\nexport class CheckoutConflictError extends CommerceApiError {\n constructor(\n url: string,\n readonly productIds: string[],\n ) {\n super(\n 409,\n url,\n `Some items are no longer available: ${productIds.join(', ')}`,\n );\n this.name = 'CheckoutConflictError';\n }\n}\n","import {\n categorySchema,\n checkoutConflictSchema,\n checkoutResponseSchema,\n orderSummarySchema,\n productCardPageSchema,\n productSchema,\n type Category,\n type CheckoutRequest,\n type CheckoutResponse,\n type OrderSummary,\n type ProductCardPage,\n type Product,\n type ProductListQuery,\n} from '@develemit/commerce-contracts';\nimport {\n CheckoutConflictError,\n CommerceApiError,\n NotFoundError,\n} from './errors';\n\nexport interface CommerceClientOptions {\n baseUrl: string;\n fetch?: typeof fetch;\n}\n\nexport interface CommerceClient {\n listProducts(query?: ProductListQuery): Promise<ProductCardPage>;\n getProduct(slug: string): Promise<Product>;\n listCategories(): Promise<Category[]>;\n createCheckout(items: CheckoutRequest['items']): Promise<CheckoutResponse>;\n getOrderBySession(sessionId: string): Promise<OrderSummary>;\n}\n\ntype Schema<T> = { parse(data: unknown): T };\n\nexport function createCommerceClient({\n baseUrl,\n fetch: fetchImpl = (...args) => globalThis.fetch(...args),\n}: CommerceClientOptions): CommerceClient {\n const root = baseUrl.replace(/\\/+$/, '');\n\n async function readJson<T>(\n url: string,\n res: Response,\n schema: Schema<T>,\n ): Promise<T> {\n if (res.status === 404) throw new NotFoundError(url);\n if (!res.ok) throw new CommerceApiError(res.status, url);\n return schema.parse(await res.json());\n }\n\n async function get<T>(path: string, schema: Schema<T>): Promise<T> {\n const url = `${root}${path}`;\n const res = await fetchImpl(url, {\n headers: { accept: 'application/json' },\n });\n return readJson(url, res, schema);\n }\n\n async function createCheckout(\n items: CheckoutRequest['items'],\n ): Promise<CheckoutResponse> {\n const url = `${root}/checkout`;\n const res = await fetchImpl(url, {\n method: 'POST',\n headers: {\n accept: 'application/json',\n 'content-type': 'application/json',\n },\n body: JSON.stringify({ items }),\n });\n if (res.status === 409) {\n const conflict = checkoutConflictSchema.parse(await res.json());\n throw new CheckoutConflictError(url, conflict.unavailableProductIds);\n }\n return readJson(url, res, checkoutResponseSchema);\n }\n\n return {\n async listProducts(query = {}) {\n const params = new URLSearchParams();\n if (query.category) params.set('category', query.category);\n if (query.featured !== undefined) {\n params.set('featured', String(query.featured));\n }\n if (query.limit !== undefined) params.set('limit', String(query.limit));\n if (query.cursor) params.set('cursor', query.cursor);\n const qs = params.toString();\n return get(\n `/catalog/products${qs ? `?${qs}` : ''}`,\n productCardPageSchema,\n );\n },\n getProduct(slug) {\n return get(\n `/catalog/products/${encodeURIComponent(slug)}`,\n productSchema,\n );\n },\n listCategories() {\n return get('/catalog/categories', categorySchema.array());\n },\n createCheckout,\n getOrderBySession(sessionId) {\n return get(\n `/orders/session/${encodeURIComponent(sessionId)}`,\n orderSummarySchema,\n );\n },\n };\n}\n"],"mappings":";;AAAA,IAAaA,IAAb,cAAsC,MAAM;CAE/B;CACA;CAFX,YACE,GACA,GACA,GACA;EAEA,AADA,MAAMC,KAAW,gCAAgCC,EAAO,KAAKC,GAAK,GAJzD,KAAA,SAAAD,GACA,KAAA,MAAAC,GAIT,KAAK,OAAO;CACd;AACF,GAEaC,IAAb,cAAmCJ,EAAiB;CAClD,YAAY,GAAa;EAEvB,AADA,MAAM,KAAKG,GAAK,cAAcA,GAAK,GACnC,KAAK,OAAO;CACd;AACF,GAEaE,IAAb,cAA2CL,EAAiB;CAG/C;CAFX,YACE,GACA,GACA;EAMA,AALA,MACE,KACAG,GACA,uCAAuCG,EAAW,KAAK,IAAI,GAC7D,GANS,KAAA,aAAAA,GAOT,KAAK,OAAO;CACd;AACF;;ACMA,SAAgBC,EAAqB,EACnC,YACA,OAAOC,KAAa,GAAGC,MAAS,WAAW,MAAM,GAAGA,CAAI,KAChB;CACxC,IAAMC,IAAOC,EAAQ,QAAQ,QAAQ,EAAE;CAEvC,eAAeC,EACb,GACA,GACA,GACY;EACZ,IAAIC,EAAI,WAAW,KAAK,MAAM,IAAIT,EAAcD,CAAG;EACnD,IAAI,CAACU,EAAI,IAAI,MAAM,IAAIb,EAAiBa,EAAI,QAAQV,CAAG;EACvD,OAAOW,EAAO,MAAM,MAAMD,EAAI,KAAK,CAAC;CACtC;CAEA,eAAeE,EAAO,GAAc,GAA+B;EACjE,IAAMZ,IAAM,GAAGO,IAAOM;EAItB,OAAOJ,EAAST,GAAK,MAHHK,EAAUL,GAAK,EAC/B,SAAS,EAAE,QAAQ,mBAAmB,EACxC,CAAC,GACyBW,CAAM;CAClC;CAEA,eAAeG,EACb,GAC2B;EAC3B,IAAMd,IAAM,GAAGO,EAAK,YACdG,IAAM,MAAML,EAAUL,GAAK;GAC/B,QAAQ;GACR,SAAS;IACP,QAAQ;IACR,gBAAgB;GAClB;GACA,MAAM,KAAK,UAAU,EAAE,SAAM,CAAC;EAChC,CAAC;EACD,IAAIU,EAAI,WAAW,KAEjB,MAAM,IAAIR,EAAsBF,GADfe,EAAuB,MAAM,MAAML,EAAI,KAAK,CACxB,CAAA,CAAS,qBAAqB;EAErE,OAAOD,EAAST,GAAKU,GAAKM,CAAsB;CAClD;CAEA,OAAO;EACL,MAAM,aAAa,IAAQ,CAAC,GAAG;GAC7B,IAAMC,IAAS,IAAI,gBAAgB;GAMnC,AALIC,EAAM,YAAUD,EAAO,IAAI,YAAYC,EAAM,QAAQ,GACrDA,EAAM,aAAa,KAAA,KACrBD,EAAO,IAAI,YAAY,OAAOC,EAAM,QAAQ,CAAC,GAE3CA,EAAM,UAAU,KAAA,KAAWD,EAAO,IAAI,SAAS,OAAOC,EAAM,KAAK,CAAC,GAClEA,EAAM,UAAQD,EAAO,IAAI,UAAUC,EAAM,MAAM;GACnD,IAAMC,IAAKF,EAAO,SAAS;GAC3B,OAAOL,EACL,oBAAoBO,IAAK,IAAIA,MAAO,MACpCC,CACF;EACF;EACA,WAAW,GAAM;GACf,OAAOR,EACL,qBAAqB,mBAAmBS,CAAI,KAC5CC,CACF;EACF;EACA,iBAAiB;GACf,OAAOV,EAAI,uBAAuBW,EAAe,MAAM,CAAC;EAC1D;EACA;EACA,kBAAkB,GAAW;GAC3B,OAAOX,EACL,mBAAmB,mBAAmBY,CAAS,KAC/CC,CACF;EACF;CACF;AACF"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Product } from '@develemit/commerce-contracts';
|
|
2
|
+
export interface AddToCartLabels {
|
|
3
|
+
add: string;
|
|
4
|
+
inCart: string;
|
|
5
|
+
unavailable: string;
|
|
6
|
+
}
|
|
7
|
+
export declare const defaultAddToCartLabels: AddToCartLabels;
|
|
8
|
+
export interface AddToCartButtonProps {
|
|
9
|
+
product: Product;
|
|
10
|
+
labels?: Partial<AddToCartLabels>;
|
|
11
|
+
openOnAdd?: boolean;
|
|
12
|
+
className?: string;
|
|
13
|
+
}
|
|
14
|
+
export declare function AddToCartButton({ product, labels, openOnAdd, className, }: AddToCartButtonProps): import('react').JSX.Element;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { ProductAvailability } from '@develemit/commerce-contracts';
|
|
2
|
+
export interface AvailabilityLabels {
|
|
3
|
+
oneOfAKind: string;
|
|
4
|
+
sold: string;
|
|
5
|
+
reserved: string;
|
|
6
|
+
available: string;
|
|
7
|
+
}
|
|
8
|
+
export declare const defaultAvailabilityLabels: AvailabilityLabels;
|
|
9
|
+
export interface AvailabilityBadgeProps {
|
|
10
|
+
availability: ProductAvailability;
|
|
11
|
+
quantity: number;
|
|
12
|
+
labels?: Partial<AvailabilityLabels>;
|
|
13
|
+
className?: string;
|
|
14
|
+
}
|
|
15
|
+
export declare function AvailabilityBadge({ availability, quantity, labels, className, }: AvailabilityBadgeProps): import('react').JSX.Element;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { ReactNode } from 'react';
|
|
2
|
+
export interface CartButtonProps {
|
|
3
|
+
label?: string;
|
|
4
|
+
className?: string;
|
|
5
|
+
icon?: ReactNode;
|
|
6
|
+
labelHidden?: boolean;
|
|
7
|
+
hideZeroCount?: boolean;
|
|
8
|
+
}
|
|
9
|
+
export declare function CartButton({ label, className, icon, labelHidden, hideZeroCount, }: CartButtonProps): import('react').JSX.Element;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { ReactNode } from 'react';
|
|
2
|
+
import { CartSummaryProps } from '../cart-summary/cart-summary';
|
|
3
|
+
export interface CartDrawerLabels {
|
|
4
|
+
title: string;
|
|
5
|
+
close: string;
|
|
6
|
+
empty: string;
|
|
7
|
+
}
|
|
8
|
+
export declare const defaultCartDrawerLabels: CartDrawerLabels;
|
|
9
|
+
export interface CartDrawerProps {
|
|
10
|
+
labels?: Partial<CartDrawerLabels>;
|
|
11
|
+
summary?: CartSummaryProps;
|
|
12
|
+
slots?: CartDrawerSlots;
|
|
13
|
+
}
|
|
14
|
+
export interface CartDrawerSlots {
|
|
15
|
+
empty?: ReactNode;
|
|
16
|
+
summaryNote?: ReactNode;
|
|
17
|
+
}
|
|
18
|
+
export declare function CartDrawer({ labels, summary, slots }: CartDrawerProps): import('react').JSX.Element | null;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { CartLine } from '@develemit/commerce-contracts';
|
|
2
|
+
export interface CartLineItemLabels {
|
|
3
|
+
remove: string;
|
|
4
|
+
quantity: string;
|
|
5
|
+
}
|
|
6
|
+
export declare const defaultCartLineItemLabels: CartLineItemLabels;
|
|
7
|
+
export interface CartLineItemProps {
|
|
8
|
+
line: CartLine;
|
|
9
|
+
labels?: Partial<CartLineItemLabels>;
|
|
10
|
+
}
|
|
11
|
+
export declare function CartLineItem({ line, labels }: CartLineItemProps): import('react').JSX.Element;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export interface CartSummaryLabels {
|
|
2
|
+
subtotal: string;
|
|
3
|
+
}
|
|
4
|
+
export interface CartSummaryProps {
|
|
5
|
+
onCheckout?: () => void;
|
|
6
|
+
checkoutLabel?: string;
|
|
7
|
+
checkoutDisabled?: boolean;
|
|
8
|
+
labels?: Partial<CartSummaryLabels>;
|
|
9
|
+
}
|
|
10
|
+
export declare function CartSummary({ onCheckout, checkoutLabel, checkoutDisabled, labels, }: CartSummaryProps): import('react').JSX.Element;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { Category } from '@develemit/commerce-contracts';
|
|
2
|
+
export interface CategoryFilterProps {
|
|
3
|
+
categories: Category[];
|
|
4
|
+
selected?: string;
|
|
5
|
+
onSelect?: (slug: string | undefined) => void;
|
|
6
|
+
allLabel?: string;
|
|
7
|
+
label?: string;
|
|
8
|
+
formatLabel?: (category: Category) => string;
|
|
9
|
+
sortCategories?: (a: Category, b: Category) => number;
|
|
10
|
+
}
|
|
11
|
+
export declare function CategoryFilter({ categories, selected, onSelect, allLabel, label, formatLabel, sortCategories, }: CategoryFilterProps): import('react').JSX.Element;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { ProductCard as ProductCardData } from '@develemit/commerce-contracts';
|
|
2
|
+
import { AvailabilityLabels } from '../availability-badge/availability-badge';
|
|
3
|
+
export interface ProductCardProps {
|
|
4
|
+
product: ProductCardData;
|
|
5
|
+
labels?: Partial<AvailabilityLabels>;
|
|
6
|
+
sizes?: string;
|
|
7
|
+
priority?: boolean;
|
|
8
|
+
}
|
|
9
|
+
export declare function ProductCard({ product, labels, sizes, priority, }: ProductCardProps): import('react').JSX.Element;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { ReactNode } from 'react';
|
|
2
|
+
import { ProductCard as ProductCardData } from '@develemit/commerce-contracts';
|
|
3
|
+
import { AvailabilityLabels } from '../availability-badge/availability-badge';
|
|
4
|
+
export interface ProductGridProps {
|
|
5
|
+
products: ProductCardData[];
|
|
6
|
+
emptyState?: ReactNode;
|
|
7
|
+
labels?: Partial<AvailabilityLabels>;
|
|
8
|
+
priorityCount?: number;
|
|
9
|
+
}
|
|
10
|
+
export declare function ProductGrid({ products, emptyState, labels, priorityCount, }: ProductGridProps): import('react').JSX.Element;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { n as formatPrice, t as sortForDisplay } from "../sort-for-display-Cj6GMeCY.js";
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export type { Product, ProductCard as ProductCardData, ProductCardPage, ProductImage, ProductAvailability, Category, CartLine, CheckoutRequest, CheckoutResponse, OrderSummary, } from '@develemit/commerce-contracts';
|
|
2
|
+
export * from './lib/format-price';
|
|
3
|
+
export * from './lib/theme';
|
|
4
|
+
export * from './lib/provider';
|
|
5
|
+
export * from './components/price-tag/price-tag';
|
|
6
|
+
export * from './components/availability-badge/availability-badge';
|
|
7
|
+
export * from './components/product-card/product-card';
|
|
8
|
+
export * from './components/product-grid/product-grid';
|
|
9
|
+
export * from './components/category-filter/category-filter';
|
|
10
|
+
export * from './components/product-gallery/product-gallery';
|
|
11
|
+
export * from './views/shop-view';
|
|
12
|
+
export * from './views/product-detail-view';
|
|
13
|
+
export * from './views/featured-products';
|
|
14
|
+
export * from './views/sort-for-display';
|
|
15
|
+
export * from './cart/cart-reducer';
|
|
16
|
+
export * from './cart/cart-storage';
|
|
17
|
+
export * from './cart/cart-provider';
|
|
18
|
+
export * from './components/add-to-cart-button/add-to-cart-button';
|
|
19
|
+
export * from './components/cart-drawer/cart-drawer';
|
|
20
|
+
export * from './components/cart-line-item/cart-line-item';
|
|
21
|
+
export * from './components/cart-summary/cart-summary';
|
|
22
|
+
export * from './components/cart-button/cart-button';
|