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