@businessdash/sdk 0.9.60 → 0.9.80
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 +991 -295
- package/assets/biab-sw.js +92 -0
- package/dist/account-core/index.d.ts +126 -0
- package/dist/account-core/index.d.ts.map +1 -0
- package/dist/account-core/index.js +191 -0
- package/dist/account-core/index.js.map +1 -0
- package/dist/account.d.ts +11 -0
- package/dist/account.d.ts.map +1 -0
- package/dist/account.js +11 -0
- package/dist/account.js.map +1 -0
- package/dist/auth-handler.d.ts +1 -1
- package/dist/auth-links.d.ts +33 -0
- package/dist/auth-links.d.ts.map +1 -0
- package/dist/auth-links.js +64 -0
- package/dist/auth-links.js.map +1 -0
- package/dist/blog-core/index.d.ts +112 -0
- package/dist/blog-core/index.d.ts.map +1 -0
- package/dist/blog-core/index.js +330 -0
- package/dist/blog-core/index.js.map +1 -0
- package/dist/blog.d.ts +11 -0
- package/dist/blog.d.ts.map +1 -0
- package/dist/blog.js +11 -0
- package/dist/blog.js.map +1 -0
- package/dist/chat-core/index.d.ts +97 -0
- package/dist/chat-core/index.d.ts.map +1 -0
- package/dist/chat-core/index.js +237 -0
- package/dist/chat-core/index.js.map +1 -0
- package/dist/chat.d.ts +11 -0
- package/dist/chat.d.ts.map +1 -0
- package/dist/chat.js +11 -0
- package/dist/chat.js.map +1 -0
- package/dist/checkout-core/index.d.ts +65 -0
- package/dist/checkout-core/index.d.ts.map +1 -0
- package/dist/checkout-core/index.js +159 -0
- package/dist/checkout-core/index.js.map +1 -0
- package/dist/checkout.d.ts +10 -0
- package/dist/checkout.d.ts.map +1 -0
- package/dist/checkout.js +10 -0
- package/dist/checkout.js.map +1 -0
- package/dist/cli.js +133 -25
- package/dist/cli.js.map +1 -1
- package/dist/client.d.ts +622 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +888 -1
- package/dist/client.js.map +1 -1
- package/dist/consent-core/index.cjs +107 -0
- package/dist/consent-core/index.d.ts +103 -0
- package/dist/consent-core/index.d.ts.map +1 -0
- package/dist/consent-core/index.js +157 -0
- package/dist/consent-core/index.js.map +1 -0
- package/dist/contracts.d.ts +3526 -767
- package/dist/contracts.d.ts.map +1 -1
- package/dist/contracts.js +571 -4
- package/dist/contracts.js.map +1 -1
- package/dist/data-model-schema.d.ts +1 -1
- package/dist/data-model-schema.d.ts.map +1 -1
- package/dist/element.d.ts +30 -0
- package/dist/element.d.ts.map +1 -1
- package/dist/element.js +56 -0
- package/dist/element.js.map +1 -1
- package/dist/embed-protocol.d.ts +86 -0
- package/dist/embed-protocol.d.ts.map +1 -0
- package/dist/embed-protocol.js +60 -0
- package/dist/embed-protocol.js.map +1 -0
- package/dist/framework-support.d.ts +122 -0
- package/dist/framework-support.d.ts.map +1 -0
- package/dist/framework-support.js +323 -0
- package/dist/framework-support.js.map +1 -0
- package/dist/index.cjs +1355 -24
- package/dist/legal-core/index.cjs +115 -0
- package/dist/legal-core/index.d.ts +160 -0
- package/dist/legal-core/index.d.ts.map +1 -0
- package/dist/legal-core/index.js +220 -0
- package/dist/legal-core/index.js.map +1 -0
- package/dist/marketing-core/index.d.ts +66 -0
- package/dist/marketing-core/index.d.ts.map +1 -0
- package/dist/marketing-core/index.js +154 -0
- package/dist/marketing-core/index.js.map +1 -0
- package/dist/marketing.d.ts +7 -0
- package/dist/marketing.d.ts.map +1 -0
- package/dist/marketing.js +7 -0
- package/dist/marketing.js.map +1 -0
- package/dist/mcp-adapters/index.d.ts +135 -0
- package/dist/mcp-adapters/index.d.ts.map +1 -0
- package/dist/mcp-adapters/index.js +187 -0
- package/dist/mcp-adapters/index.js.map +1 -0
- package/dist/mcp-adapters.d.ts +7 -0
- package/dist/mcp-adapters.d.ts.map +1 -0
- package/dist/mcp-adapters.js +7 -0
- package/dist/mcp-adapters.js.map +1 -0
- package/dist/model3d-core/index.cjs +95 -0
- package/dist/model3d-core/index.d.ts +167 -0
- package/dist/model3d-core/index.d.ts.map +1 -0
- package/dist/model3d-core/index.js +158 -0
- package/dist/model3d-core/index.js.map +1 -0
- package/dist/notifications-core/index.cjs +315 -0
- package/dist/notifications-core/index.d.ts +229 -0
- package/dist/notifications-core/index.d.ts.map +1 -0
- package/dist/notifications-core/index.js +445 -0
- package/dist/notifications-core/index.js.map +1 -0
- package/dist/portal-core/index.d.ts +451 -0
- package/dist/portal-core/index.d.ts.map +1 -0
- package/dist/portal-core/index.js +819 -0
- package/dist/portal-core/index.js.map +1 -0
- package/dist/portal.d.ts +7 -0
- package/dist/portal.d.ts.map +1 -0
- package/dist/portal.js +7 -0
- package/dist/portal.js.map +1 -0
- package/dist/push-core/index.cjs +171 -0
- package/dist/push-core/index.d.ts +149 -0
- package/dist/push-core/index.d.ts.map +1 -0
- package/dist/push-core/index.js +231 -0
- package/dist/push-core/index.js.map +1 -0
- package/dist/react-marketing.d.ts +1 -17
- package/dist/react-marketing.d.ts.map +1 -1
- package/dist/react-marketing.js +4 -0
- package/dist/react-marketing.js.map +1 -1
- package/dist/react.d.ts +294 -101
- package/dist/react.d.ts.map +1 -1
- package/dist/react.js +302 -246
- package/dist/react.js.map +1 -1
- package/dist/seed-core/index.d.ts +354 -0
- package/dist/seed-core/index.d.ts.map +1 -0
- package/dist/seed-core/index.js +420 -0
- package/dist/seed-core/index.js.map +1 -0
- package/dist/seed.d.ts +7 -0
- package/dist/seed.d.ts.map +1 -0
- package/dist/seed.js +7 -0
- package/dist/seed.js.map +1 -0
- package/dist/seo-core/blog.d.ts +94 -0
- package/dist/seo-core/blog.d.ts.map +1 -0
- package/dist/seo-core/blog.js +149 -0
- package/dist/seo-core/blog.js.map +1 -0
- package/dist/seo-core/commerce.d.ts +64 -0
- package/dist/seo-core/commerce.d.ts.map +1 -0
- package/dist/seo-core/commerce.js +87 -0
- package/dist/seo-core/commerce.js.map +1 -0
- package/dist/seo-core/index.cjs +751 -0
- package/dist/seo-core/index.d.ts +226 -0
- package/dist/seo-core/index.d.ts.map +1 -0
- package/dist/seo-core/index.js +455 -0
- package/dist/seo-core/index.js.map +1 -0
- package/dist/seo-core/local.d.ts +181 -0
- package/dist/seo-core/local.d.ts.map +1 -0
- package/dist/seo-core/local.js +193 -0
- package/dist/seo-core/local.js.map +1 -0
- package/dist/sitemap-core/index.cjs +167 -0
- package/dist/sitemap-core/index.d.ts +155 -0
- package/dist/sitemap-core/index.d.ts.map +1 -0
- package/dist/sitemap-core/index.js +195 -0
- package/dist/sitemap-core/index.js.map +1 -0
- package/dist/social-core/index.d.ts +74 -0
- package/dist/social-core/index.d.ts.map +1 -0
- package/dist/social-core/index.js +72 -0
- package/dist/social-core/index.js.map +1 -0
- package/dist/social.d.ts +7 -0
- package/dist/social.d.ts.map +1 -0
- package/dist/social.js +7 -0
- package/dist/social.js.map +1 -0
- package/dist/solid-compat.d.ts +204 -0
- package/dist/solid-compat.d.ts.map +1 -0
- package/dist/solid-compat.js +221 -0
- package/dist/solid-compat.js.map +1 -0
- package/dist/store-core/index.d.ts +141 -0
- package/dist/store-core/index.d.ts.map +1 -0
- package/dist/store-core/index.js +277 -0
- package/dist/store-core/index.js.map +1 -0
- package/dist/store.d.ts +12 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/store.js +12 -0
- package/dist/store.js.map +1 -0
- package/dist/vanilla.d.ts +111 -0
- package/dist/vanilla.d.ts.map +1 -1
- package/dist/vanilla.js +206 -0
- package/dist/vanilla.js.map +1 -1
- package/dist/verify-core/index.d.ts +98 -0
- package/dist/verify-core/index.d.ts.map +1 -0
- package/dist/verify-core/index.js +186 -0
- package/dist/verify-core/index.js.map +1 -0
- package/dist/verify.d.ts +8 -0
- package/dist/verify.d.ts.map +1 -0
- package/dist/verify.js +8 -0
- package/dist/verify.js.map +1 -0
- package/openapi.json +4516 -1311
- package/package.json +128 -11
- package/src/account-core/index.ts +309 -0
- package/src/account.ts +11 -0
- package/src/angular/headless.ts +519 -0
- package/src/angular/index.ts +3 -0
- package/src/auth-links.ts +76 -0
- package/src/blog-core/index.ts +423 -0
- package/src/blog.ts +11 -0
- package/src/chat-core/index.ts +335 -0
- package/src/chat.ts +11 -0
- package/src/checkout-core/index.ts +225 -0
- package/src/checkout.ts +10 -0
- package/src/cli.ts +184 -28
- package/src/client.ts +1097 -0
- package/src/consent-core/index.ts +213 -0
- package/src/contracts.ts +768 -11
- package/src/data-model-schema.ts +1 -1
- package/src/element.ts +102 -0
- package/src/embed-protocol.ts +100 -0
- package/src/framework-support.ts +440 -0
- package/src/legal-core/index.ts +276 -0
- package/src/marketing-core/index.ts +209 -0
- package/src/marketing.ts +7 -0
- package/src/mcp-adapters/index.ts +264 -0
- package/src/mcp-adapters.ts +7 -0
- package/src/model3d-core/index.ts +248 -0
- package/src/notifications-core/index.ts +650 -0
- package/src/portal-core/index.ts +1564 -0
- package/src/portal.ts +7 -0
- package/src/push-core/index.ts +335 -0
- package/src/qwik.tsx +602 -2
- package/src/react-marketing.tsx +5 -0
- package/src/react.tsx +434 -319
- package/src/seed-core/index.ts +735 -0
- package/src/seed.ts +7 -0
- package/src/seo-core/blog.ts +216 -0
- package/src/seo-core/commerce.ts +138 -0
- package/src/seo-core/index.ts +605 -0
- package/src/seo-core/local.ts +333 -0
- package/src/sitemap-core/index.ts +296 -0
- package/src/social-core/index.ts +103 -0
- package/src/social.ts +7 -0
- package/src/solid-compat.tsx +347 -0
- package/src/solid-surfaces.tsx +1311 -0
- package/src/solid.tsx +82 -60
- package/src/store-core/index.ts +426 -0
- package/src/store.ts +12 -0
- package/src/svelte/SocialLinks.svelte +84 -0
- package/src/svelte/headless.ts +435 -0
- package/src/svelte/index.ts +9 -0
- package/src/vanilla.ts +367 -0
- package/src/verify-core/index.ts +288 -0
- package/src/verify.ts +8 -0
- package/src/vue/headless.ts +527 -0
- package/src/vue/index.ts +3 -0
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@businessdash/sdk/store` — the headless storefront + cart controller.
|
|
3
|
+
*
|
|
4
|
+
* ## Why this exists
|
|
5
|
+
*
|
|
6
|
+
* Until now the only storefront the SDK offered was `<Storefront>`: a
|
|
7
|
+
* BusinessDash-hosted page in a sandboxed iframe. It works, and it ships fast,
|
|
8
|
+
* but it renders OUR markup inside YOUR app — your design system stops at the
|
|
9
|
+
* iframe boundary, and there is no way to put a product card in your own grid.
|
|
10
|
+
* That was true in every binding, React included.
|
|
11
|
+
*
|
|
12
|
+
* This is the native path. It owns all of the behaviour — pagination, category
|
|
13
|
+
* filtering, the cart, coupons, the product detail — and none of the markup.
|
|
14
|
+
* Exactly the split `createFormController` already uses for forms: one
|
|
15
|
+
* framework-agnostic controller, and a thin reactive view per framework.
|
|
16
|
+
*
|
|
17
|
+
* A binding subscribes to `snapshot()` and renders it. That is the whole
|
|
18
|
+
* contract, and it is why React, Solid, Vue, Svelte, Qwik, Angular and plain
|
|
19
|
+
* DOM can each expose a native storefront without any of them reimplementing
|
|
20
|
+
* what "add to cart" means.
|
|
21
|
+
*
|
|
22
|
+
* ## Money
|
|
23
|
+
*
|
|
24
|
+
* Every price crossing this boundary is an INTEGER of the currency's minor unit
|
|
25
|
+
* — cents for USD. The controller never divides, rounds or formats: it passes
|
|
26
|
+
* what the API sent straight to your renderer. Format at the edge, where you
|
|
27
|
+
* know the locale.
|
|
28
|
+
*
|
|
29
|
+
* @module
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
import type {
|
|
33
|
+
CartAddItemInput,
|
|
34
|
+
CartSnapshot,
|
|
35
|
+
StorefrontCategory,
|
|
36
|
+
StorefrontProductDetail,
|
|
37
|
+
StorefrontProductRow,
|
|
38
|
+
} from "../contracts.js";
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* The client surface this controller needs. Declared structurally rather than
|
|
42
|
+
* importing `BiabDevClient`, so a consumer can hand in a same-origin proxy, a
|
|
43
|
+
* test double, or a narrowed client without the controller caring.
|
|
44
|
+
*/
|
|
45
|
+
export type StoreCoreClient = {
|
|
46
|
+
storefront: {
|
|
47
|
+
listProducts(input?: {
|
|
48
|
+
limit?: number;
|
|
49
|
+
cursor?: number | null;
|
|
50
|
+
categoryId?: string;
|
|
51
|
+
}): Promise<{ items: StorefrontProductRow[]; nextCursor: number | null }>;
|
|
52
|
+
listCategories(): Promise<{ items: StorefrontCategory[] }>;
|
|
53
|
+
getProduct(productId: string): Promise<StorefrontProductDetail>;
|
|
54
|
+
};
|
|
55
|
+
cart: {
|
|
56
|
+
get(): Promise<CartSnapshot>;
|
|
57
|
+
addItem(input: CartAddItemInput): Promise<CartSnapshot>;
|
|
58
|
+
updateItem(
|
|
59
|
+
itemId: string,
|
|
60
|
+
input: { quantity: number },
|
|
61
|
+
): Promise<CartSnapshot>;
|
|
62
|
+
removeItem(itemId: string): Promise<CartSnapshot>;
|
|
63
|
+
applyCoupon(input: { code: string }): Promise<CartSnapshot>;
|
|
64
|
+
removeCoupon(): Promise<CartSnapshot>;
|
|
65
|
+
clear(): Promise<CartSnapshot>;
|
|
66
|
+
};
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
export type StoreStatus = "idle" | "loading" | "ready" | "error";
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* What a binding renders. Field names are STABLE and load-bearing for every
|
|
73
|
+
* framework binding — renaming one is a breaking change in eight places.
|
|
74
|
+
*/
|
|
75
|
+
export type StoreSnapshot = {
|
|
76
|
+
status: StoreStatus;
|
|
77
|
+
/** The current page of products, accumulated across `loadMore()`. */
|
|
78
|
+
products: readonly StorefrontProductRow[];
|
|
79
|
+
categories: readonly StorefrontCategory[];
|
|
80
|
+
/** `null` until the first cart read resolves. */
|
|
81
|
+
cart: CartSnapshot | null;
|
|
82
|
+
/** The product opened via `openProduct()`, if any. */
|
|
83
|
+
activeProduct: StorefrontProductDetail | null;
|
|
84
|
+
/** Active category filter, or `null` for everything. */
|
|
85
|
+
categoryId: string | null;
|
|
86
|
+
/** Whether `loadMore()` would fetch anything. */
|
|
87
|
+
hasMore: boolean;
|
|
88
|
+
/** Last failure. Cleared on the next successful action. */
|
|
89
|
+
error: Error | null;
|
|
90
|
+
/**
|
|
91
|
+
* In-flight work, so a binding can disable a button without inventing its
|
|
92
|
+
* own loading state. `cart` covers every mutation — add, update, remove,
|
|
93
|
+
* coupon, clear — because they all serialise onto the same cart.
|
|
94
|
+
*/
|
|
95
|
+
pending: {
|
|
96
|
+
products: boolean;
|
|
97
|
+
categories: boolean;
|
|
98
|
+
product: boolean;
|
|
99
|
+
cart: boolean;
|
|
100
|
+
};
|
|
101
|
+
/** Convenience mirrors of `cart`, so a badge doesn't need a null check. */
|
|
102
|
+
itemCount: number;
|
|
103
|
+
subtotal: number;
|
|
104
|
+
currency: string | null;
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
export type StoreController = {
|
|
108
|
+
snapshot(): StoreSnapshot;
|
|
109
|
+
/**
|
|
110
|
+
* Subscribe to snapshot changes. Returns an unsubscribe function. The
|
|
111
|
+
* callback is NOT invoked on subscribe — read `snapshot()` for the initial
|
|
112
|
+
* value, the same contract `createFormController` uses.
|
|
113
|
+
*/
|
|
114
|
+
subscribe(callback: (snapshot: StoreSnapshot) => void): () => void;
|
|
115
|
+
/** Fetch products, categories and the cart. Safe to call more than once. */
|
|
116
|
+
load(): Promise<void>;
|
|
117
|
+
/** Append the next page. No-op when `hasMore` is false or a page is in flight. */
|
|
118
|
+
loadMore(): Promise<void>;
|
|
119
|
+
/** Filter by category (or `null` for all) and reset pagination. */
|
|
120
|
+
filterByCategory(categoryId: string | null): Promise<void>;
|
|
121
|
+
/** Load one product's detail into `activeProduct`. */
|
|
122
|
+
openProduct(productId: string): Promise<void>;
|
|
123
|
+
closeProduct(): void;
|
|
124
|
+
addToCart(input: CartAddItemInput): Promise<void>;
|
|
125
|
+
/** Quantity 0 removes the line, matching the API's own semantics. */
|
|
126
|
+
updateQuantity(itemId: string, quantity: number): Promise<void>;
|
|
127
|
+
removeItem(itemId: string): Promise<void>;
|
|
128
|
+
applyCoupon(code: string): Promise<void>;
|
|
129
|
+
removeCoupon(): Promise<void>;
|
|
130
|
+
clearCart(): Promise<void>;
|
|
131
|
+
/** Re-read the cart without touching products. */
|
|
132
|
+
refreshCart(): Promise<void>;
|
|
133
|
+
/** Stop notifying and drop all subscribers. */
|
|
134
|
+
destroy(): void;
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
export type CreateStoreControllerOptions = {
|
|
138
|
+
client: StoreCoreClient;
|
|
139
|
+
/** Products per page. Default 24. */
|
|
140
|
+
pageSize?: number;
|
|
141
|
+
/** Start filtered to a category. */
|
|
142
|
+
categoryId?: string | null;
|
|
143
|
+
/** Called on every failure, in addition to landing in `snapshot().error`. */
|
|
144
|
+
onError?: (error: Error) => void;
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
const DEFAULT_PAGE_SIZE = 24;
|
|
148
|
+
|
|
149
|
+
function toError(cause: unknown, fallback: string): Error {
|
|
150
|
+
return cause instanceof Error ? cause : new Error(fallback);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function createStoreController(
|
|
154
|
+
options: CreateStoreControllerOptions,
|
|
155
|
+
): StoreController {
|
|
156
|
+
const { client, onError } = options;
|
|
157
|
+
const pageSize = options.pageSize ?? DEFAULT_PAGE_SIZE;
|
|
158
|
+
|
|
159
|
+
let destroyed = false;
|
|
160
|
+
let cursor: number | null = null;
|
|
161
|
+
/**
|
|
162
|
+
* Guards against a stale page landing after the category changed. Every
|
|
163
|
+
* product fetch captures the token it started under and drops its result if
|
|
164
|
+
* the token moved on — otherwise clicking two categories quickly can leave
|
|
165
|
+
* the second one's products under the first one's heading.
|
|
166
|
+
*/
|
|
167
|
+
let requestToken = 0;
|
|
168
|
+
|
|
169
|
+
const subscribers = new Set<(s: StoreSnapshot) => void>();
|
|
170
|
+
|
|
171
|
+
const state: {
|
|
172
|
+
status: StoreStatus;
|
|
173
|
+
products: StorefrontProductRow[];
|
|
174
|
+
categories: StorefrontCategory[];
|
|
175
|
+
cart: CartSnapshot | null;
|
|
176
|
+
activeProduct: StorefrontProductDetail | null;
|
|
177
|
+
categoryId: string | null;
|
|
178
|
+
hasMore: boolean;
|
|
179
|
+
error: Error | null;
|
|
180
|
+
pending: StoreSnapshot["pending"];
|
|
181
|
+
} = {
|
|
182
|
+
status: "idle",
|
|
183
|
+
products: [],
|
|
184
|
+
categories: [],
|
|
185
|
+
cart: null,
|
|
186
|
+
activeProduct: null,
|
|
187
|
+
categoryId: options.categoryId ?? null,
|
|
188
|
+
hasMore: false,
|
|
189
|
+
error: null,
|
|
190
|
+
pending: {
|
|
191
|
+
products: false,
|
|
192
|
+
categories: false,
|
|
193
|
+
product: false,
|
|
194
|
+
cart: false,
|
|
195
|
+
},
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
let cached: StoreSnapshot | null = null;
|
|
199
|
+
|
|
200
|
+
function snapshot(): StoreSnapshot {
|
|
201
|
+
if (!cached) {
|
|
202
|
+
cached = {
|
|
203
|
+
status: state.status,
|
|
204
|
+
products: state.products,
|
|
205
|
+
categories: state.categories,
|
|
206
|
+
cart: state.cart,
|
|
207
|
+
activeProduct: state.activeProduct,
|
|
208
|
+
categoryId: state.categoryId,
|
|
209
|
+
hasMore: state.hasMore,
|
|
210
|
+
error: state.error,
|
|
211
|
+
pending: state.pending,
|
|
212
|
+
itemCount: state.cart?.itemCount ?? 0,
|
|
213
|
+
subtotal: state.cart?.subtotal ?? 0,
|
|
214
|
+
currency: state.cart?.currency ?? null,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
return cached;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function notify(): void {
|
|
221
|
+
if (destroyed) return;
|
|
222
|
+
cached = null;
|
|
223
|
+
const next = snapshot();
|
|
224
|
+
for (const cb of subscribers) cb(next);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function fail(cause: unknown, fallback: string): void {
|
|
228
|
+
const error = toError(cause, fallback);
|
|
229
|
+
state.error = error;
|
|
230
|
+
if (state.status !== "ready") state.status = "error";
|
|
231
|
+
onError?.(error);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** Run a cart mutation, keeping `pending.cart` honest even on failure. */
|
|
235
|
+
async function cartAction(
|
|
236
|
+
run: () => Promise<CartSnapshot>,
|
|
237
|
+
fallback: string,
|
|
238
|
+
): Promise<void> {
|
|
239
|
+
if (destroyed) return;
|
|
240
|
+
state.pending.cart = true;
|
|
241
|
+
state.error = null;
|
|
242
|
+
notify();
|
|
243
|
+
try {
|
|
244
|
+
const cart = await run();
|
|
245
|
+
if (destroyed) return;
|
|
246
|
+
state.cart = cart;
|
|
247
|
+
} catch (cause) {
|
|
248
|
+
if (destroyed) return;
|
|
249
|
+
fail(cause, fallback);
|
|
250
|
+
} finally {
|
|
251
|
+
if (!destroyed) {
|
|
252
|
+
state.pending.cart = false;
|
|
253
|
+
notify();
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
async function fetchProducts(append: boolean): Promise<void> {
|
|
259
|
+
const token = ++requestToken;
|
|
260
|
+
state.pending.products = true;
|
|
261
|
+
if (!append) state.error = null;
|
|
262
|
+
notify();
|
|
263
|
+
try {
|
|
264
|
+
const page = await client.storefront.listProducts({
|
|
265
|
+
limit: pageSize,
|
|
266
|
+
...(append && cursor !== null ? { cursor } : {}),
|
|
267
|
+
...(state.categoryId ? { categoryId: state.categoryId } : {}),
|
|
268
|
+
});
|
|
269
|
+
// A newer request superseded this one — its result is now wrong.
|
|
270
|
+
if (destroyed || token !== requestToken) return;
|
|
271
|
+
state.products = append ? [...state.products, ...page.items] : page.items;
|
|
272
|
+
cursor = page.nextCursor;
|
|
273
|
+
state.hasMore = page.nextCursor !== null;
|
|
274
|
+
state.status = "ready";
|
|
275
|
+
} catch (cause) {
|
|
276
|
+
if (destroyed || token !== requestToken) return;
|
|
277
|
+
fail(cause, "Failed to load products.");
|
|
278
|
+
} finally {
|
|
279
|
+
if (!destroyed && token === requestToken) {
|
|
280
|
+
state.pending.products = false;
|
|
281
|
+
notify();
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return {
|
|
287
|
+
snapshot,
|
|
288
|
+
|
|
289
|
+
subscribe(callback) {
|
|
290
|
+
subscribers.add(callback);
|
|
291
|
+
return () => {
|
|
292
|
+
subscribers.delete(callback);
|
|
293
|
+
};
|
|
294
|
+
},
|
|
295
|
+
|
|
296
|
+
async load() {
|
|
297
|
+
if (destroyed) return;
|
|
298
|
+
if (state.status === "idle") state.status = "loading";
|
|
299
|
+
state.pending.categories = true;
|
|
300
|
+
notify();
|
|
301
|
+
|
|
302
|
+
// Categories and the cart are independent of the product page, so all
|
|
303
|
+
// three go out together rather than waterfalling.
|
|
304
|
+
const categories = client.storefront
|
|
305
|
+
.listCategories()
|
|
306
|
+
.then((res) => {
|
|
307
|
+
if (destroyed) return;
|
|
308
|
+
state.categories = res.items;
|
|
309
|
+
})
|
|
310
|
+
.catch((cause) => {
|
|
311
|
+
if (!destroyed) fail(cause, "Failed to load categories.");
|
|
312
|
+
})
|
|
313
|
+
.finally(() => {
|
|
314
|
+
if (!destroyed) state.pending.categories = false;
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
const cart = client.cart
|
|
318
|
+
.get()
|
|
319
|
+
.then((res) => {
|
|
320
|
+
if (destroyed) return;
|
|
321
|
+
state.cart = res;
|
|
322
|
+
})
|
|
323
|
+
.catch((cause) => {
|
|
324
|
+
// A visitor with no cart yet is normal, not an error worth
|
|
325
|
+
// surfacing — leave `cart` null and let the products render.
|
|
326
|
+
void cause;
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
await Promise.all([fetchProducts(false), categories, cart]);
|
|
330
|
+
if (!destroyed) notify();
|
|
331
|
+
},
|
|
332
|
+
|
|
333
|
+
async loadMore() {
|
|
334
|
+
if (destroyed || !state.hasMore || state.pending.products) return;
|
|
335
|
+
await fetchProducts(true);
|
|
336
|
+
},
|
|
337
|
+
|
|
338
|
+
async filterByCategory(categoryId) {
|
|
339
|
+
if (destroyed) return;
|
|
340
|
+
state.categoryId = categoryId;
|
|
341
|
+
cursor = null;
|
|
342
|
+
await fetchProducts(false);
|
|
343
|
+
},
|
|
344
|
+
|
|
345
|
+
async openProduct(productId) {
|
|
346
|
+
if (destroyed) return;
|
|
347
|
+
state.pending.product = true;
|
|
348
|
+
state.error = null;
|
|
349
|
+
notify();
|
|
350
|
+
try {
|
|
351
|
+
const detail = await client.storefront.getProduct(productId);
|
|
352
|
+
if (destroyed) return;
|
|
353
|
+
state.activeProduct = detail;
|
|
354
|
+
} catch (cause) {
|
|
355
|
+
if (!destroyed) fail(cause, "Failed to load the product.");
|
|
356
|
+
} finally {
|
|
357
|
+
if (!destroyed) {
|
|
358
|
+
state.pending.product = false;
|
|
359
|
+
notify();
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
},
|
|
363
|
+
|
|
364
|
+
closeProduct() {
|
|
365
|
+
if (destroyed) return;
|
|
366
|
+
state.activeProduct = null;
|
|
367
|
+
notify();
|
|
368
|
+
},
|
|
369
|
+
|
|
370
|
+
addToCart(input) {
|
|
371
|
+
return cartAction(
|
|
372
|
+
() => client.cart.addItem(input),
|
|
373
|
+
"Failed to add the item to the cart.",
|
|
374
|
+
);
|
|
375
|
+
},
|
|
376
|
+
|
|
377
|
+
updateQuantity(itemId, quantity) {
|
|
378
|
+
// The API treats 0 as a removal; going through `removeItem` keeps that
|
|
379
|
+
// explicit rather than relying on every binding knowing the convention.
|
|
380
|
+
if (quantity <= 0) {
|
|
381
|
+
return cartAction(
|
|
382
|
+
() => client.cart.removeItem(itemId),
|
|
383
|
+
"Failed to remove the item.",
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
return cartAction(
|
|
387
|
+
() => client.cart.updateItem(itemId, { quantity }),
|
|
388
|
+
"Failed to update the quantity.",
|
|
389
|
+
);
|
|
390
|
+
},
|
|
391
|
+
|
|
392
|
+
removeItem(itemId) {
|
|
393
|
+
return cartAction(
|
|
394
|
+
() => client.cart.removeItem(itemId),
|
|
395
|
+
"Failed to remove the item.",
|
|
396
|
+
);
|
|
397
|
+
},
|
|
398
|
+
|
|
399
|
+
applyCoupon(code) {
|
|
400
|
+
return cartAction(
|
|
401
|
+
() => client.cart.applyCoupon({ code }),
|
|
402
|
+
"That coupon could not be applied.",
|
|
403
|
+
);
|
|
404
|
+
},
|
|
405
|
+
|
|
406
|
+
removeCoupon() {
|
|
407
|
+
return cartAction(
|
|
408
|
+
() => client.cart.removeCoupon(),
|
|
409
|
+
"Failed to remove the coupon.",
|
|
410
|
+
);
|
|
411
|
+
},
|
|
412
|
+
|
|
413
|
+
clearCart() {
|
|
414
|
+
return cartAction(() => client.cart.clear(), "Failed to clear the cart.");
|
|
415
|
+
},
|
|
416
|
+
|
|
417
|
+
refreshCart() {
|
|
418
|
+
return cartAction(() => client.cart.get(), "Failed to read the cart.");
|
|
419
|
+
},
|
|
420
|
+
|
|
421
|
+
destroy() {
|
|
422
|
+
destroyed = true;
|
|
423
|
+
subscribers.clear();
|
|
424
|
+
},
|
|
425
|
+
};
|
|
426
|
+
}
|
package/src/store.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@businessdash/sdk/store` — the framework-agnostic storefront + cart core.
|
|
3
|
+
*
|
|
4
|
+
* Bindings shipped as SOURCE (Vue / Svelte / Solid / Qwik / Angular) import
|
|
5
|
+
* through this public subpath rather than a relative path, so the consumer's
|
|
6
|
+
* bundler resolves it exactly as their own code would. `forms` works the same
|
|
7
|
+
* way.
|
|
8
|
+
*
|
|
9
|
+
* @module
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export * from "./store-core/index.js";
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
The company's social profiles as icon links.
|
|
3
|
+
|
|
4
|
+
Every decision that could drift between frameworks — which platforms exist,
|
|
5
|
+
how a handle becomes a URL, where the brand icon comes from — lives in
|
|
6
|
+
`social-core`. This file is only the markup, which is the one part that
|
|
7
|
+
genuinely cannot be shared.
|
|
8
|
+
-->
|
|
9
|
+
<script lang="ts">
|
|
10
|
+
import {
|
|
11
|
+
resolveSocialLinks,
|
|
12
|
+
SOCIAL_FALLBACK_GLYPH,
|
|
13
|
+
SOCIAL_LIST_STYLE_STRING,
|
|
14
|
+
} from "../social-core/index.js";
|
|
15
|
+
|
|
16
|
+
type Props = {
|
|
17
|
+
/** The company profile's `socials` record. */
|
|
18
|
+
socials: unknown;
|
|
19
|
+
/** Icon colour — hex without `#`, or a CSS keyword. */
|
|
20
|
+
color?: string;
|
|
21
|
+
/** Icon edge length in px. */
|
|
22
|
+
size?: number;
|
|
23
|
+
linkClass?: string;
|
|
24
|
+
iconClass?: string;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
let {
|
|
28
|
+
socials,
|
|
29
|
+
color = undefined,
|
|
30
|
+
size = 20,
|
|
31
|
+
linkClass = undefined,
|
|
32
|
+
iconClass = undefined,
|
|
33
|
+
}: Props = $props();
|
|
34
|
+
|
|
35
|
+
const rows = $derived(resolveSocialLinks(socials, { color }));
|
|
36
|
+
</script>
|
|
37
|
+
|
|
38
|
+
<!-- Nothing at all rather than an empty <ul> in a footer. -->
|
|
39
|
+
{#if rows.length}
|
|
40
|
+
<ul style={SOCIAL_LIST_STYLE_STRING}>
|
|
41
|
+
{#each rows as row (row.key)}
|
|
42
|
+
<li>
|
|
43
|
+
<a
|
|
44
|
+
href={row.url}
|
|
45
|
+
target="_blank"
|
|
46
|
+
rel="noopener noreferrer"
|
|
47
|
+
title={row.label}
|
|
48
|
+
aria-label={row.label}
|
|
49
|
+
class={linkClass}
|
|
50
|
+
>
|
|
51
|
+
{#if row.iconUrl}
|
|
52
|
+
<img
|
|
53
|
+
src={row.iconUrl}
|
|
54
|
+
alt={row.label}
|
|
55
|
+
width={size}
|
|
56
|
+
height={size}
|
|
57
|
+
loading="lazy"
|
|
58
|
+
class={iconClass}
|
|
59
|
+
/>
|
|
60
|
+
{:else}
|
|
61
|
+
<svg
|
|
62
|
+
viewBox={SOCIAL_FALLBACK_GLYPH.viewBox}
|
|
63
|
+
width={size}
|
|
64
|
+
height={size}
|
|
65
|
+
fill="none"
|
|
66
|
+
stroke="currentColor"
|
|
67
|
+
stroke-width={SOCIAL_FALLBACK_GLYPH.strokeWidth}
|
|
68
|
+
aria-hidden="true"
|
|
69
|
+
class={iconClass}
|
|
70
|
+
>
|
|
71
|
+
<title>{row.label}</title>
|
|
72
|
+
<circle
|
|
73
|
+
cx={SOCIAL_FALLBACK_GLYPH.circle.cx}
|
|
74
|
+
cy={SOCIAL_FALLBACK_GLYPH.circle.cy}
|
|
75
|
+
r={SOCIAL_FALLBACK_GLYPH.circle.r}
|
|
76
|
+
/>
|
|
77
|
+
<path d={SOCIAL_FALLBACK_GLYPH.path} />
|
|
78
|
+
</svg>
|
|
79
|
+
{/if}
|
|
80
|
+
</a>
|
|
81
|
+
</li>
|
|
82
|
+
{/each}
|
|
83
|
+
</ul>
|
|
84
|
+
{/if}
|