@cimplify/sdk 0.73.0 → 0.75.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/dist/advanced.d.mts +3 -3
- package/dist/advanced.d.ts +3 -3
- package/dist/{client-BvqaIlJ0.d.ts → client--w53owD4.d.ts} +2 -2
- package/dist/{client-C7EMuGoz.d.mts → client-D7uHF_jh.d.mts} +2 -2
- package/dist/{client-D6CZ2Ggp.d.ts → client-xDpke-KS.d.ts} +2 -2
- package/dist/{client-DWA_XIh3.d.mts → client-yFghjc1-.d.mts} +2 -2
- package/dist/{index-DIIlPUOC.d.ts → index-DTGS0QC1.d.ts} +3 -3
- package/dist/{index-D8vnKlOQ.d.mts → index-lt-fe29V.d.mts} +3 -3
- package/dist/index.d.mts +6 -6
- package/dist/index.d.ts +6 -6
- package/dist/{payment-DVS7ZUEp.d.mts → payment-C-2gApyQ.d.mts} +1 -1
- package/dist/{payment-sn-yGL7v.d.ts → payment-CfemTORM.d.ts} +1 -1
- package/dist/{price-Cm0tU0V5.d.mts → price-CZBQw-Rh.d.mts} +1 -1
- package/dist/{price-9T1Y47oS.d.ts → price-DNeyoO7y.d.ts} +1 -1
- package/dist/{product-B_kS4Oxa.d.mts → product-_GFoaPHU.d.mts} +2 -0
- package/dist/{product-B_kS4Oxa.d.ts → product-_GFoaPHU.d.ts} +2 -0
- package/dist/react.d.mts +39 -5
- package/dist/react.d.ts +39 -5
- package/dist/react.js +187 -66
- package/dist/react.mjs +187 -67
- package/dist/{server-DMvQp27Z.d.ts → server-1hXIXH2k.d.ts} +1 -1
- package/dist/{server-DDQ1O3XR.d.mts → server-G8I4JtqK.d.mts} +1 -1
- package/dist/server.d.mts +4 -4
- package/dist/server.d.ts +4 -4
- package/dist/testing/msw.d.mts +2 -2
- package/dist/testing/msw.d.ts +2 -2
- package/dist/testing/suite.d.mts +6 -6
- package/dist/testing/suite.d.ts +6 -6
- package/dist/testing.d.mts +5 -5
- package/dist/testing.d.ts +5 -5
- package/dist/utils.d.mts +4 -4
- package/dist/utils.d.ts +4 -4
- package/package.json +1 -1
- package/registry/product-customizer.json +1 -1
package/dist/react.mjs
CHANGED
|
@@ -7306,6 +7306,144 @@ function formatDuration2(value, unit) {
|
|
|
7306
7306
|
const label = value === 1 ? labels.singular : labels.plural;
|
|
7307
7307
|
return `${value} ${label}`;
|
|
7308
7308
|
}
|
|
7309
|
+
var ANY_VALUE = "__any__";
|
|
7310
|
+
function StaffPicker({
|
|
7311
|
+
staff,
|
|
7312
|
+
selectedStaffId,
|
|
7313
|
+
onStaffSelect,
|
|
7314
|
+
showAnyOption = true,
|
|
7315
|
+
anyLabel = "Any available",
|
|
7316
|
+
className,
|
|
7317
|
+
classNames
|
|
7318
|
+
}) {
|
|
7319
|
+
const groupValue = selectedStaffId === null ? ANY_VALUE : selectedStaffId ?? "";
|
|
7320
|
+
return /* @__PURE__ */ jsxs(
|
|
7321
|
+
RadioGroup,
|
|
7322
|
+
{
|
|
7323
|
+
"data-cimplify-staff-picker": true,
|
|
7324
|
+
className: cn(className, classNames?.root),
|
|
7325
|
+
value: groupValue,
|
|
7326
|
+
onValueChange: (value) => {
|
|
7327
|
+
onStaffSelect?.(value === ANY_VALUE ? null : value);
|
|
7328
|
+
},
|
|
7329
|
+
children: [
|
|
7330
|
+
showAnyOption && /* @__PURE__ */ jsx(
|
|
7331
|
+
Radio.Root,
|
|
7332
|
+
{
|
|
7333
|
+
value: ANY_VALUE,
|
|
7334
|
+
"data-cimplify-staff-option": true,
|
|
7335
|
+
"data-selected": selectedStaffId === null || void 0,
|
|
7336
|
+
"data-any": true,
|
|
7337
|
+
className: classNames?.option,
|
|
7338
|
+
children: /* @__PURE__ */ jsx("span", { "data-cimplify-staff-name": true, className: classNames?.name, children: anyLabel })
|
|
7339
|
+
}
|
|
7340
|
+
),
|
|
7341
|
+
staff.map((member) => /* @__PURE__ */ jsxs(
|
|
7342
|
+
Radio.Root,
|
|
7343
|
+
{
|
|
7344
|
+
value: member.id,
|
|
7345
|
+
"data-cimplify-staff-option": true,
|
|
7346
|
+
"data-selected": selectedStaffId === member.id || void 0,
|
|
7347
|
+
className: classNames?.option,
|
|
7348
|
+
children: [
|
|
7349
|
+
member.avatar_url && /* @__PURE__ */ jsx(
|
|
7350
|
+
"img",
|
|
7351
|
+
{
|
|
7352
|
+
src: member.avatar_url,
|
|
7353
|
+
alt: member.name,
|
|
7354
|
+
"data-cimplify-staff-avatar": true,
|
|
7355
|
+
className: classNames?.avatar
|
|
7356
|
+
}
|
|
7357
|
+
),
|
|
7358
|
+
/* @__PURE__ */ jsx("span", { "data-cimplify-staff-name": true, className: classNames?.name, children: member.name }),
|
|
7359
|
+
member.bio && /* @__PURE__ */ jsx("span", { "data-cimplify-staff-bio": true, className: classNames?.bio, children: member.bio })
|
|
7360
|
+
]
|
|
7361
|
+
},
|
|
7362
|
+
member.id
|
|
7363
|
+
))
|
|
7364
|
+
]
|
|
7365
|
+
}
|
|
7366
|
+
);
|
|
7367
|
+
}
|
|
7368
|
+
function formatDate(value) {
|
|
7369
|
+
const date = new Date(value);
|
|
7370
|
+
if (Number.isNaN(date.getTime())) return value;
|
|
7371
|
+
return date.toLocaleDateString(void 0, {
|
|
7372
|
+
weekday: "short",
|
|
7373
|
+
month: "short",
|
|
7374
|
+
day: "numeric"
|
|
7375
|
+
});
|
|
7376
|
+
}
|
|
7377
|
+
function formatTime(value) {
|
|
7378
|
+
const date = new Date(value);
|
|
7379
|
+
if (Number.isNaN(date.getTime())) return value;
|
|
7380
|
+
return date.toLocaleTimeString(void 0, { hour: "numeric", minute: "2-digit" });
|
|
7381
|
+
}
|
|
7382
|
+
function BookingSummary({
|
|
7383
|
+
slot,
|
|
7384
|
+
total,
|
|
7385
|
+
staffName,
|
|
7386
|
+
durationMinutes,
|
|
7387
|
+
depositType,
|
|
7388
|
+
depositAmount,
|
|
7389
|
+
currency,
|
|
7390
|
+
className,
|
|
7391
|
+
classNames
|
|
7392
|
+
}) {
|
|
7393
|
+
const depositValue = depositAmount != null ? parsePrice(depositAmount) : 0;
|
|
7394
|
+
const depositDue = depositType === "percentage" ? total * depositValue / 100 : depositType === "fixed" ? depositValue : 0;
|
|
7395
|
+
const hasDeposit = depositDue > 0;
|
|
7396
|
+
const balance = Math.max(0, total - depositDue);
|
|
7397
|
+
const rowClass = cn("flex items-center justify-between", classNames?.row);
|
|
7398
|
+
const labelClass = cn("text-muted-foreground", classNames?.label);
|
|
7399
|
+
const valueClass = cn("font-medium", classNames?.value);
|
|
7400
|
+
return /* @__PURE__ */ jsxs(
|
|
7401
|
+
"div",
|
|
7402
|
+
{
|
|
7403
|
+
"data-cimplify-booking-summary": true,
|
|
7404
|
+
className: cn("rounded-lg border border-border p-4 space-y-3", className, classNames?.root),
|
|
7405
|
+
children: [
|
|
7406
|
+
/* @__PURE__ */ jsxs("div", { className: cn("flex items-center justify-between", classNames?.header), children: [
|
|
7407
|
+
/* @__PURE__ */ jsx("span", { className: "text-sm font-semibold", children: "Your booking" }),
|
|
7408
|
+
durationMinutes != null && /* @__PURE__ */ jsxs("span", { className: "text-xs text-muted-foreground", children: [
|
|
7409
|
+
durationMinutes,
|
|
7410
|
+
" min"
|
|
7411
|
+
] })
|
|
7412
|
+
] }),
|
|
7413
|
+
/* @__PURE__ */ jsxs("div", { className: "space-y-1.5 text-sm", children: [
|
|
7414
|
+
staffName && /* @__PURE__ */ jsxs("div", { className: rowClass, children: [
|
|
7415
|
+
/* @__PURE__ */ jsx("span", { className: labelClass, children: "With" }),
|
|
7416
|
+
/* @__PURE__ */ jsx("span", { className: valueClass, children: staffName })
|
|
7417
|
+
] }),
|
|
7418
|
+
/* @__PURE__ */ jsxs("div", { className: rowClass, children: [
|
|
7419
|
+
/* @__PURE__ */ jsx("span", { className: labelClass, children: "Date" }),
|
|
7420
|
+
/* @__PURE__ */ jsx("span", { className: valueClass, children: formatDate(slot.start_time) })
|
|
7421
|
+
] }),
|
|
7422
|
+
/* @__PURE__ */ jsxs("div", { className: rowClass, children: [
|
|
7423
|
+
/* @__PURE__ */ jsx("span", { className: labelClass, children: "Time" }),
|
|
7424
|
+
/* @__PURE__ */ jsx("span", { className: valueClass, children: formatTime(slot.start_time) })
|
|
7425
|
+
] })
|
|
7426
|
+
] }),
|
|
7427
|
+
/* @__PURE__ */ jsxs("div", { className: cn("border-t border-border pt-3 space-y-1.5 text-sm", classNames?.totals), children: [
|
|
7428
|
+
/* @__PURE__ */ jsxs("div", { className: rowClass, children: [
|
|
7429
|
+
/* @__PURE__ */ jsx("span", { className: labelClass, children: "Subtotal" }),
|
|
7430
|
+
/* @__PURE__ */ jsx("span", { className: "font-semibold", children: /* @__PURE__ */ jsx(Price, { amount: total, currency }) })
|
|
7431
|
+
] }),
|
|
7432
|
+
hasDeposit && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
7433
|
+
/* @__PURE__ */ jsxs("div", { className: cn(rowClass, classNames?.deposit), children: [
|
|
7434
|
+
/* @__PURE__ */ jsx("span", { className: labelClass, children: "Due now (deposit)" }),
|
|
7435
|
+
/* @__PURE__ */ jsx("span", { className: "font-semibold", children: /* @__PURE__ */ jsx(Price, { amount: depositDue, currency }) })
|
|
7436
|
+
] }),
|
|
7437
|
+
/* @__PURE__ */ jsxs("div", { className: cn(rowClass, "text-xs text-muted-foreground", classNames?.balance), children: [
|
|
7438
|
+
/* @__PURE__ */ jsx("span", { children: "Balance at appointment" }),
|
|
7439
|
+
/* @__PURE__ */ jsx(Price, { amount: balance, currency })
|
|
7440
|
+
] })
|
|
7441
|
+
] })
|
|
7442
|
+
] })
|
|
7443
|
+
]
|
|
7444
|
+
}
|
|
7445
|
+
);
|
|
7446
|
+
}
|
|
7309
7447
|
var FREQUENCY_LABELS = {
|
|
7310
7448
|
weekly: "Weekly",
|
|
7311
7449
|
biweekly: "Biweekly",
|
|
@@ -8670,7 +8808,7 @@ function groupSlots(slots) {
|
|
|
8670
8808
|
}
|
|
8671
8809
|
return ["morning", "afternoon", "evening"].filter((tod) => groups[tod]?.length).map((tod) => ({ label: TIME_OF_DAY_LABELS[tod], slots: groups[tod] }));
|
|
8672
8810
|
}
|
|
8673
|
-
function
|
|
8811
|
+
function formatTime2(timeStr) {
|
|
8674
8812
|
try {
|
|
8675
8813
|
const date = new Date(timeStr);
|
|
8676
8814
|
if (!isNaN(date.getTime())) {
|
|
@@ -8845,7 +8983,7 @@ function SlotPicker({
|
|
|
8845
8983
|
classNames?.slot
|
|
8846
8984
|
),
|
|
8847
8985
|
children: [
|
|
8848
|
-
/* @__PURE__ */ jsx("span", { "data-cimplify-slot-time": true, className: classNames?.slotTime, children: isMultiDay ? formatStaySummary(slot, durationUnit, durationValue) :
|
|
8986
|
+
/* @__PURE__ */ jsx("span", { "data-cimplify-slot-time": true, className: classNames?.slotTime, children: isMultiDay ? formatStaySummary(slot, durationUnit, durationValue) : formatTime2(slot.start_time) }),
|
|
8849
8987
|
showPrice && slot.price && /* @__PURE__ */ jsx(
|
|
8850
8988
|
"span",
|
|
8851
8989
|
{
|
|
@@ -8879,7 +9017,7 @@ function SlotPicker({
|
|
|
8879
9017
|
}
|
|
8880
9018
|
);
|
|
8881
9019
|
}
|
|
8882
|
-
function
|
|
9020
|
+
function formatDate2(dateStr) {
|
|
8883
9021
|
const date = /* @__PURE__ */ new Date(dateStr + "T00:00:00");
|
|
8884
9022
|
return date.toLocaleDateString(void 0, { weekday: "short", month: "short", day: "numeric" });
|
|
8885
9023
|
}
|
|
@@ -9024,7 +9162,7 @@ function DateSlotPicker({
|
|
|
9024
9162
|
"flex flex-col items-center justify-center rounded-md border border-border bg-background px-1 py-2 text-center text-xs font-medium text-foreground transition-colors hover:border-foreground/40 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring data-[selected]:border-foreground data-[selected]:bg-foreground data-[selected]:text-background data-[fully-booked]:cursor-not-allowed data-[fully-booked]:opacity-40",
|
|
9025
9163
|
classNames?.dateButton
|
|
9026
9164
|
),
|
|
9027
|
-
children:
|
|
9165
|
+
children: formatDate2(date)
|
|
9028
9166
|
},
|
|
9029
9167
|
date
|
|
9030
9168
|
);
|
|
@@ -9084,6 +9222,7 @@ function ProductCustomizer({
|
|
|
9084
9222
|
const [customerInputValues, setCustomerInputValues] = useState({});
|
|
9085
9223
|
const [specialInstructions, setSpecialInstructions] = useState("");
|
|
9086
9224
|
const [selectedSlot, setSelectedSlot] = useState(null);
|
|
9225
|
+
const [selectedStaffId, setSelectedStaffId] = useState(null);
|
|
9087
9226
|
const cart = useCart();
|
|
9088
9227
|
const currency = getProductCurrency(product);
|
|
9089
9228
|
const productType = product.type || "product";
|
|
@@ -9111,7 +9250,16 @@ function ProductCustomizer({
|
|
|
9111
9250
|
setCustomerInputValues({});
|
|
9112
9251
|
setSpecialInstructions("");
|
|
9113
9252
|
setSelectedSlot(null);
|
|
9253
|
+
setSelectedStaffId(null);
|
|
9114
9254
|
}, [product.id, product.min_order_quantity]);
|
|
9255
|
+
const slotStaff = useMemo(
|
|
9256
|
+
() => (selectedSlot?.available_staff ?? []).map((member) => ({
|
|
9257
|
+
id: member.staff_id,
|
|
9258
|
+
name: member.name
|
|
9259
|
+
})),
|
|
9260
|
+
[selectedSlot]
|
|
9261
|
+
);
|
|
9262
|
+
const selectedStaffName = selectedStaffId ? slotStaff.find((member) => member.id === selectedStaffId)?.name ?? null : null;
|
|
9115
9263
|
const selectedAddOnOptions = useMemo(() => {
|
|
9116
9264
|
if (!product.add_ons) return [];
|
|
9117
9265
|
const options = [];
|
|
@@ -9223,7 +9371,7 @@ function ProductCustomizer({
|
|
|
9223
9371
|
...isService && selectedSlot ? {
|
|
9224
9372
|
scheduledStart: selectedSlot.start_time,
|
|
9225
9373
|
scheduledEnd: selectedSlot.end_time,
|
|
9226
|
-
staffId: selectedSlot.available_staff?.[0]?.staff_id,
|
|
9374
|
+
staffId: selectedStaffId ?? selectedSlot.available_staff?.[0]?.staff_id,
|
|
9227
9375
|
resourceId: selectedSlot.available_resources?.[0]?.resource_id
|
|
9228
9376
|
} : {},
|
|
9229
9377
|
customerInputs: Object.keys(customerInputValues).length > 0 ? Object.entries(customerInputValues).filter(([, v]) => v !== void 0 && v !== "").map(([fieldId, value]) => ({ field_id: fieldId, value })) : void 0,
|
|
@@ -9245,7 +9393,7 @@ function ProductCustomizer({
|
|
|
9245
9393
|
} finally {
|
|
9246
9394
|
setIsSubmitting(false);
|
|
9247
9395
|
}
|
|
9248
|
-
}, [product, quantity, selectedVariantId, selectedVariant, quoteId, normalizedAddOnOptionIds, selectedAddOnOptions, isComposite, compositeSelections, isBundle, bundleSelections, selectedBillingPlan, customerInputValues, specialInstructions, isService, selectedSlot, isSubmitting, onAddToCart, cart]);
|
|
9396
|
+
}, [product, quantity, selectedVariantId, selectedVariant, quoteId, normalizedAddOnOptionIds, selectedAddOnOptions, isComposite, compositeSelections, isBundle, bundleSelections, selectedBillingPlan, customerInputValues, specialInstructions, isService, selectedSlot, selectedStaffId, isSubmitting, onAddToCart, cart]);
|
|
9249
9397
|
return /* @__PURE__ */ jsxs("div", { "data-cimplify-customizer": true, className: cn("space-y-6", className, classNames?.root), children: [
|
|
9250
9398
|
isComposite && product.groups && product.composite_id && /* @__PURE__ */ jsx(
|
|
9251
9399
|
CompositeSelector,
|
|
@@ -9277,11 +9425,30 @@ function ProductCustomizer({
|
|
|
9277
9425
|
{
|
|
9278
9426
|
serviceId: product.id,
|
|
9279
9427
|
selectedSlot,
|
|
9280
|
-
onSlotSelect: (slot) =>
|
|
9428
|
+
onSlotSelect: (slot) => {
|
|
9429
|
+
setSelectedSlot(slot);
|
|
9430
|
+
setSelectedStaffId(null);
|
|
9431
|
+
},
|
|
9281
9432
|
participantCount: quantity,
|
|
9282
9433
|
currency
|
|
9283
9434
|
}
|
|
9284
9435
|
),
|
|
9436
|
+
isService && slotStaff.length > 0 && /* @__PURE__ */ jsxs("div", { "data-cimplify-customizer-staff": true, children: [
|
|
9437
|
+
/* @__PURE__ */ jsx("label", { className: "block text-xs font-medium uppercase tracking-wider text-muted-foreground mb-3", children: "Choose your specialist" }),
|
|
9438
|
+
/* @__PURE__ */ jsx(
|
|
9439
|
+
StaffPicker,
|
|
9440
|
+
{
|
|
9441
|
+
staff: slotStaff,
|
|
9442
|
+
selectedStaffId,
|
|
9443
|
+
onStaffSelect: setSelectedStaffId,
|
|
9444
|
+
className: "flex flex-wrap gap-2",
|
|
9445
|
+
classNames: {
|
|
9446
|
+
option: "inline-flex items-center gap-2 rounded-md border border-border px-3 py-2 text-sm font-medium transition-colors hover:border-foreground/40 cursor-pointer data-[selected]:border-foreground data-[selected]:bg-foreground data-[selected]:text-background",
|
|
9447
|
+
avatar: "w-6 h-6 rounded-full object-cover"
|
|
9448
|
+
}
|
|
9449
|
+
}
|
|
9450
|
+
)
|
|
9451
|
+
] }),
|
|
9285
9452
|
hasVariants && /* @__PURE__ */ jsx(
|
|
9286
9453
|
VariantSelector,
|
|
9287
9454
|
{
|
|
@@ -9366,6 +9533,18 @@ function ProductCustomizer({
|
|
|
9366
9533
|
)
|
|
9367
9534
|
}
|
|
9368
9535
|
) }),
|
|
9536
|
+
isService && selectedSlot && /* @__PURE__ */ jsx(
|
|
9537
|
+
BookingSummary,
|
|
9538
|
+
{
|
|
9539
|
+
slot: selectedSlot,
|
|
9540
|
+
total: displayTotalPrice,
|
|
9541
|
+
staffName: selectedStaffName,
|
|
9542
|
+
durationMinutes: product.duration_minutes,
|
|
9543
|
+
depositType: product.deposit_type,
|
|
9544
|
+
depositAmount: product.deposit_amount,
|
|
9545
|
+
currency
|
|
9546
|
+
}
|
|
9547
|
+
),
|
|
9369
9548
|
/* @__PURE__ */ jsxs(
|
|
9370
9549
|
"div",
|
|
9371
9550
|
{
|
|
@@ -12481,65 +12660,6 @@ function StoreNav({
|
|
|
12481
12660
|
] })
|
|
12482
12661
|
] });
|
|
12483
12662
|
}
|
|
12484
|
-
var ANY_VALUE = "__any__";
|
|
12485
|
-
function StaffPicker({
|
|
12486
|
-
staff,
|
|
12487
|
-
selectedStaffId,
|
|
12488
|
-
onStaffSelect,
|
|
12489
|
-
showAnyOption = true,
|
|
12490
|
-
anyLabel = "Any available",
|
|
12491
|
-
className,
|
|
12492
|
-
classNames
|
|
12493
|
-
}) {
|
|
12494
|
-
const groupValue = selectedStaffId === null ? ANY_VALUE : selectedStaffId ?? "";
|
|
12495
|
-
return /* @__PURE__ */ jsxs(
|
|
12496
|
-
RadioGroup,
|
|
12497
|
-
{
|
|
12498
|
-
"data-cimplify-staff-picker": true,
|
|
12499
|
-
className: cn(className, classNames?.root),
|
|
12500
|
-
value: groupValue,
|
|
12501
|
-
onValueChange: (value) => {
|
|
12502
|
-
onStaffSelect?.(value === ANY_VALUE ? null : value);
|
|
12503
|
-
},
|
|
12504
|
-
children: [
|
|
12505
|
-
showAnyOption && /* @__PURE__ */ jsx(
|
|
12506
|
-
Radio.Root,
|
|
12507
|
-
{
|
|
12508
|
-
value: ANY_VALUE,
|
|
12509
|
-
"data-cimplify-staff-option": true,
|
|
12510
|
-
"data-selected": selectedStaffId === null || void 0,
|
|
12511
|
-
"data-any": true,
|
|
12512
|
-
className: classNames?.option,
|
|
12513
|
-
children: /* @__PURE__ */ jsx("span", { "data-cimplify-staff-name": true, className: classNames?.name, children: anyLabel })
|
|
12514
|
-
}
|
|
12515
|
-
),
|
|
12516
|
-
staff.map((member) => /* @__PURE__ */ jsxs(
|
|
12517
|
-
Radio.Root,
|
|
12518
|
-
{
|
|
12519
|
-
value: member.id,
|
|
12520
|
-
"data-cimplify-staff-option": true,
|
|
12521
|
-
"data-selected": selectedStaffId === member.id || void 0,
|
|
12522
|
-
className: classNames?.option,
|
|
12523
|
-
children: [
|
|
12524
|
-
member.avatar_url && /* @__PURE__ */ jsx(
|
|
12525
|
-
"img",
|
|
12526
|
-
{
|
|
12527
|
-
src: member.avatar_url,
|
|
12528
|
-
alt: member.name,
|
|
12529
|
-
"data-cimplify-staff-avatar": true,
|
|
12530
|
-
className: classNames?.avatar
|
|
12531
|
-
}
|
|
12532
|
-
),
|
|
12533
|
-
/* @__PURE__ */ jsx("span", { "data-cimplify-staff-name": true, className: classNames?.name, children: member.name }),
|
|
12534
|
-
member.bio && /* @__PURE__ */ jsx("span", { "data-cimplify-staff-bio": true, className: classNames?.bio, children: member.bio })
|
|
12535
|
-
]
|
|
12536
|
-
},
|
|
12537
|
-
member.id
|
|
12538
|
-
))
|
|
12539
|
-
]
|
|
12540
|
-
}
|
|
12541
|
-
);
|
|
12542
|
-
}
|
|
12543
12663
|
var STATUS_LABELS3 = {
|
|
12544
12664
|
pending: "Pending",
|
|
12545
12665
|
confirmed: "Confirmed",
|
|
@@ -16632,4 +16752,4 @@ function SparkleIcon({ className }) {
|
|
|
16632
16752
|
);
|
|
16633
16753
|
}
|
|
16634
16754
|
|
|
16635
|
-
export { AccommodationCard, AccountContent, AccountDashboardPage, AccountHero, AccountIdentityStrip, AccountLineItem, AccountLineItems, AccountNav, AccountOrderDetailPage, AccountOrderView, AccountOrdersPage, AccountShell, AccountSidebar, AccountSignedOutPrompt, AccountSubscriptionDetailPage, AccountSubscriptionsPage, Ad, AdProvider, AddOnSelector, AddressElement, AuthElement, AvailabilityBadge, BackLink, BillingPlanSelector, BookingCard, BookingList, BookingPage, BookingsPage, BoxIcon24, BundleProductCard, BundleProductLayout, BundleSelector, Button, CardImage, CardShell, CardVariant, CartDrawer, CartDrawerProvider, CartPage, CartSummary, CartTemplate, CatalogueCollectionLayout, CataloguePage, CatalogueTemplate, CategoryFilter, CategoryGrid, ChatWidget, CheckIcon, CheckoutPage, CimplifyCheckout, CimplifyProvider, CimplifySignInButton, CimplifyUserButton, CollectionPage, CollectionTemplate, CompactCartLayout, CompactCatalogueLayout, CompactSearchLayout, CompactServiceCard, CompositeProductCard, CompositeProductLayout, CompositeSelector, ContextCard, ContinuityCard, CurrencySelector, CustomAttributesTable, CustomerInputFields, DashboardIcon, DashboardIcon20, DatePicker, DateSlotPicker, DealBanner, DealsPage, DefaultCartLayout, DefaultCatalogueLayout, DefaultCollectionLayout, DefaultProductLayout, DefaultSearchLayout, DeliveryEstimate, DeliveryTracker, DeliveryTruckIcon, DigitalProductCard, DigitalProductLayout, DiscountInput, ElementsProvider, EmptyState, ErrorState, FeaturedCollectionLayout, FilterTabs, FoodProductCard, FoodProductLayout, HappeningCard, IdentityIcon, IdentityOfframp, InventoryBadge, ItemsCard, LeaseServiceCard, LineItemDetails, LocationPicker, MediaGallery, MetadataStringList, MobileAccountShell, MobileBackButton, MobileMoreButton, MobileTabBar, NextDeliveryCard, OrderDetailHeader, OrderDetailMetaPipe, OrderDetailPage, OrderHistory, OrderHistoryPage, OrderList, OrderRow, OrderSummary, OrderTimeline, OrderTotals, OrdersIcon, OrdersIcon20, OvenIcon, PackageIcon, PaymentElement, Price, PriceRange, ProductCard, ProductCustomizer, ProductGrid, ProductImageGallery, ProductModel3D, ProductPage, ProductSheet, ProductTemplate, PropertiesTable, QuantitySelector, QuickAddButton, RecentlyViewed, RecommendationCarousel, RelatedProductsSection, RentalServiceCard, ResourcePicker, RetailProductCard, RichText, SaleBadge, ScheduleServiceCard, SearchInput, SearchPage, SearchTemplate, Section, SectionMore, ServiceProductLayout, SessionMessageBanner, SignOutIcon, Skeleton, SlotPicker, SoldOutOverlay, StaffPicker, StandardServiceCard, Status, StoreNav, StoreVideo, SubscriptionCard, SubscriptionList, SubscriptionRow, SubscriptionsIcon, SubscriptionsIcon20, TagPills, TimePicker, Toast, TwoColumnGrid, VariantSelector, VolumePricing, WelcomeCard, WholesaleProductCard, WholesaleProductLayout, WishlistButton, cn, roomToResource, sanitizeRichTextHtml, stripHtmlToText, toAccountLineItem, toAccountLineItems, toAccountOrderView, useActivityState, useAds, useAttributeDefinitions, useAvailableSlots, useBillingPlans, useBookings, useBootstrap, useBundles, useCart, useCartDrawer, useCategories, useChat, useCheckout, useCimplify, useCimplifyClient, useCimplifySession, useCollection, useCollections, useDeals, useDeliveryFee, useElements, useElementsReady, useFxRate, useLocations, useOptionalCimplify, useOrder, useOrders, useProduct, useProductAvailability, useProductDeals, useProductPrice, useProductSchedules, useProducts, useProductsOnSale, usePropertyFacets, useQuote, useRecommendations, useSearch, useServiceAvailability, useServices, useSubscription, useSubscriptions, useTaxonomies, useTaxonomy, useTaxonomyPath, useValidateDiscount, useVariantSelector };
|
|
16755
|
+
export { AccommodationCard, AccountContent, AccountDashboardPage, AccountHero, AccountIdentityStrip, AccountLineItem, AccountLineItems, AccountNav, AccountOrderDetailPage, AccountOrderView, AccountOrdersPage, AccountShell, AccountSidebar, AccountSignedOutPrompt, AccountSubscriptionDetailPage, AccountSubscriptionsPage, Ad, AdProvider, AddOnSelector, AddressElement, AuthElement, AvailabilityBadge, BackLink, BillingPlanSelector, BookingCard, BookingList, BookingPage, BookingSummary, BookingsPage, BoxIcon24, BundleProductCard, BundleProductLayout, BundleSelector, Button, CardImage, CardShell, CardVariant, CartDrawer, CartDrawerProvider, CartPage, CartSummary, CartTemplate, CatalogueCollectionLayout, CataloguePage, CatalogueTemplate, CategoryFilter, CategoryGrid, ChatWidget, CheckIcon, CheckoutPage, CimplifyCheckout, CimplifyProvider, CimplifySignInButton, CimplifyUserButton, CollectionPage, CollectionTemplate, CompactCartLayout, CompactCatalogueLayout, CompactSearchLayout, CompactServiceCard, CompositeProductCard, CompositeProductLayout, CompositeSelector, ContextCard, ContinuityCard, CurrencySelector, CustomAttributesTable, CustomerInputFields, DashboardIcon, DashboardIcon20, DatePicker, DateSlotPicker, DealBanner, DealsPage, DefaultCartLayout, DefaultCatalogueLayout, DefaultCollectionLayout, DefaultProductLayout, DefaultSearchLayout, DeliveryEstimate, DeliveryTracker, DeliveryTruckIcon, DigitalProductCard, DigitalProductLayout, DiscountInput, ElementsProvider, EmptyState, ErrorState, FeaturedCollectionLayout, FilterTabs, FoodProductCard, FoodProductLayout, HappeningCard, IdentityIcon, IdentityOfframp, InventoryBadge, ItemsCard, LeaseServiceCard, LineItemDetails, LocationPicker, MediaGallery, MetadataStringList, MobileAccountShell, MobileBackButton, MobileMoreButton, MobileTabBar, NextDeliveryCard, OrderDetailHeader, OrderDetailMetaPipe, OrderDetailPage, OrderHistory, OrderHistoryPage, OrderList, OrderRow, OrderSummary, OrderTimeline, OrderTotals, OrdersIcon, OrdersIcon20, OvenIcon, PackageIcon, PaymentElement, Price, PriceRange, ProductCard, ProductCustomizer, ProductGrid, ProductImageGallery, ProductModel3D, ProductPage, ProductSheet, ProductTemplate, PropertiesTable, QuantitySelector, QuickAddButton, RecentlyViewed, RecommendationCarousel, RelatedProductsSection, RentalServiceCard, ResourcePicker, RetailProductCard, RichText, SaleBadge, ScheduleServiceCard, SearchInput, SearchPage, SearchTemplate, Section, SectionMore, ServiceProductLayout, SessionMessageBanner, SignOutIcon, Skeleton, SlotPicker, SoldOutOverlay, StaffPicker, StandardServiceCard, Status, StoreNav, StoreVideo, SubscriptionCard, SubscriptionList, SubscriptionRow, SubscriptionsIcon, SubscriptionsIcon20, TagPills, TimePicker, Toast, TwoColumnGrid, VariantSelector, VolumePricing, WelcomeCard, WholesaleProductCard, WholesaleProductLayout, WishlistButton, cn, roomToResource, sanitizeRichTextHtml, stripHtmlToText, toAccountLineItem, toAccountLineItems, toAccountOrderView, useActivityState, useAds, useAttributeDefinitions, useAvailableSlots, useBillingPlans, useBookings, useBootstrap, useBundles, useCart, useCartDrawer, useCategories, useChat, useCheckout, useCimplify, useCimplifyClient, useCimplifySession, useCollection, useCollections, useDeals, useDeliveryFee, useElements, useElementsReady, useFxRate, useLocations, useOptionalCimplify, useOrder, useOrders, useProduct, useProductAvailability, useProductDeals, useProductPrice, useProductSchedules, useProducts, useProductsOnSale, usePropertyFacets, useQuote, useRecommendations, useSearch, useServiceAvailability, useServices, useSubscription, useSubscriptions, useTaxonomies, useTaxonomy, useTaxonomyPath, useValidateDiscount, useVariantSelector };
|
package/dist/server.d.mts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { ad as CimplifyClient } from './client-
|
|
2
|
-
export { K as CacheOptions, cq as ReadRequestOptions, cM as Result } from './client-
|
|
3
|
-
export { Q as Category, T as CimplifyError, U as Collection, aX as Product, b9 as ProductWithDetails } from './product-
|
|
1
|
+
import { ad as CimplifyClient } from './client-D7uHF_jh.mjs';
|
|
2
|
+
export { K as CacheOptions, cq as ReadRequestOptions, cM as Result } from './client-D7uHF_jh.mjs';
|
|
3
|
+
export { Q as Category, T as CimplifyError, U as Collection, aX as Product, b9 as ProductWithDetails } from './product-_GFoaPHU.mjs';
|
|
4
4
|
import { JWTVerifyResult } from 'jose';
|
|
5
|
-
import './payment-
|
|
5
|
+
import './payment-C-2gApyQ.mjs';
|
|
6
6
|
|
|
7
7
|
interface ServerClientOptions {
|
|
8
8
|
/**
|
package/dist/server.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { ad as CimplifyClient } from './client-
|
|
2
|
-
export { K as CacheOptions, cq as ReadRequestOptions, cM as Result } from './client-
|
|
3
|
-
export { Q as Category, T as CimplifyError, U as Collection, aX as Product, b9 as ProductWithDetails } from './product-
|
|
1
|
+
import { ad as CimplifyClient } from './client-xDpke-KS.js';
|
|
2
|
+
export { K as CacheOptions, cq as ReadRequestOptions, cM as Result } from './client-xDpke-KS.js';
|
|
3
|
+
export { Q as Category, T as CimplifyError, U as Collection, aX as Product, b9 as ProductWithDetails } from './product-_GFoaPHU.js';
|
|
4
4
|
import { JWTVerifyResult } from 'jose';
|
|
5
|
-
import './payment-
|
|
5
|
+
import './payment-CfemTORM.js';
|
|
6
6
|
|
|
7
7
|
interface ServerClientOptions {
|
|
8
8
|
/**
|
package/dist/testing/msw.d.mts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { A as AppHandle, C as CreateAppOptions } from '../server-
|
|
1
|
+
import { A as AppHandle, C as CreateAppOptions } from '../server-G8I4JtqK.mjs';
|
|
2
2
|
import 'hono';
|
|
3
|
-
import '../product-
|
|
3
|
+
import '../product-_GFoaPHU.mjs';
|
|
4
4
|
|
|
5
5
|
interface MswHandlerSetup {
|
|
6
6
|
handlers: unknown[];
|
package/dist/testing/msw.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { A as AppHandle, C as CreateAppOptions } from '../server-
|
|
1
|
+
import { A as AppHandle, C as CreateAppOptions } from '../server-1hXIXH2k.js';
|
|
2
2
|
import 'hono';
|
|
3
|
-
import '../product-
|
|
3
|
+
import '../product-_GFoaPHU.js';
|
|
4
4
|
|
|
5
5
|
interface MswHandlerSetup {
|
|
6
6
|
handlers: unknown[];
|
package/dist/testing/suite.d.mts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { TestAPI } from 'vitest';
|
|
2
|
-
import { T as TestClientHandle } from '../client-
|
|
3
|
-
import { C as CreateAppOptions } from '../server-
|
|
4
|
-
export { S as SeedName } from '../server-
|
|
5
|
-
import '../client-
|
|
6
|
-
import '../product-
|
|
7
|
-
import '../payment-
|
|
2
|
+
import { T as TestClientHandle } from '../client-yFghjc1-.mjs';
|
|
3
|
+
import { C as CreateAppOptions } from '../server-G8I4JtqK.mjs';
|
|
4
|
+
export { S as SeedName } from '../server-G8I4JtqK.mjs';
|
|
5
|
+
import '../client-D7uHF_jh.mjs';
|
|
6
|
+
import '../product-_GFoaPHU.mjs';
|
|
7
|
+
import '../payment-C-2gApyQ.mjs';
|
|
8
8
|
import 'hono';
|
|
9
9
|
|
|
10
10
|
/**
|
package/dist/testing/suite.d.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { TestAPI } from 'vitest';
|
|
2
|
-
import { T as TestClientHandle } from '../client
|
|
3
|
-
import { C as CreateAppOptions } from '../server-
|
|
4
|
-
export { S as SeedName } from '../server-
|
|
5
|
-
import '../client-
|
|
6
|
-
import '../product-
|
|
7
|
-
import '../payment-
|
|
2
|
+
import { T as TestClientHandle } from '../client--w53owD4.js';
|
|
3
|
+
import { C as CreateAppOptions } from '../server-1hXIXH2k.js';
|
|
4
|
+
export { S as SeedName } from '../server-1hXIXH2k.js';
|
|
5
|
+
import '../client-xDpke-KS.js';
|
|
6
|
+
import '../product-_GFoaPHU.js';
|
|
7
|
+
import '../payment-CfemTORM.js';
|
|
8
8
|
import 'hono';
|
|
9
9
|
|
|
10
10
|
/**
|
package/dist/testing.d.mts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { $ZodType } from 'zod/v4/core';
|
|
3
|
-
export { A as AddFirstProductOptions, a as AddFirstProductResult, P as ProductSummary, T as TestClientHandle, c as createTestClient, f as fixtures } from './client-
|
|
4
|
-
import './client-
|
|
5
|
-
import './product-
|
|
6
|
-
import './payment-
|
|
7
|
-
import './server-
|
|
3
|
+
export { A as AddFirstProductOptions, a as AddFirstProductResult, P as ProductSummary, T as TestClientHandle, c as createTestClient, f as fixtures } from './client-yFghjc1-.mjs';
|
|
4
|
+
import './client-D7uHF_jh.mjs';
|
|
5
|
+
import './product-_GFoaPHU.mjs';
|
|
6
|
+
import './payment-C-2gApyQ.mjs';
|
|
7
|
+
import './server-G8I4JtqK.mjs';
|
|
8
8
|
import 'hono';
|
|
9
9
|
|
|
10
10
|
/**
|
package/dist/testing.d.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { $ZodType } from 'zod/v4/core';
|
|
3
|
-
export { A as AddFirstProductOptions, a as AddFirstProductResult, P as ProductSummary, T as TestClientHandle, c as createTestClient, f as fixtures } from './client
|
|
4
|
-
import './client-
|
|
5
|
-
import './product-
|
|
6
|
-
import './payment-
|
|
7
|
-
import './server-
|
|
3
|
+
export { A as AddFirstProductOptions, a as AddFirstProductResult, P as ProductSummary, T as TestClientHandle, c as createTestClient, f as fixtures } from './client--w53owD4.js';
|
|
4
|
+
import './client-xDpke-KS.js';
|
|
5
|
+
import './product-_GFoaPHU.js';
|
|
6
|
+
import './payment-CfemTORM.js';
|
|
7
|
+
import './server-1hXIXH2k.js';
|
|
8
8
|
import 'hono';
|
|
9
9
|
|
|
10
10
|
/**
|
package/dist/utils.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export { C as CURRENCY_SYMBOLS, M as MOBILE_MONEY_PROVIDERS, c as categorizePaymentError, d as detectMobileMoneyProvider, f as formatMoney, a as formatNumberCompact, b as formatPrice, e as formatPriceAdjustment, g as formatPriceCompact, i as formatPriceWithTax, j as formatProductPrice, k as getBasePrice, l as getCurrencySymbol, m as getDiscountPercentage, n as getDisplayPrice, o as getMarkupPercentage, q as getProductCurrency, r as getTaxAmount, t as hasTaxInfo, u as isOnSale, v as isPaymentStatusFailure, w as isPaymentStatusRequiresAction, x as isPaymentStatusSuccess, y as isTaxInclusive, z as isValidPhoneForContext, A as normalizePaymentResponse, B as normalizePhoneToE164, D as normalizeStatusResponse, E as parsePrice, F as resolvePhoneCountryCode } from './index-
|
|
2
|
-
import './product-
|
|
3
|
-
import './price-
|
|
4
|
-
import './payment-
|
|
1
|
+
export { C as CURRENCY_SYMBOLS, M as MOBILE_MONEY_PROVIDERS, c as categorizePaymentError, d as detectMobileMoneyProvider, f as formatMoney, a as formatNumberCompact, b as formatPrice, e as formatPriceAdjustment, g as formatPriceCompact, i as formatPriceWithTax, j as formatProductPrice, k as getBasePrice, l as getCurrencySymbol, m as getDiscountPercentage, n as getDisplayPrice, o as getMarkupPercentage, q as getProductCurrency, r as getTaxAmount, t as hasTaxInfo, u as isOnSale, v as isPaymentStatusFailure, w as isPaymentStatusRequiresAction, x as isPaymentStatusSuccess, y as isTaxInclusive, z as isValidPhoneForContext, A as normalizePaymentResponse, B as normalizePhoneToE164, D as normalizeStatusResponse, E as parsePrice, F as resolvePhoneCountryCode } from './index-lt-fe29V.mjs';
|
|
2
|
+
import './product-_GFoaPHU.mjs';
|
|
3
|
+
import './price-CZBQw-Rh.mjs';
|
|
4
|
+
import './payment-C-2gApyQ.mjs';
|
|
5
5
|
import 'libphonenumber-js';
|
package/dist/utils.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export { C as CURRENCY_SYMBOLS, M as MOBILE_MONEY_PROVIDERS, c as categorizePaymentError, d as detectMobileMoneyProvider, f as formatMoney, a as formatNumberCompact, b as formatPrice, e as formatPriceAdjustment, g as formatPriceCompact, i as formatPriceWithTax, j as formatProductPrice, k as getBasePrice, l as getCurrencySymbol, m as getDiscountPercentage, n as getDisplayPrice, o as getMarkupPercentage, q as getProductCurrency, r as getTaxAmount, t as hasTaxInfo, u as isOnSale, v as isPaymentStatusFailure, w as isPaymentStatusRequiresAction, x as isPaymentStatusSuccess, y as isTaxInclusive, z as isValidPhoneForContext, A as normalizePaymentResponse, B as normalizePhoneToE164, D as normalizeStatusResponse, E as parsePrice, F as resolvePhoneCountryCode } from './index-
|
|
2
|
-
import './product-
|
|
3
|
-
import './price-
|
|
4
|
-
import './payment-
|
|
1
|
+
export { C as CURRENCY_SYMBOLS, M as MOBILE_MONEY_PROVIDERS, c as categorizePaymentError, d as detectMobileMoneyProvider, f as formatMoney, a as formatNumberCompact, b as formatPrice, e as formatPriceAdjustment, g as formatPriceCompact, i as formatPriceWithTax, j as formatProductPrice, k as getBasePrice, l as getCurrencySymbol, m as getDiscountPercentage, n as getDisplayPrice, o as getMarkupPercentage, q as getProductCurrency, r as getTaxAmount, t as hasTaxInfo, u as isOnSale, v as isPaymentStatusFailure, w as isPaymentStatusRequiresAction, x as isPaymentStatusSuccess, y as isTaxInclusive, z as isValidPhoneForContext, A as normalizePaymentResponse, B as normalizePhoneToE164, D as normalizeStatusResponse, E as parsePrice, F as resolvePhoneCountryCode } from './index-DTGS0QC1.js';
|
|
2
|
+
import './product-_GFoaPHU.js';
|
|
3
|
+
import './price-DNeyoO7y.js';
|
|
4
|
+
import './payment-CfemTORM.js';
|
|
5
5
|
import 'libphonenumber-js';
|
package/package.json
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"files": [
|
|
15
15
|
{
|
|
16
16
|
"path": "product-customizer.tsx",
|
|
17
|
-
"content": "\"use client\";\n\nimport React, { useState, useCallback, useMemo, useEffect, useRef } from \"react\";\nimport { Button } from \"@base-ui/react/button\";\nimport type {\n ProductWithDetails,\n VariantView,\n AddOnOption,\n ComponentSelectionInput,\n CompositePriceResult,\n} from \"@cimplify/sdk\";\nimport type { ProductBillingPlan } from \"@cimplify/sdk\";\nimport type { BundleSelectionInput } from \"@cimplify/sdk\";\nimport { parsePrice, getUnitPriceAtQuantity, getProductCurrency } from \"@cimplify/sdk\";\nimport { formatDuration } from \"./utils/format-duration\";\nimport { useCart, useQuote } from \"@cimplify/sdk/react\";\nimport type { AddToCartOptions } from \"@cimplify/sdk/react\";\nimport { Price } from \"@cimplify/sdk/react\";\nimport { QuantitySelector } from \"@cimplify/sdk/react\";\nimport { VariantSelector } from \"@cimplify/sdk/react\";\nimport { AddOnSelector } from \"@cimplify/sdk/react\";\nimport { CompositeSelector } from \"@cimplify/sdk/react\";\nimport { BundleSelector } from \"@cimplify/sdk/react\";\nimport { BillingPlanSelector } from \"@cimplify/sdk/react\";\nimport { VolumePricing } from \"@cimplify/sdk/react\";\nimport { CustomerInputFields } from \"./customer-input-fields\";\nimport { DateSlotPicker } from \"@cimplify/sdk/react\";\nimport type { AvailableSlot } from \"@cimplify/sdk\";\nimport { cn } from \"@cimplify/sdk/react\";\n\nexport interface ProductCustomizerClassNames {\n root?: string;\n actions?: string;\n submitButton?: string;\n submitButtonAdded?: string;\n validation?: string;\n specialInstructions?: string;\n depositInfo?: string;\n allergens?: string;\n duration?: string;\n}\n\nexport interface ProductCustomizerProps {\n product: ProductWithDetails;\n onAddToCart?: (\n product: ProductWithDetails,\n quantity: number,\n options: AddToCartOptions,\n ) => void | Promise<void>;\n /** Lets the parent swap its gallery for `variant.images` on selection. */\n onVariantChange?: (\n variantId: string | undefined,\n variant: VariantView | undefined,\n ) => void;\n showSpecialInstructions?: boolean;\n className?: string;\n classNames?: ProductCustomizerClassNames;\n}\n\nexport function ProductCustomizer({\n product,\n onAddToCart,\n onVariantChange,\n showSpecialInstructions = true,\n className,\n classNames,\n}: ProductCustomizerProps): React.ReactElement {\n const [quantity, setQuantity] = useState(product.min_order_quantity ?? 1);\n const [isAdded, setIsAdded] = useState(false);\n const [isSubmitting, setIsSubmitting] = useState(false);\n const [selectedVariantId, setSelectedVariantId] = useState<string | undefined>();\n const [selectedVariant, setSelectedVariant] = useState<VariantView | undefined>();\n const [selectedAddOnOptionIds, setSelectedAddOnOptionIds] = useState<string[]>([]);\n\n const [compositeSelections, setCompositeSelections] = useState<ComponentSelectionInput[]>([]);\n const [compositePrice, setCompositePrice] = useState<CompositePriceResult | null>(null);\n const [compositeReady, setCompositeReady] = useState(false);\n\n const [bundleSelections, setBundleSelections] = useState<BundleSelectionInput[]>([]);\n const [bundleTotalPrice, setBundleTotalPrice] = useState<number | null>(null);\n const [bundleReady, setBundleReady] = useState(false);\n const [selectedBillingPlan, setSelectedBillingPlan] = useState<ProductBillingPlan | null>(null);\n const [customerInputValues, setCustomerInputValues] = useState<Record<string, unknown>>({});\n const [specialInstructions, setSpecialInstructions] = useState(\"\");\n const [selectedSlot, setSelectedSlot] = useState<AvailableSlot | null>(null);\n\n const cart = useCart();\n\n const currency = getProductCurrency(product);\n const productType = product.type || \"product\";\n const isComposite = productType === \"composite\";\n const isBundle = productType === \"bundle\";\n const isDigital = productType === \"digital\";\n const isService = productType === \"service\";\n const isStandard = !isComposite && !isBundle;\n\n const hasVariants = isStandard && product.variants && product.variants.length > 0;\n const hasAddOns = isStandard && product.add_ons && product.add_ons.length > 0;\n\n useEffect(() => {\n setQuantity(product.min_order_quantity ?? 1);\n setIsAdded(false);\n setIsSubmitting(false);\n setSelectedVariantId(undefined);\n setSelectedVariant(undefined);\n setSelectedAddOnOptionIds([]);\n setCompositeSelections([]);\n setCompositePrice(null);\n setCompositeReady(false);\n setBundleSelections([]);\n setBundleTotalPrice(null);\n setBundleReady(false);\n setSelectedBillingPlan(null);\n setCustomerInputValues({});\n setSpecialInstructions(\"\");\n setSelectedSlot(null);\n }, [product.id, product.min_order_quantity]);\n\n const selectedAddOnOptions = useMemo(() => {\n if (!product.add_ons) return [];\n const options: AddOnOption[] = [];\n for (const addOn of product.add_ons) {\n for (const option of addOn.options) {\n if (selectedAddOnOptionIds.includes(option.id)) {\n options.push(option);\n }\n }\n }\n return options;\n }, [product.add_ons, selectedAddOnOptionIds]);\n\n const normalizedAddOnOptionIds = useMemo(() => {\n if (selectedAddOnOptionIds.length === 0) return [];\n return Array.from(new Set(selectedAddOnOptionIds.map((id) => id.trim()).filter(Boolean))).sort();\n }, [selectedAddOnOptionIds]);\n\n const localTotalPrice = useMemo(() => {\n if (isComposite && compositePrice) {\n return parsePrice(compositePrice.final_price) * quantity;\n }\n\n if (isBundle && bundleTotalPrice != null) {\n return bundleTotalPrice * quantity;\n }\n\n let price = parsePrice(product.default_price);\n\n if (selectedVariant?.price_adjustment) {\n price += parsePrice(selectedVariant.price_adjustment);\n }\n\n for (const option of selectedAddOnOptions) {\n if (option.default_price) {\n price += parsePrice(option.default_price);\n }\n }\n\n return price * quantity;\n }, [product.default_price, selectedVariant, selectedAddOnOptions, quantity, isComposite, compositePrice, isBundle, bundleTotalPrice]);\n\n const requiredAddOnsSatisfied = useMemo(() => {\n if (!product.add_ons) return true;\n\n for (const addOn of product.add_ons) {\n if (addOn.is_required) {\n const selectedInGroup = selectedAddOnOptionIds.filter((id) =>\n addOn.options.some((opt) => opt.id === id),\n ).length;\n\n const minRequired = addOn.min_selections || 1;\n if (selectedInGroup < minRequired) {\n return false;\n }\n }\n }\n return true;\n }, [product.add_ons, selectedAddOnOptionIds]);\n\n const quoteInput = useMemo(\n () => ({\n productId: product.id,\n quantity,\n variantId: selectedVariantId,\n addOnOptionIds: normalizedAddOnOptionIds.length > 0 ? normalizedAddOnOptionIds : undefined,\n bundleSelections: isBundle && bundleSelections.length > 0 ? bundleSelections : undefined,\n compositeSelections: isComposite && compositeSelections.length > 0 ? compositeSelections : undefined,\n }),\n [product.id, quantity, selectedVariantId, normalizedAddOnOptionIds, isBundle, bundleSelections, isComposite, compositeSelections],\n );\n\n const requiredInputsSatisfied = useMemo(() => {\n if (!product.input_fields || product.input_fields.length === 0) return true;\n return product.input_fields.every((field) => {\n if (!field.is_required) return true;\n const val = customerInputValues[field.id];\n return val !== undefined && val !== \"\" && val !== null;\n });\n }, [product.input_fields, customerInputValues]);\n\n const quoteEnabled = isComposite\n ? compositeReady && requiredInputsSatisfied\n : isBundle\n ? bundleReady && requiredInputsSatisfied\n : requiredAddOnsSatisfied && requiredInputsSatisfied;\n\n const { quote } = useQuote(quoteInput, {\n enabled: quoteEnabled,\n });\n\n const quoteId = quote?.quote_id;\n const quotedTotalPrice = useMemo(() => {\n if (!quote) return undefined;\n\n const priceInfo = quote.quoted_total_price_info ?? quote.final_price_info;\n const perUnit = priceInfo.pre_tax_price;\n if (perUnit === undefined || perUnit === null) return undefined;\n return parsePrice(perUnit) * quantity;\n }, [quote, quantity]);\n\n const displayTotalPrice = quotedTotalPrice ?? localTotalPrice;\n\n const handleVariantChange = useCallback(\n (variantId: string | undefined, variant: VariantView | undefined) => {\n setSelectedVariantId(variantId);\n setSelectedVariant(variant);\n onVariantChange?.(variantId, variant);\n },\n [onVariantChange],\n );\n\n const addedTimerRef = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);\n useEffect(() => () => clearTimeout(addedTimerRef.current), []);\n\n const handleAddToCart = useCallback(async () => {\n if (isSubmitting) return;\n\n setIsSubmitting(true);\n\n const options: AddToCartOptions = {\n variantId: selectedVariantId,\n variant: selectedVariant\n ? { id: selectedVariant.id, name: selectedVariant.name || \"\", price_adjustment: selectedVariant.price_adjustment }\n : undefined,\n quoteId,\n addOnOptionIds: normalizedAddOnOptionIds.length > 0 ? normalizedAddOnOptionIds : undefined,\n addOnOptions: selectedAddOnOptions.length > 0\n ? selectedAddOnOptions.map((opt) => ({\n id: opt.id,\n name: opt.name,\n add_on_id: opt.add_on_id,\n default_price: opt.default_price,\n }))\n : undefined,\n compositeSelections: isComposite && compositeSelections.length > 0 ? compositeSelections : undefined,\n bundleSelections: isBundle && bundleSelections.length > 0 ? bundleSelections : undefined,\n billingPlanId: selectedBillingPlan?.id,\n ...(isService && selectedSlot\n ? {\n scheduledStart: selectedSlot.start_time,\n scheduledEnd: selectedSlot.end_time,\n staffId: selectedSlot.available_staff?.[0]?.staff_id,\n resourceId: selectedSlot.available_resources?.[0]?.resource_id,\n }\n : {}),\n customerInputs: Object.keys(customerInputValues).length > 0\n ? Object.entries(customerInputValues)\n .filter(([, v]) => v !== undefined && v !== \"\")\n .map(([fieldId, value]) => ({ field_id: fieldId, value }))\n : undefined,\n specialInstructions: specialInstructions.trim() || undefined,\n };\n\n try {\n if (onAddToCart) {\n await onAddToCart(product, quantity, options);\n } else {\n await cart.addItem(product, quantity, options);\n }\n setIsAdded(true);\n clearTimeout(addedTimerRef.current);\n addedTimerRef.current = setTimeout(() => {\n setIsAdded(false);\n setQuantity(product.min_order_quantity ?? 1);\n }, 2000);\n } catch {\n // Caller handles errors via onAddToCart or cart hook error state\n } finally {\n setIsSubmitting(false);\n }\n }, [product, quantity, selectedVariantId, selectedVariant, quoteId, normalizedAddOnOptionIds, selectedAddOnOptions, isComposite, compositeSelections, isBundle, bundleSelections, selectedBillingPlan, customerInputValues, specialInstructions, isService, selectedSlot, isSubmitting, onAddToCart, cart]);\n\n return (\n <div data-cimplify-customizer className={cn(\"space-y-6\", className, classNames?.root)}>\n {isComposite && product.groups && product.composite_id && (\n <CompositeSelector\n compositeId={product.composite_id}\n groups={product.groups}\n currency={currency}\n onSelectionsChange={setCompositeSelections}\n onPriceChange={setCompositePrice}\n onReady={setCompositeReady}\n skipPriceFetch\n />\n )}\n\n {isBundle && product.components && (\n <BundleSelector\n components={product.components}\n bundlePrice={product.bundle_price}\n discountValue={product.discount_value}\n pricingType={product.pricing_type}\n currency={currency}\n onSelectionsChange={setBundleSelections}\n onPriceChange={setBundleTotalPrice}\n onReady={setBundleReady}\n />\n )}\n\n {isService && (\n <DateSlotPicker\n serviceId={product.id}\n selectedSlot={selectedSlot}\n onSlotSelect={(slot) => setSelectedSlot(slot)}\n participantCount={quantity}\n currency={currency}\n />\n )}\n\n {hasVariants && (\n <VariantSelector\n variants={product.variants!}\n variantAxes={product.variant_axes}\n basePrice={product.default_price}\n currency={currency}\n selectedVariantId={selectedVariantId}\n onVariantChange={handleVariantChange}\n productName={product.name}\n />\n )}\n\n {hasAddOns && (\n <AddOnSelector\n addOns={product.add_ons!}\n selectedOptions={selectedAddOnOptionIds}\n onOptionsChange={setSelectedAddOnOptionIds}\n currency={currency}\n />\n )}\n\n {/* Billing plans */}\n {product.billing_plans && product.billing_plans.length > 0 && (\n <BillingPlanSelector\n productId={product.id}\n plans={product.billing_plans}\n currency={currency}\n onPlanSelect={setSelectedBillingPlan}\n selectedPlanId={selectedBillingPlan?.id ?? null}\n showOneTimePurchase\n />\n )}\n\n {/* Volume pricing tiers */}\n {product.quantity_pricing && product.quantity_pricing.length > 1 && (\n <VolumePricing\n tiers={product.quantity_pricing}\n currentQuantity={quantity}\n currency={currency}\n />\n )}\n\n {/* Customer input fields */}\n {product.input_fields && product.input_fields.length > 0 && (\n <CustomerInputFields\n fields={product.input_fields}\n values={customerInputValues}\n onChange={setCustomerInputValues}\n currency={currency}\n />\n )}\n\n {/* Deposit info for service products */}\n {product.deposit_type && product.deposit_type !== \"none\" && product.deposit_amount && (\n <div data-cimplify-customizer-deposit className={cn(\"text-sm text-muted-foreground\", classNames?.depositInfo)}>\n {product.deposit_type === \"fixed\" ? (\n <span>Deposit required: <Price amount={product.deposit_amount} currency={currency} /></span>\n ) : (\n <span>Deposit required: {parsePrice(product.deposit_amount)}%</span>\n )}\n </div>\n )}\n\n {/* Allergens */}\n {product.allergies && product.allergies.length > 0 && (\n <div data-cimplify-customizer-allergens className={cn(\"flex flex-wrap gap-1.5\", classNames?.allergens)}>\n {product.allergies.map((allergen) => (\n <span\n key={allergen}\n data-cimplify-allergen-tag\n className=\"inline-block text-xs px-2 py-0.5 rounded-full bg-muted text-muted-foreground\"\n >\n {allergen}\n </span>\n ))}\n </div>\n )}\n\n {/* Service duration */}\n {isService && product.duration_minutes != null && (\n <div data-cimplify-customizer-duration className={cn(\"text-sm text-muted-foreground\", classNames?.duration)}>\n Duration: {formatDuration(product.duration_minutes, product.duration_unit)}\n </div>\n )}\n\n {/* Special instructions */}\n {showSpecialInstructions && !isDigital && (\n <div data-cimplify-customizer-special-instructions>\n <textarea\n value={specialInstructions}\n onChange={(e) => setSpecialInstructions(e.target.value)}\n placeholder=\"Special instructions (e.g., no onions, extra sauce)\"\n rows={2}\n data-cimplify-customizer-textarea\n className={cn(\n \"w-full rounded-md border border-input bg-background px-3 py-2 text-sm placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring resize-none\",\n classNames?.specialInstructions,\n )}\n />\n </div>\n )}\n\n <div\n data-cimplify-customizer-actions\n className={cn(\"pt-4 border-t border-border\", classNames?.actions)}\n >\n {!quoteEnabled && (\n <p\n id=\"cimplify-customizer-validation\"\n data-cimplify-customizer-validation\n className={cn(\"text-sm text-destructive mb-3\", classNames?.validation)}\n >\n Please select all required options\n </p>\n )}\n <div className=\"flex items-center gap-4\">\n <div className=\"flex flex-col gap-1\">\n <QuantitySelector\n value={quantity}\n onChange={setQuantity}\n min={product.min_order_quantity ?? 1}\n />\n {product.min_order_quantity && product.min_order_quantity > 1 && (\n <span data-cimplify-customizer-min-hint className=\"text-xs text-muted-foreground text-center\">\n Min. {product.min_order_quantity}\n </span>\n )}\n </div>\n\n {product.quantity_pricing && product.quantity_pricing.length > 0 && (\n <div data-cimplify-customizer-unit-price className=\"text-sm text-muted-foreground text-right shrink-0\">\n <Price amount={getUnitPriceAtQuantity(product.quantity_pricing, quantity, parsePrice(product.default_price))} currency={currency} className=\"font-medium text-foreground\" /> ea.\n </div>\n )}\n\n <Button\n onClick={handleAddToCart}\n disabled={isAdded || isSubmitting || !quoteEnabled}\n aria-describedby={!quoteEnabled ? \"cimplify-customizer-validation\" : undefined}\n data-cimplify-customizer-submit\n className={cn(\n \"flex-1 h-14 text-base bg-primary text-primary-foreground font-medium hover:bg-primary/90 transition-colors disabled:opacity-50 disabled:cursor-not-allowed rounded-full\",\n isAdded && classNames?.submitButtonAdded,\n classNames?.submitButton,\n )}\n >\n {isAdded ? \"Added to Cart\" : (\n <>\n Add to Cart · <Price amount={displayTotalPrice} currency={currency} />\n </>\n )}\n </Button>\n </div>\n </div>\n\n </div>\n );\n}\n"
|
|
17
|
+
"content": "\"use client\";\n\nimport React, { useState, useCallback, useMemo, useEffect, useRef } from \"react\";\nimport { Button } from \"@base-ui/react/button\";\nimport type {\n ProductWithDetails,\n VariantView,\n AddOnOption,\n ComponentSelectionInput,\n CompositePriceResult,\n} from \"@cimplify/sdk\";\nimport type { ProductBillingPlan } from \"@cimplify/sdk\";\nimport type { BundleSelectionInput } from \"@cimplify/sdk\";\nimport { parsePrice, getUnitPriceAtQuantity, getProductCurrency } from \"@cimplify/sdk\";\nimport { formatDuration } from \"./utils/format-duration\";\nimport { useCart, useQuote } from \"@cimplify/sdk/react\";\nimport type { AddToCartOptions } from \"@cimplify/sdk/react\";\nimport { Price } from \"@cimplify/sdk/react\";\nimport { QuantitySelector } from \"@cimplify/sdk/react\";\nimport { VariantSelector } from \"@cimplify/sdk/react\";\nimport { AddOnSelector } from \"@cimplify/sdk/react\";\nimport { StaffPicker } from \"@cimplify/sdk/react\";\nimport { BookingSummary } from \"./booking-summary\";\nimport type { Staff } from \"@cimplify/sdk\";\nimport { CompositeSelector } from \"@cimplify/sdk/react\";\nimport { BundleSelector } from \"@cimplify/sdk/react\";\nimport { BillingPlanSelector } from \"@cimplify/sdk/react\";\nimport { VolumePricing } from \"@cimplify/sdk/react\";\nimport { CustomerInputFields } from \"./customer-input-fields\";\nimport { DateSlotPicker } from \"@cimplify/sdk/react\";\nimport type { AvailableSlot } from \"@cimplify/sdk\";\nimport { cn } from \"@cimplify/sdk/react\";\n\nexport interface ProductCustomizerClassNames {\n root?: string;\n actions?: string;\n submitButton?: string;\n submitButtonAdded?: string;\n validation?: string;\n specialInstructions?: string;\n depositInfo?: string;\n allergens?: string;\n duration?: string;\n}\n\nexport interface ProductCustomizerProps {\n product: ProductWithDetails;\n onAddToCart?: (\n product: ProductWithDetails,\n quantity: number,\n options: AddToCartOptions,\n ) => void | Promise<void>;\n /** Lets the parent swap its gallery for `variant.images` on selection. */\n onVariantChange?: (\n variantId: string | undefined,\n variant: VariantView | undefined,\n ) => void;\n showSpecialInstructions?: boolean;\n className?: string;\n classNames?: ProductCustomizerClassNames;\n}\n\nexport function ProductCustomizer({\n product,\n onAddToCart,\n onVariantChange,\n showSpecialInstructions = true,\n className,\n classNames,\n}: ProductCustomizerProps): React.ReactElement {\n const [quantity, setQuantity] = useState(product.min_order_quantity ?? 1);\n const [isAdded, setIsAdded] = useState(false);\n const [isSubmitting, setIsSubmitting] = useState(false);\n const [selectedVariantId, setSelectedVariantId] = useState<string | undefined>();\n const [selectedVariant, setSelectedVariant] = useState<VariantView | undefined>();\n const [selectedAddOnOptionIds, setSelectedAddOnOptionIds] = useState<string[]>([]);\n\n const [compositeSelections, setCompositeSelections] = useState<ComponentSelectionInput[]>([]);\n const [compositePrice, setCompositePrice] = useState<CompositePriceResult | null>(null);\n const [compositeReady, setCompositeReady] = useState(false);\n\n const [bundleSelections, setBundleSelections] = useState<BundleSelectionInput[]>([]);\n const [bundleTotalPrice, setBundleTotalPrice] = useState<number | null>(null);\n const [bundleReady, setBundleReady] = useState(false);\n const [selectedBillingPlan, setSelectedBillingPlan] = useState<ProductBillingPlan | null>(null);\n const [customerInputValues, setCustomerInputValues] = useState<Record<string, unknown>>({});\n const [specialInstructions, setSpecialInstructions] = useState(\"\");\n const [selectedSlot, setSelectedSlot] = useState<AvailableSlot | null>(null);\n const [selectedStaffId, setSelectedStaffId] = useState<string | null>(null);\n\n const cart = useCart();\n\n const currency = getProductCurrency(product);\n const productType = product.type || \"product\";\n const isComposite = productType === \"composite\";\n const isBundle = productType === \"bundle\";\n const isDigital = productType === \"digital\";\n const isService = productType === \"service\";\n const isStandard = !isComposite && !isBundle;\n\n const hasVariants = isStandard && product.variants && product.variants.length > 0;\n const hasAddOns = isStandard && product.add_ons && product.add_ons.length > 0;\n\n useEffect(() => {\n setQuantity(product.min_order_quantity ?? 1);\n setIsAdded(false);\n setIsSubmitting(false);\n setSelectedVariantId(undefined);\n setSelectedVariant(undefined);\n setSelectedAddOnOptionIds([]);\n setCompositeSelections([]);\n setCompositePrice(null);\n setCompositeReady(false);\n setBundleSelections([]);\n setBundleTotalPrice(null);\n setBundleReady(false);\n setSelectedBillingPlan(null);\n setCustomerInputValues({});\n setSpecialInstructions(\"\");\n setSelectedSlot(null);\n setSelectedStaffId(null);\n }, [product.id, product.min_order_quantity]);\n\n // Staff offered for the chosen slot. SlotStaffInfo carries id + name; richer\n // profiles (avatars/bios) would come from a staff roster fetch.\n const slotStaff = useMemo<Staff[]>(\n () =>\n (selectedSlot?.available_staff ?? []).map((member) => ({\n id: member.staff_id,\n name: member.name,\n })),\n [selectedSlot],\n );\n const selectedStaffName = selectedStaffId\n ? (slotStaff.find((member) => member.id === selectedStaffId)?.name ?? null)\n : null;\n\n const selectedAddOnOptions = useMemo(() => {\n if (!product.add_ons) return [];\n const options: AddOnOption[] = [];\n for (const addOn of product.add_ons) {\n for (const option of addOn.options) {\n if (selectedAddOnOptionIds.includes(option.id)) {\n options.push(option);\n }\n }\n }\n return options;\n }, [product.add_ons, selectedAddOnOptionIds]);\n\n const normalizedAddOnOptionIds = useMemo(() => {\n if (selectedAddOnOptionIds.length === 0) return [];\n return Array.from(new Set(selectedAddOnOptionIds.map((id) => id.trim()).filter(Boolean))).sort();\n }, [selectedAddOnOptionIds]);\n\n const localTotalPrice = useMemo(() => {\n if (isComposite && compositePrice) {\n return parsePrice(compositePrice.final_price) * quantity;\n }\n\n if (isBundle && bundleTotalPrice != null) {\n return bundleTotalPrice * quantity;\n }\n\n let price = parsePrice(product.default_price);\n\n if (selectedVariant?.price_adjustment) {\n price += parsePrice(selectedVariant.price_adjustment);\n }\n\n for (const option of selectedAddOnOptions) {\n if (option.default_price) {\n price += parsePrice(option.default_price);\n }\n }\n\n return price * quantity;\n }, [product.default_price, selectedVariant, selectedAddOnOptions, quantity, isComposite, compositePrice, isBundle, bundleTotalPrice]);\n\n const requiredAddOnsSatisfied = useMemo(() => {\n if (!product.add_ons) return true;\n\n for (const addOn of product.add_ons) {\n if (addOn.is_required) {\n const selectedInGroup = selectedAddOnOptionIds.filter((id) =>\n addOn.options.some((opt) => opt.id === id),\n ).length;\n\n const minRequired = addOn.min_selections || 1;\n if (selectedInGroup < minRequired) {\n return false;\n }\n }\n }\n return true;\n }, [product.add_ons, selectedAddOnOptionIds]);\n\n const quoteInput = useMemo(\n () => ({\n productId: product.id,\n quantity,\n variantId: selectedVariantId,\n addOnOptionIds: normalizedAddOnOptionIds.length > 0 ? normalizedAddOnOptionIds : undefined,\n bundleSelections: isBundle && bundleSelections.length > 0 ? bundleSelections : undefined,\n compositeSelections: isComposite && compositeSelections.length > 0 ? compositeSelections : undefined,\n }),\n [product.id, quantity, selectedVariantId, normalizedAddOnOptionIds, isBundle, bundleSelections, isComposite, compositeSelections],\n );\n\n const requiredInputsSatisfied = useMemo(() => {\n if (!product.input_fields || product.input_fields.length === 0) return true;\n return product.input_fields.every((field) => {\n if (!field.is_required) return true;\n const val = customerInputValues[field.id];\n return val !== undefined && val !== \"\" && val !== null;\n });\n }, [product.input_fields, customerInputValues]);\n\n const quoteEnabled = isComposite\n ? compositeReady && requiredInputsSatisfied\n : isBundle\n ? bundleReady && requiredInputsSatisfied\n : requiredAddOnsSatisfied && requiredInputsSatisfied;\n\n const { quote } = useQuote(quoteInput, {\n enabled: quoteEnabled,\n });\n\n const quoteId = quote?.quote_id;\n const quotedTotalPrice = useMemo(() => {\n if (!quote) return undefined;\n\n const priceInfo = quote.quoted_total_price_info ?? quote.final_price_info;\n const perUnit = priceInfo.pre_tax_price;\n if (perUnit === undefined || perUnit === null) return undefined;\n return parsePrice(perUnit) * quantity;\n }, [quote, quantity]);\n\n const displayTotalPrice = quotedTotalPrice ?? localTotalPrice;\n\n const handleVariantChange = useCallback(\n (variantId: string | undefined, variant: VariantView | undefined) => {\n setSelectedVariantId(variantId);\n setSelectedVariant(variant);\n onVariantChange?.(variantId, variant);\n },\n [onVariantChange],\n );\n\n const addedTimerRef = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);\n useEffect(() => () => clearTimeout(addedTimerRef.current), []);\n\n const handleAddToCart = useCallback(async () => {\n if (isSubmitting) return;\n\n setIsSubmitting(true);\n\n const options: AddToCartOptions = {\n variantId: selectedVariantId,\n variant: selectedVariant\n ? { id: selectedVariant.id, name: selectedVariant.name || \"\", price_adjustment: selectedVariant.price_adjustment }\n : undefined,\n quoteId,\n addOnOptionIds: normalizedAddOnOptionIds.length > 0 ? normalizedAddOnOptionIds : undefined,\n addOnOptions: selectedAddOnOptions.length > 0\n ? selectedAddOnOptions.map((opt) => ({\n id: opt.id,\n name: opt.name,\n add_on_id: opt.add_on_id,\n default_price: opt.default_price,\n }))\n : undefined,\n compositeSelections: isComposite && compositeSelections.length > 0 ? compositeSelections : undefined,\n bundleSelections: isBundle && bundleSelections.length > 0 ? bundleSelections : undefined,\n billingPlanId: selectedBillingPlan?.id,\n ...(isService && selectedSlot\n ? {\n scheduledStart: selectedSlot.start_time,\n scheduledEnd: selectedSlot.end_time,\n staffId: selectedStaffId ?? selectedSlot.available_staff?.[0]?.staff_id,\n resourceId: selectedSlot.available_resources?.[0]?.resource_id,\n }\n : {}),\n customerInputs: Object.keys(customerInputValues).length > 0\n ? Object.entries(customerInputValues)\n .filter(([, v]) => v !== undefined && v !== \"\")\n .map(([fieldId, value]) => ({ field_id: fieldId, value }))\n : undefined,\n specialInstructions: specialInstructions.trim() || undefined,\n };\n\n try {\n if (onAddToCart) {\n await onAddToCart(product, quantity, options);\n } else {\n await cart.addItem(product, quantity, options);\n }\n setIsAdded(true);\n clearTimeout(addedTimerRef.current);\n addedTimerRef.current = setTimeout(() => {\n setIsAdded(false);\n setQuantity(product.min_order_quantity ?? 1);\n }, 2000);\n } catch {\n // Caller handles errors via onAddToCart or cart hook error state\n } finally {\n setIsSubmitting(false);\n }\n }, [product, quantity, selectedVariantId, selectedVariant, quoteId, normalizedAddOnOptionIds, selectedAddOnOptions, isComposite, compositeSelections, isBundle, bundleSelections, selectedBillingPlan, customerInputValues, specialInstructions, isService, selectedSlot, selectedStaffId, isSubmitting, onAddToCart, cart]);\n\n return (\n <div data-cimplify-customizer className={cn(\"space-y-6\", className, classNames?.root)}>\n {isComposite && product.groups && product.composite_id && (\n <CompositeSelector\n compositeId={product.composite_id}\n groups={product.groups}\n currency={currency}\n onSelectionsChange={setCompositeSelections}\n onPriceChange={setCompositePrice}\n onReady={setCompositeReady}\n skipPriceFetch\n />\n )}\n\n {isBundle && product.components && (\n <BundleSelector\n components={product.components}\n bundlePrice={product.bundle_price}\n discountValue={product.discount_value}\n pricingType={product.pricing_type}\n currency={currency}\n onSelectionsChange={setBundleSelections}\n onPriceChange={setBundleTotalPrice}\n onReady={setBundleReady}\n />\n )}\n\n {isService && (\n <DateSlotPicker\n serviceId={product.id}\n selectedSlot={selectedSlot}\n onSlotSelect={(slot) => {\n setSelectedSlot(slot);\n setSelectedStaffId(null);\n }}\n participantCount={quantity}\n currency={currency}\n />\n )}\n\n {isService && slotStaff.length > 0 && (\n <div data-cimplify-customizer-staff>\n <label className=\"block text-xs font-medium uppercase tracking-wider text-muted-foreground mb-3\">\n Choose your specialist\n </label>\n <StaffPicker\n staff={slotStaff}\n selectedStaffId={selectedStaffId}\n onStaffSelect={setSelectedStaffId}\n className=\"flex flex-wrap gap-2\"\n classNames={{\n option:\n \"inline-flex items-center gap-2 rounded-md border border-border px-3 py-2 text-sm font-medium transition-colors hover:border-foreground/40 cursor-pointer data-[selected]:border-foreground data-[selected]:bg-foreground data-[selected]:text-background\",\n avatar: \"w-6 h-6 rounded-full object-cover\",\n }}\n />\n </div>\n )}\n\n {hasVariants && (\n <VariantSelector\n variants={product.variants!}\n variantAxes={product.variant_axes}\n basePrice={product.default_price}\n currency={currency}\n selectedVariantId={selectedVariantId}\n onVariantChange={handleVariantChange}\n productName={product.name}\n />\n )}\n\n {hasAddOns && (\n <AddOnSelector\n addOns={product.add_ons!}\n selectedOptions={selectedAddOnOptionIds}\n onOptionsChange={setSelectedAddOnOptionIds}\n currency={currency}\n />\n )}\n\n {/* Billing plans */}\n {product.billing_plans && product.billing_plans.length > 0 && (\n <BillingPlanSelector\n productId={product.id}\n plans={product.billing_plans}\n currency={currency}\n onPlanSelect={setSelectedBillingPlan}\n selectedPlanId={selectedBillingPlan?.id ?? null}\n showOneTimePurchase\n />\n )}\n\n {/* Volume pricing tiers */}\n {product.quantity_pricing && product.quantity_pricing.length > 1 && (\n <VolumePricing\n tiers={product.quantity_pricing}\n currentQuantity={quantity}\n currency={currency}\n />\n )}\n\n {/* Customer input fields */}\n {product.input_fields && product.input_fields.length > 0 && (\n <CustomerInputFields\n fields={product.input_fields}\n values={customerInputValues}\n onChange={setCustomerInputValues}\n currency={currency}\n />\n )}\n\n {/* Deposit info for service products */}\n {product.deposit_type && product.deposit_type !== \"none\" && product.deposit_amount && (\n <div data-cimplify-customizer-deposit className={cn(\"text-sm text-muted-foreground\", classNames?.depositInfo)}>\n {product.deposit_type === \"fixed\" ? (\n <span>Deposit required: <Price amount={product.deposit_amount} currency={currency} /></span>\n ) : (\n <span>Deposit required: {parsePrice(product.deposit_amount)}%</span>\n )}\n </div>\n )}\n\n {/* Allergens */}\n {product.allergies && product.allergies.length > 0 && (\n <div data-cimplify-customizer-allergens className={cn(\"flex flex-wrap gap-1.5\", classNames?.allergens)}>\n {product.allergies.map((allergen) => (\n <span\n key={allergen}\n data-cimplify-allergen-tag\n className=\"inline-block text-xs px-2 py-0.5 rounded-full bg-muted text-muted-foreground\"\n >\n {allergen}\n </span>\n ))}\n </div>\n )}\n\n {/* Service duration */}\n {isService && product.duration_minutes != null && (\n <div data-cimplify-customizer-duration className={cn(\"text-sm text-muted-foreground\", classNames?.duration)}>\n Duration: {formatDuration(product.duration_minutes, product.duration_unit)}\n </div>\n )}\n\n {/* Special instructions */}\n {showSpecialInstructions && !isDigital && (\n <div data-cimplify-customizer-special-instructions>\n <textarea\n value={specialInstructions}\n onChange={(e) => setSpecialInstructions(e.target.value)}\n placeholder=\"Special instructions (e.g., no onions, extra sauce)\"\n rows={2}\n data-cimplify-customizer-textarea\n className={cn(\n \"w-full rounded-md border border-input bg-background px-3 py-2 text-sm placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring resize-none\",\n classNames?.specialInstructions,\n )}\n />\n </div>\n )}\n\n {isService && selectedSlot && (\n <BookingSummary\n slot={selectedSlot}\n total={displayTotalPrice}\n staffName={selectedStaffName}\n durationMinutes={product.duration_minutes}\n depositType={product.deposit_type}\n depositAmount={product.deposit_amount}\n currency={currency}\n />\n )}\n\n <div\n data-cimplify-customizer-actions\n className={cn(\"pt-4 border-t border-border\", classNames?.actions)}\n >\n {!quoteEnabled && (\n <p\n id=\"cimplify-customizer-validation\"\n data-cimplify-customizer-validation\n className={cn(\"text-sm text-destructive mb-3\", classNames?.validation)}\n >\n Please select all required options\n </p>\n )}\n <div className=\"flex items-center gap-4\">\n <div className=\"flex flex-col gap-1\">\n <QuantitySelector\n value={quantity}\n onChange={setQuantity}\n min={product.min_order_quantity ?? 1}\n />\n {product.min_order_quantity && product.min_order_quantity > 1 && (\n <span data-cimplify-customizer-min-hint className=\"text-xs text-muted-foreground text-center\">\n Min. {product.min_order_quantity}\n </span>\n )}\n </div>\n\n {product.quantity_pricing && product.quantity_pricing.length > 0 && (\n <div data-cimplify-customizer-unit-price className=\"text-sm text-muted-foreground text-right shrink-0\">\n <Price amount={getUnitPriceAtQuantity(product.quantity_pricing, quantity, parsePrice(product.default_price))} currency={currency} className=\"font-medium text-foreground\" /> ea.\n </div>\n )}\n\n <Button\n onClick={handleAddToCart}\n disabled={isAdded || isSubmitting || !quoteEnabled}\n aria-describedby={!quoteEnabled ? \"cimplify-customizer-validation\" : undefined}\n data-cimplify-customizer-submit\n className={cn(\n \"flex-1 h-14 text-base bg-primary text-primary-foreground font-medium hover:bg-primary/90 transition-colors disabled:opacity-50 disabled:cursor-not-allowed rounded-full\",\n isAdded && classNames?.submitButtonAdded,\n classNames?.submitButton,\n )}\n >\n {isAdded ? \"Added to Cart\" : (\n <>\n Add to Cart · <Price amount={displayTotalPrice} currency={currency} />\n </>\n )}\n </Button>\n </div>\n </div>\n\n </div>\n );\n}\n"
|
|
18
18
|
}
|
|
19
19
|
]
|
|
20
20
|
}
|