@resira/ui 0.4.17 → 0.4.18

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 CHANGED
@@ -304,6 +304,45 @@ function writeCache(cache, key, value, ttlMs) {
304
304
  expiresAt: Date.now() + Math.max(1e3, ttlMs)
305
305
  });
306
306
  }
307
+ function normalizePositiveInt(value) {
308
+ if (typeof value !== "number" || !Number.isFinite(value)) return null;
309
+ const rounded = Math.round(value);
310
+ return rounded > 0 ? rounded : null;
311
+ }
312
+ function toIsoOrNull(value) {
313
+ if (typeof value !== "string" || !value.trim()) return null;
314
+ const date = new Date(value);
315
+ if (Number.isNaN(date.getTime())) return null;
316
+ return date.toISOString();
317
+ }
318
+ function withCanonicalPaymentWindow(payload) {
319
+ const next = { ...payload };
320
+ const durationMinutes = normalizePositiveInt(next.durationMinutes);
321
+ if (next.durationMinutes != null && !durationMinutes) {
322
+ throw new Error("Invalid duration selected.");
323
+ }
324
+ if (durationMinutes) {
325
+ next.durationMinutes = durationMinutes;
326
+ next.durationHours = Number((durationMinutes / 60).toFixed(6));
327
+ }
328
+ const startIso = toIsoOrNull(next.startTime);
329
+ const endIso = toIsoOrNull(next.endTime);
330
+ if (startIso) next.startTime = startIso;
331
+ if (durationMinutes && startIso) {
332
+ const computedEnd = new Date(
333
+ new Date(startIso).getTime() + durationMinutes * 6e4
334
+ ).toISOString();
335
+ if (!endIso) {
336
+ next.endTime = computedEnd;
337
+ } else {
338
+ const matchesDuration = new Date(endIso).getTime() - new Date(startIso).getTime() === durationMinutes * 6e4;
339
+ next.endTime = matchesDuration ? endIso : computedEnd;
340
+ }
341
+ } else if (endIso) {
342
+ next.endTime = endIso;
343
+ }
344
+ return next;
345
+ }
307
346
  async function getDishCompat(client, dishId) {
308
347
  const maybeClient = client;
309
348
  if (typeof maybeClient.getDish === "function") {
@@ -616,7 +655,8 @@ function usePaymentIntent() {
616
655
  setCreating(true);
617
656
  setError(null);
618
657
  try {
619
- const response = await client.createPaymentIntent(data);
658
+ const payload = withCanonicalPaymentWindow(data);
659
+ const response = await client.createPaymentIntent(payload);
620
660
  setPaymentIntent(response);
621
661
  return response;
622
662
  } catch (err) {
@@ -2955,6 +2995,15 @@ function formatTime(isoStr) {
2955
2995
  return isoStr;
2956
2996
  }
2957
2997
  }
2998
+ function renderTimeRange(start, end) {
2999
+ if (start && end) {
3000
+ return `${formatTime(start)} \u2013 ${formatTime(end)}`;
3001
+ }
3002
+ if (start || end) {
3003
+ return "Time unavailable";
3004
+ }
3005
+ return "Time unavailable";
3006
+ }
2958
3007
  function formatPrice4(cents, currency) {
2959
3008
  return new Intl.NumberFormat("default", {
2960
3009
  style: "currency",
@@ -3008,14 +3057,11 @@ function SummaryPreview({
3008
3057
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: "resira-summary-item-value", children: formatDate(selection.startDate) })
3009
3058
  ] })
3010
3059
  ] }),
3011
- !isDateBased && selection.startTime && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "resira-summary-item", children: [
3060
+ !isDateBased && (selection.startTime || selection.endTime) && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "resira-summary-item", children: [
3012
3061
  /* @__PURE__ */ jsxRuntime.jsx(ClockIcon, { size: 14 }),
3013
3062
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "resira-summary-item-content", children: [
3014
3063
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: "resira-summary-item-label", children: "Time" }),
3015
- /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "resira-summary-item-value", children: [
3016
- formatTime(selection.startTime),
3017
- selection.endTime && ` \u2013 ${formatTime(selection.endTime)}`
3018
- ] })
3064
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "resira-summary-item-value", children: renderTimeRange(selection.startTime, selection.endTime) })
3019
3065
  ] })
3020
3066
  ] }),
3021
3067
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "resira-summary-item", children: [
@@ -3085,14 +3131,11 @@ function ConfirmationView({ reservation }) {
3085
3131
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: "resira-summary-item-value", children: formatDate(reservation.endDate) })
3086
3132
  ] })
3087
3133
  ] }),
3088
- reservation.startTime && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "resira-summary-item", children: [
3134
+ (reservation.startTime || reservation.endTime) && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "resira-summary-item", children: [
3089
3135
  /* @__PURE__ */ jsxRuntime.jsx(ClockIcon, { size: 14 }),
3090
3136
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "resira-summary-item-content", children: [
3091
3137
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: "resira-summary-item-label", children: "Time" }),
3092
- /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "resira-summary-item-value", children: [
3093
- formatTime(reservation.startTime),
3094
- reservation.endTime && ` \u2013 ${formatTime(reservation.endTime)}`
3095
- ] })
3138
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "resira-summary-item-value", children: renderTimeRange(reservation.startTime, reservation.endTime) })
3096
3139
  ] })
3097
3140
  ] }),
3098
3141
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "resira-summary-item", children: [
@@ -3280,6 +3323,9 @@ function ResiraBookingWidget() {
3280
3323
  duration: domainConfig.defaultDuration
3281
3324
  });
3282
3325
  const [partySizeByProductId, setPartySizeByProductId] = react.useState({});
3326
+ const slotByProductRef = react.useRef({});
3327
+ const selectionRef = react.useRef(selection);
3328
+ selectionRef.current = selection;
3283
3329
  const [guest, setGuest] = react.useState({
3284
3330
  guestName: "",
3285
3331
  guestEmail: "",
@@ -3296,6 +3342,25 @@ function ResiraBookingWidget() {
3296
3342
  setPromoValidation(result);
3297
3343
  }, []);
3298
3344
  const [slotDate, setSlotDate] = react.useState(todayStr());
3345
+ const handleSlotDateChange = react.useCallback(
3346
+ (date) => {
3347
+ setSlotDate(date);
3348
+ setSelection((s) => {
3349
+ const next = { ...s, startDate: date, endDate: date, startTime: void 0, endTime: void 0 };
3350
+ if (selectedProduct?.id) {
3351
+ slotByProductRef.current[selectedProduct.id] = {
3352
+ ...slotByProductRef.current[selectedProduct.id],
3353
+ startDate: date,
3354
+ endDate: date,
3355
+ startTime: void 0,
3356
+ endTime: void 0
3357
+ };
3358
+ }
3359
+ return next;
3360
+ });
3361
+ },
3362
+ [selectedProduct?.id]
3363
+ );
3299
3364
  const activeDurationMinutes = normalizeDurationMinutes(selection.duration);
3300
3365
  const selectedProductPartyBounds = react.useMemo(
3301
3366
  () => getServicePartySizeBounds(selectedProduct, domainConfig),
@@ -3481,13 +3546,22 @@ function ResiraBookingWidget() {
3481
3546
  );
3482
3547
  const handleSlotSelect = react.useCallback(
3483
3548
  (start, end) => {
3484
- setSelection((prev) => ({
3485
- ...prev,
3486
- startDate: slotDate,
3487
- endDate: slotDate,
3488
- startTime: start,
3489
- endTime: end
3490
- }));
3549
+ setSelection((prev) => {
3550
+ const next = {
3551
+ ...prev,
3552
+ startDate: slotDate,
3553
+ endDate: slotDate,
3554
+ startTime: start,
3555
+ endTime: end
3556
+ };
3557
+ if (selectedProduct?.id) {
3558
+ slotByProductRef.current[selectedProduct.id] = {
3559
+ ...slotByProductRef.current[selectedProduct.id],
3560
+ ...next
3561
+ };
3562
+ }
3563
+ return next;
3564
+ });
3491
3565
  if (promoterEnabled && promoterMode.autoAdvanceAvailability && step === "availability") {
3492
3566
  const nextIdx = stepIndex(step, STEPS) + 1;
3493
3567
  if (nextIdx < STEPS.length) {
@@ -3495,7 +3569,7 @@ function ResiraBookingWidget() {
3495
3569
  }
3496
3570
  }
3497
3571
  },
3498
- [slotDate, promoterEnabled, promoterMode.autoAdvanceAvailability, step, STEPS]
3572
+ [slotDate, promoterEnabled, promoterMode.autoAdvanceAvailability, step, STEPS, selectedProduct?.id]
3499
3573
  );
3500
3574
  const handlePartySizeChange = react.useCallback((size) => {
3501
3575
  const clamped = clampPartySize(size, selectedProductPartyBounds);
@@ -3505,15 +3579,40 @@ function ResiraBookingWidget() {
3505
3579
  }
3506
3580
  }, [selectedProduct?.id, selectedProductPartyBounds]);
3507
3581
  const handleDurationChange = react.useCallback((minutes) => {
3508
- setSelection((prev) => ({
3509
- ...prev,
3510
- duration: minutes,
3511
- startTime: void 0,
3512
- endTime: void 0
3513
- }));
3514
- }, []);
3582
+ setSelection((prev) => {
3583
+ const next = {
3584
+ ...prev,
3585
+ duration: minutes,
3586
+ startTime: void 0,
3587
+ endTime: void 0
3588
+ };
3589
+ if (selectedProduct?.id) {
3590
+ slotByProductRef.current[selectedProduct.id] = {
3591
+ ...slotByProductRef.current[selectedProduct.id],
3592
+ duration: minutes,
3593
+ startTime: void 0,
3594
+ endTime: void 0
3595
+ };
3596
+ }
3597
+ return next;
3598
+ });
3599
+ }, [selectedProduct?.id]);
3515
3600
  const handleProductSelect = react.useCallback(
3516
3601
  (product) => {
3602
+ if (selectedProduct?.id) {
3603
+ const s = selectionRef.current;
3604
+ slotByProductRef.current[selectedProduct.id] = {
3605
+ startTime: s.startTime,
3606
+ endTime: s.endTime,
3607
+ startDate: s.startDate,
3608
+ endDate: s.endDate,
3609
+ duration: s.duration
3610
+ };
3611
+ }
3612
+ const saved = slotByProductRef.current[product.id] ?? {};
3613
+ if (saved.startDate && typeof saved.startDate === "string" && saved.startDate.length >= 10) {
3614
+ setSlotDate(saved.startDate.slice(0, 10));
3615
+ }
3517
3616
  setSelectedProduct(product);
3518
3617
  if (product.equipmentIds?.length) {
3519
3618
  setActiveResourceId(product.equipmentIds[0]);
@@ -3525,9 +3624,10 @@ function ResiraBookingWidget() {
3525
3624
  persistedPartySize ?? prev.partySize,
3526
3625
  bounds
3527
3626
  );
3528
- const defaultDuration = product.durationPricing?.[0]?.durationMinutes ?? product.durationMinutes ?? prev.duration;
3627
+ const defaultDuration = saved.duration ?? product.durationPricing?.[0]?.durationMinutes ?? product.durationMinutes ?? prev.duration;
3529
3628
  return {
3530
3629
  ...prev,
3630
+ ...saved,
3531
3631
  productId: product.id,
3532
3632
  duration: defaultDuration,
3533
3633
  partySize: nextPartySize
@@ -3549,7 +3649,7 @@ function ResiraBookingWidget() {
3549
3649
  }
3550
3650
  }
3551
3651
  },
3552
- [setActiveResourceId, domainConfig, partySizeByProductId, selection.partySize, step, isServiceBased, STEPS]
3652
+ [setActiveResourceId, domainConfig, partySizeByProductId, selection.partySize, step, isServiceBased, STEPS, selectedProduct?.id]
3553
3653
  );
3554
3654
  const handleResourceSelect = react.useCallback(
3555
3655
  (resourceId) => {
@@ -3870,7 +3970,7 @@ function ResiraBookingWidget() {
3870
3970
  {
3871
3971
  timeSlots: availability?.timeSlots ?? [],
3872
3972
  selectedDate: slotDate,
3873
- onDateChange: setSlotDate,
3973
+ onDateChange: handleSlotDateChange,
3874
3974
  selectedSlot: selection.startTime,
3875
3975
  onSlotSelect: handleSlotSelect,
3876
3976
  partySize: selection.partySize,