@resira/ui 0.4.14 → 0.4.16
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/index.cjs +132 -21
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +22 -2
- package/dist/index.d.ts +22 -2
- package/dist/index.js +132 -22
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -783,6 +783,28 @@ function useCheckoutSession(token) {
|
|
|
783
783
|
}, [client, token]);
|
|
784
784
|
return { session, loading, error, errorCode };
|
|
785
785
|
}
|
|
786
|
+
function normalizeMinutes(value) {
|
|
787
|
+
if (typeof value === "number" && Number.isFinite(value) && value > 0) return value;
|
|
788
|
+
if (typeof value === "string" && value.trim()) {
|
|
789
|
+
const parsed = Number(value);
|
|
790
|
+
if (Number.isFinite(parsed) && parsed > 0) return parsed;
|
|
791
|
+
}
|
|
792
|
+
return void 0;
|
|
793
|
+
}
|
|
794
|
+
function resolveServicePrice(service, params = {}) {
|
|
795
|
+
const partySize = Math.max(1, Number(params.partySize ?? 1));
|
|
796
|
+
const selectedMinutes = normalizeMinutes(params.durationMinutes);
|
|
797
|
+
const options = Array.isArray(service.options) ? service.options : [];
|
|
798
|
+
const matchedOption = selectedMinutes ? options.find((option) => option.durationMinutes === selectedMinutes) : void 0;
|
|
799
|
+
const fallbackOption = options.find((option) => option.durationMinutes === service.durationMinutes) ?? options[0];
|
|
800
|
+
const unitPriceCents = matchedOption?.priceCents ?? fallbackOption?.priceCents ?? service.priceCents;
|
|
801
|
+
const totalPriceCents = service.pricingModel === "per_person" ? unitPriceCents * partySize : unitPriceCents;
|
|
802
|
+
return {
|
|
803
|
+
unitPriceCents,
|
|
804
|
+
totalPriceCents,
|
|
805
|
+
matchedDurationMinutes: matchedOption?.durationMinutes ?? fallbackOption?.durationMinutes
|
|
806
|
+
};
|
|
807
|
+
}
|
|
786
808
|
function formatPriceCentsUtil(cents, currency) {
|
|
787
809
|
return new Intl.NumberFormat("default", { style: "currency", currency }).format(cents / 100);
|
|
788
810
|
}
|
|
@@ -1679,6 +1701,7 @@ function TimeSlotPicker({
|
|
|
1679
1701
|
showDuration = false,
|
|
1680
1702
|
selectedDuration,
|
|
1681
1703
|
onDurationChange,
|
|
1704
|
+
minPartySizeOverride,
|
|
1682
1705
|
maxPartySizeOverride,
|
|
1683
1706
|
durationPricing,
|
|
1684
1707
|
currency,
|
|
@@ -1686,7 +1709,7 @@ function TimeSlotPicker({
|
|
|
1686
1709
|
showRemainingSpots = false
|
|
1687
1710
|
}) {
|
|
1688
1711
|
const { locale, domainConfig, domain, classNames } = useResira();
|
|
1689
|
-
const minParty = domainConfig.minPartySize ?? 1;
|
|
1712
|
+
const minParty = minPartySizeOverride ?? domainConfig.minPartySize ?? 1;
|
|
1690
1713
|
const maxParty = maxPartySizeOverride ?? domainConfig.maxPartySize ?? 12;
|
|
1691
1714
|
const durations = react.useMemo(() => {
|
|
1692
1715
|
if (durationPricing && durationPricing.length > 0) {
|
|
@@ -3087,6 +3110,56 @@ function ConfirmationView({ reservation }) {
|
|
|
3087
3110
|
] })
|
|
3088
3111
|
] });
|
|
3089
3112
|
}
|
|
3113
|
+
|
|
3114
|
+
// src/service-rules.ts
|
|
3115
|
+
function isPositiveInt(value) {
|
|
3116
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0;
|
|
3117
|
+
}
|
|
3118
|
+
function getRiderTierBounds(product) {
|
|
3119
|
+
const tiers = product.riderTierPricing ?? [];
|
|
3120
|
+
if (!tiers.length) return {};
|
|
3121
|
+
let min;
|
|
3122
|
+
let max;
|
|
3123
|
+
for (const tier of tiers) {
|
|
3124
|
+
const tierMin = isPositiveInt(tier.minRiders) ? tier.minRiders : isPositiveInt(tier.riders) ? tier.riders : void 0;
|
|
3125
|
+
const tierMax = isPositiveInt(tier.maxRiders) ? tier.maxRiders : isPositiveInt(tier.riders) ? tier.riders : isPositiveInt(tier.minRiders) ? tier.minRiders : void 0;
|
|
3126
|
+
if (isPositiveInt(tierMin)) {
|
|
3127
|
+
min = min == null ? tierMin : Math.min(min, tierMin);
|
|
3128
|
+
}
|
|
3129
|
+
if (isPositiveInt(tierMax)) {
|
|
3130
|
+
max = max == null ? tierMax : Math.max(max, tierMax);
|
|
3131
|
+
}
|
|
3132
|
+
}
|
|
3133
|
+
return { min, max };
|
|
3134
|
+
}
|
|
3135
|
+
function getServicePartySizeBounds(product, domainConfig) {
|
|
3136
|
+
const baseMin = isPositiveInt(domainConfig?.minPartySize) ? domainConfig.minPartySize : 1;
|
|
3137
|
+
const baseMax = isPositiveInt(domainConfig?.maxPartySize) ? domainConfig.maxPartySize : 12;
|
|
3138
|
+
let min = baseMin;
|
|
3139
|
+
let max = baseMax;
|
|
3140
|
+
if (product) {
|
|
3141
|
+
if (isPositiveInt(product.maxPartySize)) {
|
|
3142
|
+
max = Math.min(max, product.maxPartySize);
|
|
3143
|
+
}
|
|
3144
|
+
if (product.pricingModel === "per_rider" && product.riderTierPricing?.length) {
|
|
3145
|
+
const tierBounds = getRiderTierBounds(product);
|
|
3146
|
+
if (isPositiveInt(tierBounds.min)) {
|
|
3147
|
+
min = Math.max(min, tierBounds.min);
|
|
3148
|
+
}
|
|
3149
|
+
if (isPositiveInt(tierBounds.max)) {
|
|
3150
|
+
max = Math.min(max, tierBounds.max);
|
|
3151
|
+
}
|
|
3152
|
+
}
|
|
3153
|
+
}
|
|
3154
|
+
if (max < min) {
|
|
3155
|
+
return { min, max: min };
|
|
3156
|
+
}
|
|
3157
|
+
return { min, max };
|
|
3158
|
+
}
|
|
3159
|
+
function clampPartySize(value, bounds) {
|
|
3160
|
+
if (!Number.isFinite(value)) return bounds.min;
|
|
3161
|
+
return Math.min(bounds.max, Math.max(bounds.min, Math.round(value)));
|
|
3162
|
+
}
|
|
3090
3163
|
function todayStr() {
|
|
3091
3164
|
const d = /* @__PURE__ */ new Date();
|
|
3092
3165
|
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
|
|
@@ -3097,6 +3170,14 @@ function formatPrice5(cents, currency) {
|
|
|
3097
3170
|
currency
|
|
3098
3171
|
}).format(cents / 100);
|
|
3099
3172
|
}
|
|
3173
|
+
function normalizeDurationMinutes(value) {
|
|
3174
|
+
if (typeof value === "number" && Number.isFinite(value) && value > 0) return value;
|
|
3175
|
+
if (typeof value === "string" && value.trim()) {
|
|
3176
|
+
const parsed = Number(value);
|
|
3177
|
+
if (Number.isFinite(parsed) && parsed > 0) return parsed;
|
|
3178
|
+
}
|
|
3179
|
+
return void 0;
|
|
3180
|
+
}
|
|
3100
3181
|
function buildSteps(domain, hasPayment, catalogMode) {
|
|
3101
3182
|
const steps = [];
|
|
3102
3183
|
if (domain === "watersport" || domain === "service" || catalogMode && domain !== "restaurant") {
|
|
@@ -3198,6 +3279,7 @@ function ResiraBookingWidget() {
|
|
|
3198
3279
|
partySize: domainConfig.defaultPartySize ?? 2,
|
|
3199
3280
|
duration: domainConfig.defaultDuration
|
|
3200
3281
|
});
|
|
3282
|
+
const [partySizeByProductId, setPartySizeByProductId] = react.useState({});
|
|
3201
3283
|
const [guest, setGuest] = react.useState({
|
|
3202
3284
|
guestName: "",
|
|
3203
3285
|
guestEmail: "",
|
|
@@ -3214,6 +3296,11 @@ function ResiraBookingWidget() {
|
|
|
3214
3296
|
setPromoValidation(result);
|
|
3215
3297
|
}, []);
|
|
3216
3298
|
const [slotDate, setSlotDate] = react.useState(todayStr());
|
|
3299
|
+
const activeDurationMinutes = normalizeDurationMinutes(selection.duration);
|
|
3300
|
+
const selectedProductPartyBounds = react.useMemo(
|
|
3301
|
+
() => getServicePartySizeBounds(selectedProduct, domainConfig),
|
|
3302
|
+
[selectedProduct, domainConfig]
|
|
3303
|
+
);
|
|
3217
3304
|
const availabilityParams = react.useMemo(() => {
|
|
3218
3305
|
if (isDateBased) {
|
|
3219
3306
|
if (selection.startDate && selection.endDate) {
|
|
@@ -3224,9 +3311,9 @@ function ResiraBookingWidget() {
|
|
|
3224
3311
|
return {
|
|
3225
3312
|
date: slotDate,
|
|
3226
3313
|
partySize: selection.partySize,
|
|
3227
|
-
durationMinutes:
|
|
3314
|
+
durationMinutes: activeDurationMinutes
|
|
3228
3315
|
};
|
|
3229
|
-
}, [isDateBased, selection.startDate, selection.endDate, slotDate, selection.partySize,
|
|
3316
|
+
}, [isDateBased, selection.startDate, selection.endDate, slotDate, selection.partySize, activeDurationMinutes]);
|
|
3230
3317
|
const availabilityProductId = isServiceBased ? selectedProduct?.id : void 0;
|
|
3231
3318
|
const { data: availability, loading, error, refetch } = useAvailability(
|
|
3232
3319
|
availabilityParams,
|
|
@@ -3270,7 +3357,7 @@ function ResiraBookingWidget() {
|
|
|
3270
3357
|
productId: selectedProduct?.id ?? "",
|
|
3271
3358
|
resourceId,
|
|
3272
3359
|
partySize: selection.partySize,
|
|
3273
|
-
durationMinutes:
|
|
3360
|
+
durationMinutes: activeDurationMinutes,
|
|
3274
3361
|
startDate: selection.startDate,
|
|
3275
3362
|
startTime: selection.startTime,
|
|
3276
3363
|
endTime: selection.endTime,
|
|
@@ -3282,7 +3369,7 @@ function ResiraBookingWidget() {
|
|
|
3282
3369
|
termsAccepted: termsAccepted || void 0,
|
|
3283
3370
|
waiverAccepted: waiverAccepted || void 0
|
|
3284
3371
|
};
|
|
3285
|
-
}, [activeResourceId, selectedProduct, selection, guest, discountCode, termsAccepted, waiverAccepted, isCheckoutMode, checkoutSession, checkoutSessionToken]);
|
|
3372
|
+
}, [activeResourceId, selectedProduct, selection, guest, discountCode, termsAccepted, waiverAccepted, isCheckoutMode, checkoutSession, checkoutSessionToken, activeDurationMinutes]);
|
|
3286
3373
|
const blockedDates = react.useMemo(() => {
|
|
3287
3374
|
const dates = calendarData?.dates?.blockedDates ?? availability?.dates?.blockedDates ?? [];
|
|
3288
3375
|
return new Set(dates);
|
|
@@ -3302,11 +3389,11 @@ function ResiraBookingWidget() {
|
|
|
3302
3389
|
if (selectedProduct) {
|
|
3303
3390
|
let base = selectedProduct.priceCents;
|
|
3304
3391
|
if (selectedProduct.pricingModel === "per_rider" && activeRiderDurationPricing?.length) {
|
|
3305
|
-
const match =
|
|
3392
|
+
const match = activeDurationMinutes ? activeRiderDurationPricing.find((dp) => dp.durationMinutes === activeDurationMinutes) : activeRiderDurationPricing[0];
|
|
3306
3393
|
if (match) base = match.priceCents;
|
|
3307
|
-
} else if (selectedProduct.durationPricing?.length &&
|
|
3394
|
+
} else if (selectedProduct.durationPricing?.length && activeDurationMinutes) {
|
|
3308
3395
|
const match = selectedProduct.durationPricing.find(
|
|
3309
|
-
(dp) => dp.durationMinutes ===
|
|
3396
|
+
(dp) => dp.durationMinutes === activeDurationMinutes
|
|
3310
3397
|
);
|
|
3311
3398
|
if (match) base = match.priceCents;
|
|
3312
3399
|
}
|
|
@@ -3324,7 +3411,7 @@ function ResiraBookingWidget() {
|
|
|
3324
3411
|
return { total, amountNow, amountAtVenue };
|
|
3325
3412
|
}
|
|
3326
3413
|
return null;
|
|
3327
|
-
}, [selectedProduct, availability, selection.partySize,
|
|
3414
|
+
}, [selectedProduct, availability, selection.partySize, activeDurationMinutes, depositPercent, activeRiderDurationPricing]);
|
|
3328
3415
|
const stepTitle = react.useMemo(() => {
|
|
3329
3416
|
switch (step) {
|
|
3330
3417
|
case "resource":
|
|
@@ -3411,8 +3498,12 @@ function ResiraBookingWidget() {
|
|
|
3411
3498
|
[slotDate, promoterEnabled, promoterMode.autoAdvanceAvailability, step, STEPS]
|
|
3412
3499
|
);
|
|
3413
3500
|
const handlePartySizeChange = react.useCallback((size) => {
|
|
3414
|
-
|
|
3415
|
-
|
|
3501
|
+
const clamped = clampPartySize(size, selectedProductPartyBounds);
|
|
3502
|
+
setSelection((prev) => ({ ...prev, partySize: clamped }));
|
|
3503
|
+
if (selectedProduct?.id) {
|
|
3504
|
+
setPartySizeByProductId((prev) => ({ ...prev, [selectedProduct.id]: clamped }));
|
|
3505
|
+
}
|
|
3506
|
+
}, [selectedProduct?.id, selectedProductPartyBounds]);
|
|
3416
3507
|
const handleDurationChange = react.useCallback((minutes) => {
|
|
3417
3508
|
setSelection((prev) => ({
|
|
3418
3509
|
...prev,
|
|
@@ -3428,16 +3519,29 @@ function ResiraBookingWidget() {
|
|
|
3428
3519
|
setActiveResourceId(product.equipmentIds[0]);
|
|
3429
3520
|
}
|
|
3430
3521
|
setSelection((prev) => {
|
|
3431
|
-
const
|
|
3432
|
-
const
|
|
3522
|
+
const bounds = getServicePartySizeBounds(product, domainConfig);
|
|
3523
|
+
const persistedPartySize = partySizeByProductId[product.id];
|
|
3524
|
+
const nextPartySize = clampPartySize(
|
|
3525
|
+
persistedPartySize ?? prev.partySize,
|
|
3526
|
+
bounds
|
|
3527
|
+
);
|
|
3433
3528
|
const defaultDuration = product.durationPricing?.[0]?.durationMinutes ?? product.durationMinutes ?? prev.duration;
|
|
3434
3529
|
return {
|
|
3435
3530
|
...prev,
|
|
3436
3531
|
productId: product.id,
|
|
3437
3532
|
duration: defaultDuration,
|
|
3438
|
-
partySize:
|
|
3533
|
+
partySize: nextPartySize
|
|
3439
3534
|
};
|
|
3440
3535
|
});
|
|
3536
|
+
setPartySizeByProductId((prev) => {
|
|
3537
|
+
const bounds = getServicePartySizeBounds(product, domainConfig);
|
|
3538
|
+
const persistedPartySize = prev[product.id];
|
|
3539
|
+
const nextPartySize = clampPartySize(
|
|
3540
|
+
persistedPartySize ?? selection.partySize,
|
|
3541
|
+
bounds
|
|
3542
|
+
);
|
|
3543
|
+
return { ...prev, [product.id]: nextPartySize };
|
|
3544
|
+
});
|
|
3441
3545
|
if (step === "resource" && isServiceBased) {
|
|
3442
3546
|
const nextIdx = stepIndex("resource", STEPS) + 1;
|
|
3443
3547
|
if (nextIdx < STEPS.length) {
|
|
@@ -3445,7 +3549,7 @@ function ResiraBookingWidget() {
|
|
|
3445
3549
|
}
|
|
3446
3550
|
}
|
|
3447
3551
|
},
|
|
3448
|
-
[setActiveResourceId, domainConfig.
|
|
3552
|
+
[setActiveResourceId, domainConfig, partySizeByProductId, selection.partySize, step, isServiceBased, STEPS]
|
|
3449
3553
|
);
|
|
3450
3554
|
const handleResourceSelect = react.useCallback(
|
|
3451
3555
|
(resourceId) => {
|
|
@@ -3645,6 +3749,11 @@ function ResiraBookingWidget() {
|
|
|
3645
3749
|
...deeplink.duration ? { duration: deeplink.duration } : {},
|
|
3646
3750
|
...deeplink.date ? { startDate: deeplink.date } : {}
|
|
3647
3751
|
}));
|
|
3752
|
+
if (deeplink.productId && deeplink.partySize) {
|
|
3753
|
+
const deeplinkProductId = deeplink.productId;
|
|
3754
|
+
const deeplinkPartySize = deeplink.partySize;
|
|
3755
|
+
setPartySizeByProductId((prev) => ({ ...prev, [deeplinkProductId]: deeplinkPartySize }));
|
|
3756
|
+
}
|
|
3648
3757
|
if (deeplink.date) setSlotDate(deeplink.date);
|
|
3649
3758
|
}
|
|
3650
3759
|
if (deeplinkGuest) {
|
|
@@ -3768,9 +3877,10 @@ function ResiraBookingWidget() {
|
|
|
3768
3877
|
onPartySizeChange: handlePartySizeChange,
|
|
3769
3878
|
showPartySize: true,
|
|
3770
3879
|
showDuration: domain === "watersport" || domain === "service",
|
|
3771
|
-
selectedDuration:
|
|
3880
|
+
selectedDuration: activeDurationMinutes,
|
|
3772
3881
|
onDurationChange: handleDurationChange,
|
|
3773
|
-
|
|
3882
|
+
minPartySizeOverride: selectedProductPartyBounds.min,
|
|
3883
|
+
maxPartySizeOverride: selectedProductPartyBounds.max,
|
|
3774
3884
|
durationPricing: activeRiderDurationPricing ?? selectedProduct?.durationPricing,
|
|
3775
3885
|
currency,
|
|
3776
3886
|
showRemainingSpots
|
|
@@ -3780,19 +3890,19 @@ function ResiraBookingWidget() {
|
|
|
3780
3890
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "resira-price-preview-row", children: [
|
|
3781
3891
|
/* @__PURE__ */ jsxRuntime.jsxs("span", { children: [
|
|
3782
3892
|
selectedProduct.name,
|
|
3783
|
-
|
|
3893
|
+
activeDurationMinutes && selectedProduct.durationPricing && selectedProduct.durationPricing.length > 1 && /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "resira-price-preview-label", children: [
|
|
3784
3894
|
" ",
|
|
3785
3895
|
"(",
|
|
3786
|
-
|
|
3896
|
+
activeDurationMinutes < 60 ? `${activeDurationMinutes} min` : `${Math.floor(activeDurationMinutes / 60)}h${activeDurationMinutes % 60 ? activeDurationMinutes % 60 : ""}`,
|
|
3787
3897
|
")"
|
|
3788
3898
|
] })
|
|
3789
3899
|
] }),
|
|
3790
3900
|
/* @__PURE__ */ jsxRuntime.jsxs("span", { children: [
|
|
3791
3901
|
(() => {
|
|
3792
3902
|
let unitPrice = selectedProduct.priceCents;
|
|
3793
|
-
if (selectedProduct.durationPricing?.length &&
|
|
3903
|
+
if (selectedProduct.durationPricing?.length && activeDurationMinutes) {
|
|
3794
3904
|
const match = selectedProduct.durationPricing.find(
|
|
3795
|
-
(dp) => dp.durationMinutes ===
|
|
3905
|
+
(dp) => dp.durationMinutes === activeDurationMinutes
|
|
3796
3906
|
);
|
|
3797
3907
|
if (match) unitPrice = match.priceCents;
|
|
3798
3908
|
}
|
|
@@ -4669,6 +4779,7 @@ exports.ViewfinderIcon = ViewfinderIcon;
|
|
|
4669
4779
|
exports.WaiverConsent = WaiverConsent;
|
|
4670
4780
|
exports.XIcon = XIcon;
|
|
4671
4781
|
exports.fetchServices = fetchServices;
|
|
4782
|
+
exports.resolveServicePrice = resolveServicePrice;
|
|
4672
4783
|
exports.resolveTheme = resolveTheme;
|
|
4673
4784
|
exports.themeToCSS = themeToCSS;
|
|
4674
4785
|
exports.useAvailability = useAvailability;
|