@ohhwells/bridge 0.1.42-next.93 → 0.1.42-next.94

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -318,6 +318,18 @@ npm run build
318
318
  npm publish --access public
319
319
  ```
320
320
 
321
+ ### Staging → rebound-template auto-bump
322
+
323
+ When `staging` publishes successfully, the bridge workflow dispatches a `bridge-published` event to `TheFlowOps-Eng/rebound-template`. That repo’s `bump-bridge.yml` workflow checks out `staging`, runs `npm install @ohhwells/bridge@<version> --save-exact`, and pushes the lockfile bump.
324
+
325
+ **One-time setup** (in `ohhwells-bridge` GitHub repo → Settings → Secrets):
326
+
327
+ | Secret | Value |
328
+ |--------|--------|
329
+ | `REBOUND_TEMPLATE_DISPATCH_TOKEN` | Fine-grained or classic PAT with `repo` access to `rebound-template` (needs permission to trigger `repository_dispatch`) |
330
+
331
+ The published staging version looks like `0.1.31-next.42` and is tagged `next` on npm.
332
+
321
333
  ---
322
334
 
323
335
  ## Link dialog (Canvas Editor)
package/dist/index.cjs CHANGED
@@ -115,6 +115,7 @@ var import_react3 = require("react");
115
115
  var import_react2 = require("react");
116
116
  var import_radix_ui = require("radix-ui");
117
117
  var import_jsx_runtime = require("react/jsx-runtime");
118
+ var isValidEmail = (value) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
118
119
  function Spinner() {
119
120
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
120
121
  "svg",
@@ -148,12 +149,17 @@ function EmailCaptureModal({ title, subtitle, onSubmit, onClose }) {
148
149
  const [error, setError] = (0, import_react2.useState)(null);
149
150
  const isBook = title === "Confirm your spot";
150
151
  const handleSubmit = async () => {
151
- if (!email.trim()) return;
152
+ const trimmed = email.trim();
153
+ if (!trimmed) return;
154
+ if (!isValidEmail(trimmed)) {
155
+ setError("Please enter a valid email address.");
156
+ return;
157
+ }
152
158
  setLoading(true);
153
159
  setError(null);
154
160
  try {
155
- await onSubmit(email.trim());
156
- setSubmittedEmail(email.trim());
161
+ await onSubmit(trimmed);
162
+ setSubmittedEmail(trimmed);
157
163
  setSuccess(true);
158
164
  } catch (e) {
159
165
  setError(e instanceof Error ? e.message : "Something went wrong. Please try again.");
@@ -228,7 +234,10 @@ function EmailCaptureModal({ title, subtitle, onSubmit, onClose }) {
228
234
  {
229
235
  type: "email",
230
236
  value: email,
231
- onChange: (e) => setEmail(e.target.value),
237
+ onChange: (e) => {
238
+ setEmail(e.target.value);
239
+ if (error) setError(null);
240
+ },
232
241
  onKeyDown: handleKeyDown,
233
242
  placeholder: "hello@gmail.com",
234
243
  autoFocus: true,
@@ -285,12 +294,20 @@ function formatClassTime(cls) {
285
294
  const em = endTotal % 60;
286
295
  return `${h}:${cls.startTime.minutes}-${eh}:${String(em).padStart(2, "0")}`;
287
296
  }
297
+ function formatBookingLabel(cls) {
298
+ const price = cls.price;
299
+ const isFree = price === null || price === void 0 || `${price}` === "" || `${price}` === "0";
300
+ return isFree ? "Book for free" : `Book for ${cls.currency ?? ""} ${price}`.trim();
301
+ }
288
302
  function getBookingsOnDate(cls, date) {
289
303
  if (!cls.bookings?.length) return 0;
290
- const ds = date.toISOString().split("T")[0];
304
+ const target = new Date(date);
305
+ target.setHours(0, 0, 0, 0);
291
306
  return cls.bookings.filter((b) => {
292
307
  try {
293
- return new Date(b.classDate).toISOString().split("T")[0] === ds;
308
+ const bookingDate = new Date(b.classDate);
309
+ bookingDate.setHours(0, 0, 0, 0);
310
+ return bookingDate.getTime() === target.getTime();
294
311
  } catch {
295
312
  return false;
296
313
  }
@@ -555,7 +572,7 @@ function ScheduleView({ schedule, dates, selectedIdx, onSelectDate, onOpenModal
555
572
  }
556
573
  )
557
574
  ] }),
558
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "sm:hidden flex flex-col gap-px", children: isFull ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "font-body text-sm text-(--color-dark,#200C02) opacity-80", children: "Full" }) : /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
575
+ cls.showAvailability !== false && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "sm:hidden flex flex-col gap-px", children: isFull ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "font-body text-sm text-(--color-dark,#200C02) opacity-80", children: "Full" }) : /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
559
576
  /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { className: "font-body text-sm text-(--color-dark,#200C02) opacity-80", children: [
560
577
  available,
561
578
  "/",
@@ -567,9 +584,17 @@ function ScheduleView({ schedule, dates, selectedIdx, onSelectDate, onOpenModal
567
584
  /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "flex flex-1 flex-col gap-2 min-w-0 sm:contents", children: [
568
585
  /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "flex-1 flex flex-col gap-2 min-w-0", children: [
569
586
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "font-body text-base font-bold text-(--color-dark,#200C02) block truncate", children: cls.name }),
570
- cls.hostName && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "font-body text-base font-normal text-(--color-dark,#200C02) opacity-80 block truncate", children: cls.hostName })
587
+ cls.hostName && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "font-body text-base font-normal text-(--color-dark,#200C02) opacity-80 block truncate", children: cls.hostName }),
588
+ cls.description && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
589
+ "span",
590
+ {
591
+ className: "font-body text-base font-normal text-(--color-dark,#200C02) opacity-80 block",
592
+ style: { wordBreak: "break-word" },
593
+ children: cls.description
594
+ }
595
+ )
571
596
  ] }),
572
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "hidden sm:flex w-14 shrink-0 flex-col gap-px", children: isFull ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "font-body text-sm text-(--color-dark,#200C02) opacity-80", children: "Full" }) : /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
597
+ cls.showAvailability !== false && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "hidden sm:flex w-14 shrink-0 flex-col gap-px", children: isFull ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "font-body text-sm text-(--color-dark,#200C02) opacity-80", children: "Full" }) : /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
573
598
  /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { className: "font-body text-sm text-(--color-dark,#200C02) opacity-80", children: [
574
599
  available,
575
600
  "/",
@@ -594,7 +619,7 @@ function ScheduleView({ schedule, dates, selectedIdx, onSelectDate, onOpenModal
594
619
  if (!cls.id) return;
595
620
  onOpenModal?.(isFull ? "waitlist" : "book", cls.id, selectedDate);
596
621
  },
597
- children: isFull ? "Join Waitlist" : "Book Now"
622
+ children: isFull ? "Join Waitlist" : formatBookingLabel(cls)
598
623
  }
599
624
  )
600
625
  ] })
@@ -636,6 +661,17 @@ function SchedulingWidget({ notifyOnConnect = false, initialScheduleId, insertAf
636
661
  const [isHovered, setIsHovered] = (0, import_react3.useState)(false);
637
662
  const [modalState, setModalState] = (0, import_react3.useState)(null);
638
663
  const switchScheduleIdRef = (0, import_react3.useRef)(null);
664
+ const liveScheduleIdRef = (0, import_react3.useRef)(null);
665
+ const refetchLiveSchedule = (0, import_react3.useCallback)(async () => {
666
+ const id = liveScheduleIdRef.current;
667
+ if (!id) return;
668
+ try {
669
+ const res = await fetch(`${API_URL}/api/schedule/id/${id}`);
670
+ const data = await res.json();
671
+ if (data?.id) setSchedule(data);
672
+ } catch {
673
+ }
674
+ }, []);
639
675
  const dates = (0, import_react3.useMemo)(() => {
640
676
  const today = /* @__PURE__ */ new Date();
641
677
  today.setHours(0, 0, 0, 0);
@@ -655,6 +691,18 @@ function SchedulingWidget({ notifyOnConnect = false, initialScheduleId, insertAf
655
691
  }
656
692
  }
657
693
  }, [schedule, dates]);
694
+ (0, import_react3.useEffect)(() => {
695
+ if (typeof window === "undefined") return;
696
+ const onVisible = () => {
697
+ if (!document.hidden) void refetchLiveSchedule();
698
+ };
699
+ window.addEventListener("focus", refetchLiveSchedule);
700
+ document.addEventListener("visibilitychange", onVisible);
701
+ return () => {
702
+ window.removeEventListener("focus", refetchLiveSchedule);
703
+ document.removeEventListener("visibilitychange", onVisible);
704
+ };
705
+ }, [refetchLiveSchedule]);
658
706
  (0, import_react3.useEffect)(() => {
659
707
  if (typeof window === "undefined") return;
660
708
  const isInEditor = window.self !== window.top;
@@ -716,7 +764,7 @@ function SchedulingWidget({ notifyOnConnect = false, initialScheduleId, insertAf
716
764
  setLoading(false);
717
765
  return;
718
766
  }
719
- ;
767
+ liveScheduleIdRef.current = effectiveId;
720
768
  (async () => {
721
769
  try {
722
770
  const res = await fetch(`${API_URL}/api/schedule/id/${effectiveId}`);
@@ -773,18 +821,14 @@ function SchedulingWidget({ notifyOnConnect = false, initialScheduleId, insertAf
773
821
  const data = await res.json().catch(() => ({}));
774
822
  throw new Error(data?.message ?? "Something went wrong. Please try again.");
775
823
  }
824
+ await refetchLiveSchedule();
776
825
  };
777
826
  const handleReplaceSchedule = () => {
778
827
  setIsHovered(false);
779
- if (schedule) {
780
- window.parent.postMessage({
781
- type: "ow:schedule-connected",
782
- schedule: { id: schedule.id, name: schedule.name },
783
- insertAfter
784
- }, "*");
785
- } else {
786
- window.parent.postMessage({ type: "ow:scheduling-not-connected", insertAfter }, "*");
787
- }
828
+ window.parent.postMessage(
829
+ { type: "ow:scheduling-not-connected", insertAfter, currentScheduleId: schedule?.id },
830
+ "*"
831
+ );
788
832
  };
789
833
  if (!inEditor && !loading && !schedule) return null;
790
834
  const sectionId = `scheduling-${insertAfter}`;
@@ -827,7 +871,19 @@ function SchedulingWidget({ notifyOnConnect = false, initialScheduleId, insertAf
827
871
  ),
828
872
  /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "max-w-[1280px] mx-auto flex flex-col items-center gap-16", children: [
829
873
  /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "flex flex-col items-center gap-4", children: [
830
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("h2", { className: "font-display text-4xl sm:text-5xl font-bold text-center m-0 text-(--color-dark,#200C02)", children: schedule?.name ?? "Book an appointment" }),
874
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
875
+ "h2",
876
+ {
877
+ className: "font-display text-center m-0 text-(--color-dark,#200C02)",
878
+ style: {
879
+ fontSize: "var(--fs-section-h2, clamp(40px, 4.4vw, 64px))",
880
+ fontWeight: "var(--font-weight-heading, 400)",
881
+ lineHeight: 1.05,
882
+ letterSpacing: "-0.02em"
883
+ },
884
+ children: schedule?.name ?? "Book an appointment"
885
+ }
886
+ ),
831
887
  schedule?.description && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: "font-body text-body text-center m-0 text-(--color-accent,#A89B83)", children: schedule.description })
832
888
  ] }),
833
889
  /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "w-full flex flex-col items-end gap-3", children: [