@ohhwells/bridge 0.1.49 → 0.1.50

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
@@ -116,6 +116,7 @@ var import_react3 = require("react");
116
116
  var import_react2 = require("react");
117
117
  var import_radix_ui = require("radix-ui");
118
118
  var import_jsx_runtime = require("react/jsx-runtime");
119
+ var isValidEmail = (value) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
119
120
  function Spinner() {
120
121
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
121
122
  "svg",
@@ -149,12 +150,17 @@ function EmailCaptureModal({ title, subtitle, onSubmit, onClose }) {
149
150
  const [error, setError] = (0, import_react2.useState)(null);
150
151
  const isBook = title === "Confirm your spot";
151
152
  const handleSubmit = async () => {
152
- if (!email.trim()) return;
153
+ const trimmed = email.trim();
154
+ if (!trimmed) return;
155
+ if (!isValidEmail(trimmed)) {
156
+ setError("Please enter a valid email address.");
157
+ return;
158
+ }
153
159
  setLoading(true);
154
160
  setError(null);
155
161
  try {
156
- await onSubmit(email.trim());
157
- setSubmittedEmail(email.trim());
162
+ await onSubmit(trimmed);
163
+ setSubmittedEmail(trimmed);
158
164
  setSuccess(true);
159
165
  } catch (e) {
160
166
  setError(e instanceof Error ? e.message : "Something went wrong. Please try again.");
@@ -229,7 +235,10 @@ function EmailCaptureModal({ title, subtitle, onSubmit, onClose }) {
229
235
  {
230
236
  type: "email",
231
237
  value: email,
232
- onChange: (e) => setEmail(e.target.value),
238
+ onChange: (e) => {
239
+ setEmail(e.target.value);
240
+ if (error) setError(null);
241
+ },
233
242
  onKeyDown: handleKeyDown,
234
243
  placeholder: "hello@gmail.com",
235
244
  autoFocus: true,
@@ -286,12 +295,20 @@ function formatClassTime(cls) {
286
295
  const em = endTotal % 60;
287
296
  return `${h}:${cls.startTime.minutes}-${eh}:${String(em).padStart(2, "0")}`;
288
297
  }
298
+ function formatBookingLabel(cls) {
299
+ const price = cls.price;
300
+ const isFree = price === null || price === void 0 || `${price}` === "" || `${price}` === "0";
301
+ return isFree ? "Book for free" : `Book for ${cls.currency ?? ""} ${price}`.trim();
302
+ }
289
303
  function getBookingsOnDate(cls, date) {
290
304
  if (!cls.bookings?.length) return 0;
291
- const ds = date.toISOString().split("T")[0];
305
+ const target = new Date(date);
306
+ target.setHours(0, 0, 0, 0);
292
307
  return cls.bookings.filter((b) => {
293
308
  try {
294
- return new Date(b.classDate).toISOString().split("T")[0] === ds;
309
+ const bookingDate = new Date(b.classDate);
310
+ bookingDate.setHours(0, 0, 0, 0);
311
+ return bookingDate.getTime() === target.getTime();
295
312
  } catch {
296
313
  return false;
297
314
  }
@@ -556,7 +573,7 @@ function ScheduleView({ schedule, dates, selectedIdx, onSelectDate, onOpenModal
556
573
  }
557
574
  )
558
575
  ] }),
559
- /* @__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: [
576
+ 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: [
560
577
  /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { className: "font-body text-sm text-(--color-dark,#200C02) opacity-80", children: [
561
578
  available,
562
579
  "/",
@@ -568,9 +585,17 @@ function ScheduleView({ schedule, dates, selectedIdx, onSelectDate, onOpenModal
568
585
  /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "flex flex-1 flex-col gap-2 min-w-0 sm:contents", children: [
569
586
  /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "flex-1 flex flex-col gap-2 min-w-0", children: [
570
587
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "font-body text-base font-bold text-(--color-dark,#200C02) block truncate", children: cls.name }),
571
- 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.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 }),
589
+ cls.description && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
590
+ "span",
591
+ {
592
+ className: "font-body text-base font-normal text-(--color-dark,#200C02) opacity-80 block",
593
+ style: { wordBreak: "break-word" },
594
+ children: cls.description
595
+ }
596
+ )
572
597
  ] }),
573
- /* @__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: [
598
+ 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: [
574
599
  /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { className: "font-body text-sm text-(--color-dark,#200C02) opacity-80", children: [
575
600
  available,
576
601
  "/",
@@ -595,7 +620,7 @@ function ScheduleView({ schedule, dates, selectedIdx, onSelectDate, onOpenModal
595
620
  if (!cls.id) return;
596
621
  onOpenModal?.(isFull ? "waitlist" : "book", cls.id, selectedDate);
597
622
  },
598
- children: isFull ? "Join Waitlist" : "Book Now"
623
+ children: isFull ? "Join Waitlist" : formatBookingLabel(cls)
599
624
  }
600
625
  )
601
626
  ] })
@@ -637,6 +662,17 @@ function SchedulingWidget({ notifyOnConnect = false, initialScheduleId, insertAf
637
662
  const [isHovered, setIsHovered] = (0, import_react3.useState)(false);
638
663
  const [modalState, setModalState] = (0, import_react3.useState)(null);
639
664
  const switchScheduleIdRef = (0, import_react3.useRef)(null);
665
+ const liveScheduleIdRef = (0, import_react3.useRef)(null);
666
+ const refetchLiveSchedule = (0, import_react3.useCallback)(async () => {
667
+ const id = liveScheduleIdRef.current;
668
+ if (!id) return;
669
+ try {
670
+ const res = await fetch(`${API_URL}/api/schedule/id/${id}`);
671
+ const data = await res.json();
672
+ if (data?.id) setSchedule(data);
673
+ } catch {
674
+ }
675
+ }, []);
640
676
  const dates = (0, import_react3.useMemo)(() => {
641
677
  const today = /* @__PURE__ */ new Date();
642
678
  today.setHours(0, 0, 0, 0);
@@ -656,6 +692,18 @@ function SchedulingWidget({ notifyOnConnect = false, initialScheduleId, insertAf
656
692
  }
657
693
  }
658
694
  }, [schedule, dates]);
695
+ (0, import_react3.useEffect)(() => {
696
+ if (typeof window === "undefined") return;
697
+ const onVisible = () => {
698
+ if (!document.hidden) void refetchLiveSchedule();
699
+ };
700
+ window.addEventListener("focus", refetchLiveSchedule);
701
+ document.addEventListener("visibilitychange", onVisible);
702
+ return () => {
703
+ window.removeEventListener("focus", refetchLiveSchedule);
704
+ document.removeEventListener("visibilitychange", onVisible);
705
+ };
706
+ }, [refetchLiveSchedule]);
659
707
  (0, import_react3.useEffect)(() => {
660
708
  if (typeof window === "undefined") return;
661
709
  const isInEditor = window.self !== window.top;
@@ -717,7 +765,7 @@ function SchedulingWidget({ notifyOnConnect = false, initialScheduleId, insertAf
717
765
  setLoading(false);
718
766
  return;
719
767
  }
720
- ;
768
+ liveScheduleIdRef.current = effectiveId;
721
769
  (async () => {
722
770
  try {
723
771
  const res = await fetch(`${API_URL}/api/schedule/id/${effectiveId}`);
@@ -774,18 +822,14 @@ function SchedulingWidget({ notifyOnConnect = false, initialScheduleId, insertAf
774
822
  const data = await res.json().catch(() => ({}));
775
823
  throw new Error(data?.message ?? "Something went wrong. Please try again.");
776
824
  }
825
+ await refetchLiveSchedule();
777
826
  };
778
827
  const handleReplaceSchedule = () => {
779
828
  setIsHovered(false);
780
- if (schedule) {
781
- window.parent.postMessage({
782
- type: "ow:schedule-connected",
783
- schedule: { id: schedule.id, name: schedule.name },
784
- insertAfter
785
- }, "*");
786
- } else {
787
- window.parent.postMessage({ type: "ow:scheduling-not-connected", insertAfter }, "*");
788
- }
829
+ window.parent.postMessage(
830
+ { type: "ow:scheduling-not-connected", insertAfter, currentScheduleId: schedule?.id },
831
+ "*"
832
+ );
789
833
  };
790
834
  if (!inEditor && !loading && !schedule) return null;
791
835
  const sectionId = `scheduling-${insertAfter}`;
@@ -828,7 +872,19 @@ function SchedulingWidget({ notifyOnConnect = false, initialScheduleId, insertAf
828
872
  ),
829
873
  /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "max-w-[1280px] mx-auto flex flex-col items-center gap-16", children: [
830
874
  /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "flex flex-col items-center gap-4", children: [
831
- /* @__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" }),
875
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
876
+ "h2",
877
+ {
878
+ className: "font-display text-center m-0 text-(--color-dark,#200C02)",
879
+ style: {
880
+ fontSize: "var(--fs-section-h2, clamp(40px, 4.4vw, 64px))",
881
+ fontWeight: "var(--font-weight-heading, 400)",
882
+ lineHeight: 1.05,
883
+ letterSpacing: "-0.02em"
884
+ },
885
+ children: schedule?.name ?? "Book an appointment"
886
+ }
887
+ ),
832
888
  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 })
833
889
  ] }),
834
890
  /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "w-full flex flex-col items-end gap-3", children: [