@patientos/website-kit 0.2.6 → 0.2.7
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 +54 -2
- package/dist/booking-block-D-UOAPx3.d.ts +74 -0
- package/dist/cart-block-B-ytc9us.d.ts +20 -0
- package/dist/cart-button-BCLFittB.d.ts +24 -0
- package/dist/cart-storage.d.ts +110 -0
- package/dist/cart-storage.js +39 -0
- package/dist/checkout-block-BeXwpqYe.d.ts +24 -0
- package/dist/chunk-2CZFJOKK.js +79 -0
- package/dist/{chunk-ZYX4TXBN.js → chunk-3T7UWKAH.js} +4 -310
- package/dist/{chunk-7ZOQ6UKG.js → chunk-4GKSSM5N.js} +578 -3163
- package/dist/chunk-4NF7SSIX.js +367 -0
- package/dist/chunk-673YEXAI.js +164 -0
- package/dist/chunk-7BUDUYXN.js +80 -0
- package/dist/chunk-FZ4WKWIE.js +310 -0
- package/dist/{chunk-ZZFBTLR4.js → chunk-HYXTDUP2.js} +20 -5
- package/dist/chunk-IOC6QLB7.js +159 -0
- package/dist/chunk-S3ZQQOQT.js +2057 -0
- package/dist/chunk-WQSJ46TP.js +19 -0
- package/dist/chunk-ZNH6TSYP.js +167 -0
- package/dist/index.d.ts +12 -32
- package/dist/index.js +88 -26
- package/dist/islands-impl/cart-button.d.ts +19 -0
- package/dist/islands-impl/cart-button.js +13 -0
- package/dist/islands-impl/cart.d.ts +31 -0
- package/dist/islands-impl/cart.js +11 -0
- package/dist/islands-impl/checkout.d.ts +6 -0
- package/dist/islands-impl/checkout.js +13 -0
- package/dist/islands-impl/store.d.ts +22 -0
- package/dist/islands-impl/store.js +14 -0
- package/dist/islands-impl.d.ts +14 -10
- package/dist/islands-impl.js +22 -7
- package/dist/islands-registry.js +13 -4
- package/dist/{portal-account-8uQc_Ncx.d.ts → portal-account-DRJyr3nz.d.ts} +1 -82
- package/dist/{portal-account.client-BBe_q9yC.d.ts → portal-account.client-Bke4NsgW.d.ts} +2 -1
- package/dist/portal-booking-client.d.ts +3 -2
- package/dist/portal-booking-client.js +2 -1
- package/dist/portal-client-my3q5GME.d.ts +83 -0
- package/dist/store-block-CFxnsfKU.d.ts +37 -0
- package/dist/store-catalog-p7TFOpfk.d.ts +78 -0
- package/dist/store-catalog.d.ts +3 -0
- package/dist/store-catalog.js +11 -0
- package/dist/website-kit.css +193 -0
- package/package.json +31 -1
- package/dist/checkout-block-BdpPIuQ5.d.ts +0 -136
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
// src/cart-storage.ts
|
|
2
|
+
var CART_STORAGE_KEY = "patientos.cart.v1";
|
|
3
|
+
var MAX_LINES = 50;
|
|
4
|
+
var MAX_QUANTITY = 99;
|
|
5
|
+
function emptyCart() {
|
|
6
|
+
return { v: 1, lines: [] };
|
|
7
|
+
}
|
|
8
|
+
var EMPTY_CART = Object.freeze({
|
|
9
|
+
v: 1,
|
|
10
|
+
lines: Object.freeze([])
|
|
11
|
+
});
|
|
12
|
+
function cartLineId(value) {
|
|
13
|
+
if (typeof value !== "string") return null;
|
|
14
|
+
const normalized = value.trim();
|
|
15
|
+
return normalized.length > 0 && normalized.length <= 128 ? normalized : null;
|
|
16
|
+
}
|
|
17
|
+
function legacyCartLineId(kind, variantId) {
|
|
18
|
+
const input = `${kind}\0${variantId}`;
|
|
19
|
+
let first = 2166136261;
|
|
20
|
+
let second = 2246822507;
|
|
21
|
+
for (let index = 0; index < input.length; index += 1) {
|
|
22
|
+
const code = input.charCodeAt(index);
|
|
23
|
+
first = Math.imul(first ^ code, 16777619);
|
|
24
|
+
second = Math.imul(second ^ code, 3266489909);
|
|
25
|
+
}
|
|
26
|
+
return `legacy-${(first >>> 0).toString(16).padStart(8, "0")}${(second >>> 0).toString(16).padStart(8, "0")}`;
|
|
27
|
+
}
|
|
28
|
+
function newCartLineId() {
|
|
29
|
+
return crypto.randomUUID();
|
|
30
|
+
}
|
|
31
|
+
function storage() {
|
|
32
|
+
try {
|
|
33
|
+
if (typeof window === "undefined" || !window.localStorage) return null;
|
|
34
|
+
return window.localStorage;
|
|
35
|
+
} catch {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function parseLine(raw) {
|
|
40
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
|
|
41
|
+
const line = raw;
|
|
42
|
+
const variantId = typeof line.variantId === "string" ? line.variantId.trim() : "";
|
|
43
|
+
if (!variantId) return null;
|
|
44
|
+
const kind = line.kind === "retail" ? "retail" : "fill";
|
|
45
|
+
const n = typeof line.quantity === "number" ? line.quantity : Number(line.quantity);
|
|
46
|
+
if (!Number.isFinite(n)) return null;
|
|
47
|
+
const quantity = Math.min(Math.max(Math.floor(n), 1), MAX_QUANTITY);
|
|
48
|
+
const lineId = cartLineId(line.lineId) ?? legacyCartLineId(kind, variantId);
|
|
49
|
+
return { lineId, kind, variantId, quantity };
|
|
50
|
+
}
|
|
51
|
+
function normalizeCartLines(entries) {
|
|
52
|
+
const lines = [];
|
|
53
|
+
const byIdentity = /* @__PURE__ */ new Map();
|
|
54
|
+
for (const entry of entries) {
|
|
55
|
+
const line = parseLine(entry);
|
|
56
|
+
if (!line) continue;
|
|
57
|
+
const identity = `${line.kind}\0${line.variantId}`;
|
|
58
|
+
const existing = byIdentity.get(identity);
|
|
59
|
+
if (existing) {
|
|
60
|
+
existing.quantity = Math.min(
|
|
61
|
+
existing.quantity + line.quantity,
|
|
62
|
+
MAX_QUANTITY
|
|
63
|
+
);
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (lines.length >= MAX_LINES) continue;
|
|
67
|
+
lines.push(line);
|
|
68
|
+
byIdentity.set(identity, line);
|
|
69
|
+
}
|
|
70
|
+
return lines;
|
|
71
|
+
}
|
|
72
|
+
function normalizeSettledAttemptTokens(entries) {
|
|
73
|
+
if (!Array.isArray(entries)) return [];
|
|
74
|
+
return Array.from(
|
|
75
|
+
new Set(
|
|
76
|
+
entries.filter(
|
|
77
|
+
(entry) => typeof entry === "string" && entry.length > 0 && entry.length <= 128
|
|
78
|
+
)
|
|
79
|
+
)
|
|
80
|
+
).slice(-32);
|
|
81
|
+
}
|
|
82
|
+
function readCart() {
|
|
83
|
+
const store = storage();
|
|
84
|
+
if (!store) return emptyCart();
|
|
85
|
+
let raw;
|
|
86
|
+
try {
|
|
87
|
+
raw = store.getItem(CART_STORAGE_KEY);
|
|
88
|
+
} catch {
|
|
89
|
+
return emptyCart();
|
|
90
|
+
}
|
|
91
|
+
if (!raw) return emptyCart();
|
|
92
|
+
try {
|
|
93
|
+
const parsed = JSON.parse(raw);
|
|
94
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
95
|
+
return emptyCart();
|
|
96
|
+
const bag = parsed;
|
|
97
|
+
if (bag.v !== 1 || !Array.isArray(bag.lines)) return emptyCart();
|
|
98
|
+
const settledAttemptTokens = normalizeSettledAttemptTokens(
|
|
99
|
+
bag.settledAttemptTokens
|
|
100
|
+
);
|
|
101
|
+
return {
|
|
102
|
+
v: 1,
|
|
103
|
+
lines: normalizeCartLines(bag.lines),
|
|
104
|
+
...settledAttemptTokens.length > 0 ? { settledAttemptTokens } : {}
|
|
105
|
+
};
|
|
106
|
+
} catch {
|
|
107
|
+
return emptyCart();
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
var CART_MUTATION_LOCK = "patientos-cart-mutation-v1";
|
|
111
|
+
var CART_MUTATION_VALIDATION_TIMEOUT_MS = 5e3;
|
|
112
|
+
function persistCart(cart) {
|
|
113
|
+
const settledAttemptTokens = normalizeSettledAttemptTokens(
|
|
114
|
+
cart.settledAttemptTokens
|
|
115
|
+
);
|
|
116
|
+
const normalised = {
|
|
117
|
+
v: 1,
|
|
118
|
+
lines: normalizeCartLines(cart.lines),
|
|
119
|
+
...settledAttemptTokens.length > 0 ? { settledAttemptTokens } : {}
|
|
120
|
+
};
|
|
121
|
+
const store = storage();
|
|
122
|
+
if (!store)
|
|
123
|
+
return {
|
|
124
|
+
cart: readCart(),
|
|
125
|
+
persisted: false,
|
|
126
|
+
status: "cart_storage_unavailable"
|
|
127
|
+
};
|
|
128
|
+
try {
|
|
129
|
+
store.setItem(CART_STORAGE_KEY, JSON.stringify(normalised));
|
|
130
|
+
} catch {
|
|
131
|
+
return {
|
|
132
|
+
cart: readCart(),
|
|
133
|
+
persisted: false,
|
|
134
|
+
status: "cart_storage_unavailable"
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
notify(normalised);
|
|
138
|
+
return { cart: normalised, persisted: true, status: "updated" };
|
|
139
|
+
}
|
|
140
|
+
function unavailableMutation() {
|
|
141
|
+
return {
|
|
142
|
+
cart: readCart(),
|
|
143
|
+
persisted: false,
|
|
144
|
+
status: "cart_storage_unavailable"
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
async function mutateCart(mutation, beforePersist) {
|
|
148
|
+
const locks = typeof navigator === "undefined" ? void 0 : navigator.locks;
|
|
149
|
+
if (!storage() || !locks) return unavailableMutation();
|
|
150
|
+
try {
|
|
151
|
+
return await locks.request(CART_MUTATION_LOCK, async () => {
|
|
152
|
+
const latest = readCart();
|
|
153
|
+
const mutated = mutation(latest);
|
|
154
|
+
const proposed = {
|
|
155
|
+
...mutated,
|
|
156
|
+
lines: normalizeCartLines(mutated.lines),
|
|
157
|
+
...mutated.settledAttemptTokens === void 0 && latest.settledAttemptTokens ? { settledAttemptTokens: latest.settledAttemptTokens } : {}
|
|
158
|
+
};
|
|
159
|
+
if (beforePersist) {
|
|
160
|
+
const controller = new AbortController();
|
|
161
|
+
let timer;
|
|
162
|
+
const accepted = await Promise.race([
|
|
163
|
+
beforePersist(proposed, controller.signal).catch(() => false),
|
|
164
|
+
new Promise((resolve) => {
|
|
165
|
+
timer = setTimeout(() => {
|
|
166
|
+
controller.abort();
|
|
167
|
+
resolve(false);
|
|
168
|
+
}, CART_MUTATION_VALIDATION_TIMEOUT_MS);
|
|
169
|
+
})
|
|
170
|
+
]);
|
|
171
|
+
if (timer) clearTimeout(timer);
|
|
172
|
+
if (!accepted) {
|
|
173
|
+
return {
|
|
174
|
+
cart: readCart(),
|
|
175
|
+
persisted: false,
|
|
176
|
+
status: "not_committed"
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return persistCart(proposed);
|
|
181
|
+
});
|
|
182
|
+
} catch {
|
|
183
|
+
return unavailableMutation();
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
async function withCartLock(operation) {
|
|
187
|
+
const locks = typeof navigator === "undefined" ? void 0 : navigator.locks;
|
|
188
|
+
if (!storage() || !locks)
|
|
189
|
+
return { status: "cart_storage_unavailable", cart: readCart() };
|
|
190
|
+
try {
|
|
191
|
+
return await locks.request(CART_MUTATION_LOCK, async () => {
|
|
192
|
+
const latest = readCart();
|
|
193
|
+
const controller = new AbortController();
|
|
194
|
+
let timer;
|
|
195
|
+
const outcome = await Promise.race([
|
|
196
|
+
operation(latest, controller.signal).then((result) => ({
|
|
197
|
+
completed: true,
|
|
198
|
+
result
|
|
199
|
+
})),
|
|
200
|
+
new Promise((resolve) => {
|
|
201
|
+
timer = setTimeout(() => {
|
|
202
|
+
controller.abort();
|
|
203
|
+
resolve({ completed: false });
|
|
204
|
+
}, CART_MUTATION_VALIDATION_TIMEOUT_MS);
|
|
205
|
+
})
|
|
206
|
+
]);
|
|
207
|
+
if (timer) clearTimeout(timer);
|
|
208
|
+
return outcome.completed ? { status: "completed", cart: latest, result: outcome.result } : { status: "not_completed", cart: latest };
|
|
209
|
+
});
|
|
210
|
+
} catch {
|
|
211
|
+
return { status: "not_completed", cart: readCart() };
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
async function writeCart(cart) {
|
|
215
|
+
return mutateCart(() => cart);
|
|
216
|
+
}
|
|
217
|
+
async function addToCart(variantId, quantity = 1) {
|
|
218
|
+
let status = "added";
|
|
219
|
+
const result = await mutateCart((cart) => {
|
|
220
|
+
const add = Math.min(Math.max(Math.floor(quantity), 1), MAX_QUANTITY);
|
|
221
|
+
const existing = cart.lines.find(
|
|
222
|
+
(line) => line.kind === "retail" && line.variantId === variantId
|
|
223
|
+
);
|
|
224
|
+
if (existing) {
|
|
225
|
+
if (existing.quantity >= MAX_QUANTITY) {
|
|
226
|
+
status = "quantity_limit";
|
|
227
|
+
return cart;
|
|
228
|
+
}
|
|
229
|
+
existing.quantity = Math.min(existing.quantity + add, MAX_QUANTITY);
|
|
230
|
+
} else {
|
|
231
|
+
if (cart.lines.length >= MAX_LINES) {
|
|
232
|
+
status = "line_limit";
|
|
233
|
+
return cart;
|
|
234
|
+
}
|
|
235
|
+
cart.lines.push({
|
|
236
|
+
lineId: newCartLineId(),
|
|
237
|
+
kind: "retail",
|
|
238
|
+
variantId,
|
|
239
|
+
quantity: add
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
return cart;
|
|
243
|
+
});
|
|
244
|
+
return {
|
|
245
|
+
cart: result.cart,
|
|
246
|
+
status: result.status === "cart_storage_unavailable" ? result.status : status
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
async function setCartQuantity(kind, variantId, quantity) {
|
|
250
|
+
const result = await mutateCart((cart) => {
|
|
251
|
+
const next = Math.floor(quantity);
|
|
252
|
+
return {
|
|
253
|
+
v: 1,
|
|
254
|
+
lines: next < 1 ? cart.lines.filter(
|
|
255
|
+
(line) => line.kind !== kind || line.variantId !== variantId
|
|
256
|
+
) : cart.lines.map(
|
|
257
|
+
(line) => line.kind === kind && line.variantId === variantId ? { ...line, quantity: Math.min(next, MAX_QUANTITY) } : line
|
|
258
|
+
)
|
|
259
|
+
};
|
|
260
|
+
});
|
|
261
|
+
return result;
|
|
262
|
+
}
|
|
263
|
+
async function adjustCartQuantity(kind, variantId, delta, maximumBaseline) {
|
|
264
|
+
const result = await mutateCart((cart) => ({
|
|
265
|
+
v: 1,
|
|
266
|
+
lines: cart.lines.flatMap((line) => {
|
|
267
|
+
if (line.kind !== kind || line.variantId !== variantId) return [line];
|
|
268
|
+
const baseline = maximumBaseline === void 0 ? line.quantity : Math.min(line.quantity, Math.max(0, Math.floor(maximumBaseline)));
|
|
269
|
+
const quantity = Math.min(baseline + Math.floor(delta), MAX_QUANTITY);
|
|
270
|
+
return quantity > 0 ? [{ ...line, quantity }] : [];
|
|
271
|
+
})
|
|
272
|
+
}));
|
|
273
|
+
return result;
|
|
274
|
+
}
|
|
275
|
+
async function removeFromCart(variantId, kind) {
|
|
276
|
+
const result = await mutateCart((cart) => ({
|
|
277
|
+
v: 1,
|
|
278
|
+
lines: cart.lines.filter(
|
|
279
|
+
(line) => line.variantId !== variantId || kind && line.kind !== kind
|
|
280
|
+
)
|
|
281
|
+
}));
|
|
282
|
+
return result;
|
|
283
|
+
}
|
|
284
|
+
async function clearCart() {
|
|
285
|
+
return writeCart(emptyCart());
|
|
286
|
+
}
|
|
287
|
+
async function subtractPurchasedCartLines(purchasedLines, attemptToken) {
|
|
288
|
+
const purchased = new Map(
|
|
289
|
+
normalizeCartLines(
|
|
290
|
+
purchasedLines.filter(
|
|
291
|
+
(line) => Boolean(line) && typeof line === "object" && !Array.isArray(line) && cartLineId(line.lineId) !== null
|
|
292
|
+
)
|
|
293
|
+
).map((line) => [line.lineId, line])
|
|
294
|
+
);
|
|
295
|
+
return mutateCart((cart) => {
|
|
296
|
+
if (attemptToken && cart.settledAttemptTokens?.includes(attemptToken))
|
|
297
|
+
return cart;
|
|
298
|
+
return {
|
|
299
|
+
v: 1,
|
|
300
|
+
lines: cart.lines.flatMap((line) => {
|
|
301
|
+
const purchasedLine = purchased.get(line.lineId);
|
|
302
|
+
const purchasedQuantity = purchasedLine?.kind === line.kind && purchasedLine.variantId === line.variantId ? purchasedLine.quantity : 0;
|
|
303
|
+
const quantity = line.quantity - purchasedQuantity;
|
|
304
|
+
return quantity > 0 ? [{ ...line, quantity }] : [];
|
|
305
|
+
}),
|
|
306
|
+
...attemptToken ? {
|
|
307
|
+
settledAttemptTokens: [
|
|
308
|
+
...cart.settledAttemptTokens ?? [],
|
|
309
|
+
attemptToken
|
|
310
|
+
]
|
|
311
|
+
} : {}
|
|
312
|
+
};
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
function cartItemCount(cart = readCart()) {
|
|
316
|
+
return cart.lines.reduce((sum, l) => sum + l.quantity, 0);
|
|
317
|
+
}
|
|
318
|
+
function cartToWireLines(cart) {
|
|
319
|
+
return cart.lines.map((l) => ({
|
|
320
|
+
kind: l.kind,
|
|
321
|
+
variantId: l.variantId,
|
|
322
|
+
quantity: l.quantity
|
|
323
|
+
}));
|
|
324
|
+
}
|
|
325
|
+
var listeners = /* @__PURE__ */ new Set();
|
|
326
|
+
function notify(cart) {
|
|
327
|
+
for (const fn of listeners) {
|
|
328
|
+
try {
|
|
329
|
+
fn(cart);
|
|
330
|
+
} catch {
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
function subscribeToCart(fn) {
|
|
335
|
+
listeners.add(fn);
|
|
336
|
+
const onStorage = (e) => {
|
|
337
|
+
if (e.key !== null && e.key !== CART_STORAGE_KEY) return;
|
|
338
|
+
fn(readCart());
|
|
339
|
+
};
|
|
340
|
+
if (typeof window !== "undefined")
|
|
341
|
+
window.addEventListener("storage", onStorage);
|
|
342
|
+
return () => {
|
|
343
|
+
listeners.delete(fn);
|
|
344
|
+
if (typeof window !== "undefined")
|
|
345
|
+
window.removeEventListener("storage", onStorage);
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
export {
|
|
350
|
+
CART_STORAGE_KEY,
|
|
351
|
+
emptyCart,
|
|
352
|
+
EMPTY_CART,
|
|
353
|
+
normalizeCartLines,
|
|
354
|
+
readCart,
|
|
355
|
+
mutateCart,
|
|
356
|
+
withCartLock,
|
|
357
|
+
writeCart,
|
|
358
|
+
addToCart,
|
|
359
|
+
setCartQuantity,
|
|
360
|
+
adjustCartQuantity,
|
|
361
|
+
removeFromCart,
|
|
362
|
+
clearCart,
|
|
363
|
+
subtractPurchasedCartLines,
|
|
364
|
+
cartItemCount,
|
|
365
|
+
cartToWireLines,
|
|
366
|
+
subscribeToCart
|
|
367
|
+
};
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import {
|
|
2
|
+
normalizeCatalogResponse,
|
|
3
|
+
storeCatalogPath
|
|
4
|
+
} from "./chunk-WQSJ46TP.js";
|
|
5
|
+
import {
|
|
6
|
+
createStoreClient,
|
|
7
|
+
formatMoney
|
|
8
|
+
} from "./chunk-7BUDUYXN.js";
|
|
9
|
+
import {
|
|
10
|
+
addToCart,
|
|
11
|
+
cartItemCount,
|
|
12
|
+
readCart,
|
|
13
|
+
subscribeToCart
|
|
14
|
+
} from "./chunk-4NF7SSIX.js";
|
|
15
|
+
|
|
16
|
+
// src/store-block.client.tsx
|
|
17
|
+
import * as React from "react";
|
|
18
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
19
|
+
var SKELETON_CARDS = 6;
|
|
20
|
+
function StoreClient({
|
|
21
|
+
categoryHandle,
|
|
22
|
+
productHandle,
|
|
23
|
+
storeApiOrigin,
|
|
24
|
+
title,
|
|
25
|
+
columns,
|
|
26
|
+
hideCartLink,
|
|
27
|
+
initialCatalog
|
|
28
|
+
}) {
|
|
29
|
+
const client = React.useMemo(
|
|
30
|
+
() => createStoreClient({ apiOrigin: storeApiOrigin }),
|
|
31
|
+
[storeApiOrigin]
|
|
32
|
+
);
|
|
33
|
+
const [catalog, setCatalog] = React.useState(
|
|
34
|
+
initialCatalog ?? null
|
|
35
|
+
);
|
|
36
|
+
const [failed, setFailed] = React.useState(false);
|
|
37
|
+
const [count, setCount] = React.useState(0);
|
|
38
|
+
React.useEffect(() => {
|
|
39
|
+
setCount(cartItemCount(readCart()));
|
|
40
|
+
return subscribeToCart((cart) => setCount(cartItemCount(cart)));
|
|
41
|
+
}, []);
|
|
42
|
+
const hasSeed = initialCatalog != null;
|
|
43
|
+
React.useEffect(() => {
|
|
44
|
+
let alive = true;
|
|
45
|
+
void client.get(storeCatalogPath(productHandle)).then((result) => {
|
|
46
|
+
if (!alive) return;
|
|
47
|
+
if (!result.ok) {
|
|
48
|
+
setFailed((previous) => hasSeed ? previous : true);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
setCatalog(normalizeCatalogResponse(result.data));
|
|
52
|
+
setFailed(false);
|
|
53
|
+
});
|
|
54
|
+
return () => {
|
|
55
|
+
alive = false;
|
|
56
|
+
};
|
|
57
|
+
}, [client, productHandle, hasSeed]);
|
|
58
|
+
const products = React.useMemo(() => {
|
|
59
|
+
if (!catalog) return [];
|
|
60
|
+
if (productHandle || !categoryHandle) return catalog.products;
|
|
61
|
+
return catalog.products.filter(
|
|
62
|
+
(product) => product.categories.some((category) => category.handle === categoryHandle)
|
|
63
|
+
);
|
|
64
|
+
}, [catalog, categoryHandle, productHandle]);
|
|
65
|
+
if (failed && !catalog) {
|
|
66
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
67
|
+
/* @__PURE__ */ jsx("h2", { className: "sk-store__heading", children: title ?? "Shop" }),
|
|
68
|
+
/* @__PURE__ */ jsx("p", { className: "sk-store__placeholder", role: "note", children: "Our shop is unavailable right now. Please contact the clinic to order." })
|
|
69
|
+
] });
|
|
70
|
+
}
|
|
71
|
+
const gridStyle = columns ? { "--sk-store-columns": columns } : void 0;
|
|
72
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
73
|
+
/* @__PURE__ */ jsxs("div", { className: "sk-store__header", children: [
|
|
74
|
+
/* @__PURE__ */ jsx("h2", { className: "sk-store__heading", children: title ?? "Shop" }),
|
|
75
|
+
hideCartLink ? null : /* @__PURE__ */ jsxs("a", { className: "sk-store__cart-link", href: "/cart", children: [
|
|
76
|
+
"Cart",
|
|
77
|
+
count > 0 ? ` (${count})` : ""
|
|
78
|
+
] })
|
|
79
|
+
] }),
|
|
80
|
+
!catalog ? (
|
|
81
|
+
// A fixed-height skeleton grid, not a line of text: the text-then-grid swap is
|
|
82
|
+
// what made the shop jump on every load.
|
|
83
|
+
/* @__PURE__ */ jsxs(Fragment, { children: [
|
|
84
|
+
/* @__PURE__ */ jsx("p", { className: "sk-store__placeholder sk-store__placeholder--sr", role: "status", children: "Loading the shop\u2026" }),
|
|
85
|
+
/* @__PURE__ */ jsx("ul", { className: "sk-store__grid", style: gridStyle, "aria-hidden": true, children: Array.from({ length: SKELETON_CARDS }, (_, index) => /* @__PURE__ */ jsxs("li", { className: "sk-store__card sk-store__card--skeleton", children: [
|
|
86
|
+
/* @__PURE__ */ jsx("span", { className: "sk-store__image sk-store__image--empty" }),
|
|
87
|
+
/* @__PURE__ */ jsx("span", { className: "sk-store__skeleton-line" }),
|
|
88
|
+
/* @__PURE__ */ jsx("span", { className: "sk-store__skeleton-line sk-store__skeleton-line--short" }),
|
|
89
|
+
/* @__PURE__ */ jsx("span", { className: "sk-store__skeleton-button" })
|
|
90
|
+
] }, index)) })
|
|
91
|
+
] })
|
|
92
|
+
) : products.length === 0 ? /* @__PURE__ */ jsx("p", { className: "sk-store__placeholder", role: "status", children: "Nothing is available to order online just now." }) : /* @__PURE__ */ jsx("ul", { className: "sk-store__grid", style: gridStyle, children: products.map((product) => /* @__PURE__ */ jsx(StoreCard, { product }, product.id)) })
|
|
93
|
+
] });
|
|
94
|
+
}
|
|
95
|
+
function StoreCard({ product }) {
|
|
96
|
+
const [variantId, setVariantId] = React.useState(
|
|
97
|
+
() => (product.variants.find((v) => v.inStock) ?? product.variants[0]).id
|
|
98
|
+
);
|
|
99
|
+
const [addFeedback, setAddFeedback] = React.useState(null);
|
|
100
|
+
const variant = product.variants.find((v) => v.id === variantId) ?? product.variants[0];
|
|
101
|
+
const image = product.thumbnailUrl ?? product.images[0]?.url ?? null;
|
|
102
|
+
async function onAdd() {
|
|
103
|
+
const result = await addToCart(variant.id, 1);
|
|
104
|
+
setAddFeedback(result.status);
|
|
105
|
+
window.setTimeout(() => setAddFeedback(null), 1600);
|
|
106
|
+
}
|
|
107
|
+
return /* @__PURE__ */ jsxs("li", { className: "sk-store__card", children: [
|
|
108
|
+
/* @__PURE__ */ jsxs("a", { className: "sk-store__card-link", href: `/store/${product.handle}`, children: [
|
|
109
|
+
image ? (
|
|
110
|
+
// Square by contract (CSS `aspect-ratio` AND the intrinsic ratio in the
|
|
111
|
+
// markup), lazy, and decoded off the main thread: the card must hold its
|
|
112
|
+
// height before the bytes arrive or the whole grid reflows around it.
|
|
113
|
+
/* @__PURE__ */ jsx(
|
|
114
|
+
"img",
|
|
115
|
+
{
|
|
116
|
+
className: "sk-store__image",
|
|
117
|
+
src: image,
|
|
118
|
+
alt: product.images[0]?.alt ?? "",
|
|
119
|
+
width: 600,
|
|
120
|
+
height: 600,
|
|
121
|
+
loading: "lazy",
|
|
122
|
+
decoding: "async"
|
|
123
|
+
}
|
|
124
|
+
)
|
|
125
|
+
) : /* @__PURE__ */ jsx(
|
|
126
|
+
"span",
|
|
127
|
+
{
|
|
128
|
+
className: "sk-store__image sk-store__image--empty",
|
|
129
|
+
"aria-hidden": true
|
|
130
|
+
}
|
|
131
|
+
),
|
|
132
|
+
/* @__PURE__ */ jsx("span", { className: "sk-store__title", children: product.title })
|
|
133
|
+
] }),
|
|
134
|
+
/* @__PURE__ */ jsxs("span", { className: "sk-store__price", children: [
|
|
135
|
+
formatMoney(variant.price),
|
|
136
|
+
product.gstApplicable ? /* @__PURE__ */ jsx("span", { className: "sk-store__gst", children: " incl. GST" }) : null
|
|
137
|
+
] }),
|
|
138
|
+
product.variants.length > 1 ? /* @__PURE__ */ jsxs("label", { className: "sk-store__variant", children: [
|
|
139
|
+
/* @__PURE__ */ jsx("span", { className: "sk-store__variant-label", children: "Option" }),
|
|
140
|
+
/* @__PURE__ */ jsx(
|
|
141
|
+
"select",
|
|
142
|
+
{
|
|
143
|
+
value: variantId,
|
|
144
|
+
onChange: (e) => setVariantId(e.target.value),
|
|
145
|
+
children: product.variants.map((v) => /* @__PURE__ */ jsx("option", { value: v.id, disabled: !v.inStock, children: (v.options.length ? v.options.join(" / ") : v.title) + (v.inStock ? "" : " \u2014 sold out") }, v.id))
|
|
146
|
+
}
|
|
147
|
+
)
|
|
148
|
+
] }) : null,
|
|
149
|
+
/* @__PURE__ */ jsx(
|
|
150
|
+
"button",
|
|
151
|
+
{
|
|
152
|
+
type: "button",
|
|
153
|
+
className: "sk-store__add",
|
|
154
|
+
onClick: () => void onAdd(),
|
|
155
|
+
disabled: !variant.inStock,
|
|
156
|
+
children: !variant.inStock ? "Sold out" : addFeedback === "added" ? "Added \u2713" : addFeedback === "line_limit" ? "Cart full \u2014 50 products max" : addFeedback === "quantity_limit" ? "Maximum 99 in cart" : addFeedback === "cart_storage_unavailable" ? "This browser can\u2019t safely update the cart" : "Add to cart"
|
|
157
|
+
}
|
|
158
|
+
)
|
|
159
|
+
] });
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export {
|
|
163
|
+
StoreClient
|
|
164
|
+
};
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import {
|
|
2
|
+
encodePortalReturnTarget,
|
|
3
|
+
normalizePortalApiOrigin
|
|
4
|
+
} from "./chunk-FZ4WKWIE.js";
|
|
5
|
+
|
|
6
|
+
// src/store-client.ts
|
|
7
|
+
var STORE_REQUEST_INIT = {
|
|
8
|
+
credentials: "include",
|
|
9
|
+
cache: "no-store"
|
|
10
|
+
};
|
|
11
|
+
async function readResult(response) {
|
|
12
|
+
let body;
|
|
13
|
+
try {
|
|
14
|
+
body = await response.json();
|
|
15
|
+
} catch {
|
|
16
|
+
return response.ok ? { ok: false, status: null, error: null, message: null } : { ok: false, status: response.status, error: null, message: null };
|
|
17
|
+
}
|
|
18
|
+
if (response.ok) return { ok: true, data: body };
|
|
19
|
+
const failure = body;
|
|
20
|
+
return {
|
|
21
|
+
ok: false,
|
|
22
|
+
status: response.status,
|
|
23
|
+
error: typeof failure?.error === "string" ? failure.error : null,
|
|
24
|
+
message: typeof failure?.message === "string" ? failure.message : null
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
function createStoreClient(config = {}) {
|
|
28
|
+
const rawOrigin = config.apiOrigin?.trim() ?? "";
|
|
29
|
+
const normalizedOrigin = rawOrigin ? normalizePortalApiOrigin(rawOrigin) : "";
|
|
30
|
+
if (rawOrigin && !normalizedOrigin) {
|
|
31
|
+
console.error("[store] ignoring a malformed store API origin", rawOrigin);
|
|
32
|
+
}
|
|
33
|
+
const apiOrigin = normalizedOrigin ?? "";
|
|
34
|
+
const fetchImpl = config.fetchImpl ?? ((input, init) => globalThis.fetch(input, init));
|
|
35
|
+
const url = (path) => apiOrigin && path.startsWith("/") ? apiOrigin + path : path;
|
|
36
|
+
async function request(path, init) {
|
|
37
|
+
try {
|
|
38
|
+
return await readResult(
|
|
39
|
+
await fetchImpl(url(path), {
|
|
40
|
+
...STORE_REQUEST_INIT,
|
|
41
|
+
...init
|
|
42
|
+
})
|
|
43
|
+
);
|
|
44
|
+
} catch {
|
|
45
|
+
return { ok: false, status: null, error: null, message: null };
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
apiOrigin,
|
|
50
|
+
url,
|
|
51
|
+
get: (path) => request(path, { headers: { accept: "application/json" } }),
|
|
52
|
+
post: (path, body, options) => request(path, {
|
|
53
|
+
signal: options?.signal,
|
|
54
|
+
method: "POST",
|
|
55
|
+
headers: {
|
|
56
|
+
accept: "application/json",
|
|
57
|
+
"content-type": "application/json"
|
|
58
|
+
},
|
|
59
|
+
body: JSON.stringify(body ?? {})
|
|
60
|
+
}),
|
|
61
|
+
signInHref(returnPath = "/checkout") {
|
|
62
|
+
if (typeof window === "undefined") return `${apiOrigin}/portal/sign-in`;
|
|
63
|
+
const target = new URL(returnPath, window.location.origin).toString();
|
|
64
|
+
const redirect = `/portal/return?to=${encodePortalReturnTarget(target)}`;
|
|
65
|
+
return `${apiOrigin}/portal/sign-in?redirect=${encodeURIComponent(redirect)}`;
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// src/money.ts
|
|
71
|
+
function formatMoney(value) {
|
|
72
|
+
const n = Number(value);
|
|
73
|
+
if (!Number.isFinite(n)) return value;
|
|
74
|
+
return `$${n.toFixed(2)}`;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export {
|
|
78
|
+
createStoreClient,
|
|
79
|
+
formatMoney
|
|
80
|
+
};
|