@ohhwells/bridge 0.1.35 → 0.1.36-next.45

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
@@ -110,6 +110,7 @@ var import_react3 = require("react");
110
110
  var import_react2 = require("react");
111
111
  var import_radix_ui = require("radix-ui");
112
112
  var import_jsx_runtime = require("react/jsx-runtime");
113
+ var isValidEmail = (value) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
113
114
  function Spinner() {
114
115
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
115
116
  "svg",
@@ -143,12 +144,17 @@ function EmailCaptureModal({ title, subtitle, onSubmit, onClose }) {
143
144
  const [error, setError] = (0, import_react2.useState)(null);
144
145
  const isBook = title === "Confirm your spot";
145
146
  const handleSubmit = async () => {
146
- if (!email.trim()) return;
147
+ const trimmed = email.trim();
148
+ if (!trimmed) return;
149
+ if (!isValidEmail(trimmed)) {
150
+ setError("Please enter a valid email address.");
151
+ return;
152
+ }
147
153
  setLoading(true);
148
154
  setError(null);
149
155
  try {
150
- await onSubmit(email.trim());
151
- setSubmittedEmail(email.trim());
156
+ await onSubmit(trimmed);
157
+ setSubmittedEmail(trimmed);
152
158
  setSuccess(true);
153
159
  } catch (e) {
154
160
  setError(e instanceof Error ? e.message : "Something went wrong. Please try again.");
@@ -223,7 +229,10 @@ function EmailCaptureModal({ title, subtitle, onSubmit, onClose }) {
223
229
  {
224
230
  type: "email",
225
231
  value: email,
226
- onChange: (e) => setEmail(e.target.value),
232
+ onChange: (e) => {
233
+ setEmail(e.target.value);
234
+ if (error) setError(null);
235
+ },
227
236
  onKeyDown: handleKeyDown,
228
237
  placeholder: "hello@gmail.com",
229
238
  autoFocus: true,
@@ -280,12 +289,20 @@ function formatClassTime(cls) {
280
289
  const em = endTotal % 60;
281
290
  return `${h}:${cls.startTime.minutes}-${eh}:${String(em).padStart(2, "0")}`;
282
291
  }
292
+ function formatBookingLabel(cls) {
293
+ const price = cls.price;
294
+ const isFree = price === null || price === void 0 || `${price}` === "" || `${price}` === "0";
295
+ return isFree ? "Book for free" : `Book for ${cls.currency ?? ""} ${price}`.trim();
296
+ }
283
297
  function getBookingsOnDate(cls, date) {
284
298
  if (!cls.bookings?.length) return 0;
285
- const ds = date.toISOString().split("T")[0];
299
+ const target = new Date(date);
300
+ target.setHours(0, 0, 0, 0);
286
301
  return cls.bookings.filter((b) => {
287
302
  try {
288
- return new Date(b.classDate).toISOString().split("T")[0] === ds;
303
+ const bookingDate = new Date(b.classDate);
304
+ bookingDate.setHours(0, 0, 0, 0);
305
+ return bookingDate.getTime() === target.getTime();
289
306
  } catch {
290
307
  return false;
291
308
  }
@@ -589,7 +606,7 @@ function ScheduleView({ schedule, dates, selectedIdx, onSelectDate, onOpenModal
589
606
  if (!cls.id) return;
590
607
  onOpenModal?.(isFull ? "waitlist" : "book", cls.id, selectedDate);
591
608
  },
592
- children: isFull ? "Join Waitlist" : "Book Now"
609
+ children: isFull ? "Join Waitlist" : formatBookingLabel(cls)
593
610
  }
594
611
  )
595
612
  ] })
@@ -631,6 +648,17 @@ function SchedulingWidget({ notifyOnConnect = false, initialScheduleId, insertAf
631
648
  const [isHovered, setIsHovered] = (0, import_react3.useState)(false);
632
649
  const [modalState, setModalState] = (0, import_react3.useState)(null);
633
650
  const switchScheduleIdRef = (0, import_react3.useRef)(null);
651
+ const liveScheduleIdRef = (0, import_react3.useRef)(null);
652
+ const refetchLiveSchedule = (0, import_react3.useCallback)(async () => {
653
+ const id = liveScheduleIdRef.current;
654
+ if (!id) return;
655
+ try {
656
+ const res = await fetch(`${API_URL}/api/schedule/id/${id}`);
657
+ const data = await res.json();
658
+ if (data?.id) setSchedule(data);
659
+ } catch {
660
+ }
661
+ }, []);
634
662
  const dates = (0, import_react3.useMemo)(() => {
635
663
  const today = /* @__PURE__ */ new Date();
636
664
  today.setHours(0, 0, 0, 0);
@@ -650,6 +678,18 @@ function SchedulingWidget({ notifyOnConnect = false, initialScheduleId, insertAf
650
678
  }
651
679
  }
652
680
  }, [schedule, dates]);
681
+ (0, import_react3.useEffect)(() => {
682
+ if (typeof window === "undefined") return;
683
+ const onVisible = () => {
684
+ if (!document.hidden) void refetchLiveSchedule();
685
+ };
686
+ window.addEventListener("focus", refetchLiveSchedule);
687
+ document.addEventListener("visibilitychange", onVisible);
688
+ return () => {
689
+ window.removeEventListener("focus", refetchLiveSchedule);
690
+ document.removeEventListener("visibilitychange", onVisible);
691
+ };
692
+ }, [refetchLiveSchedule]);
653
693
  (0, import_react3.useEffect)(() => {
654
694
  if (typeof window === "undefined") return;
655
695
  const isInEditor = window.self !== window.top;
@@ -711,7 +751,7 @@ function SchedulingWidget({ notifyOnConnect = false, initialScheduleId, insertAf
711
751
  setLoading(false);
712
752
  return;
713
753
  }
714
- ;
754
+ liveScheduleIdRef.current = effectiveId;
715
755
  (async () => {
716
756
  try {
717
757
  const res = await fetch(`${API_URL}/api/schedule/id/${effectiveId}`);
@@ -768,18 +808,14 @@ function SchedulingWidget({ notifyOnConnect = false, initialScheduleId, insertAf
768
808
  const data = await res.json().catch(() => ({}));
769
809
  throw new Error(data?.message ?? "Something went wrong. Please try again.");
770
810
  }
811
+ await refetchLiveSchedule();
771
812
  };
772
813
  const handleReplaceSchedule = () => {
773
814
  setIsHovered(false);
774
- if (schedule) {
775
- window.parent.postMessage({
776
- type: "ow:schedule-connected",
777
- schedule: { id: schedule.id, name: schedule.name },
778
- insertAfter
779
- }, "*");
780
- } else {
781
- window.parent.postMessage({ type: "ow:scheduling-not-connected", insertAfter }, "*");
782
- }
815
+ window.parent.postMessage(
816
+ { type: "ow:scheduling-not-connected", insertAfter, currentScheduleId: schedule?.id },
817
+ "*"
818
+ );
783
819
  };
784
820
  if (!inEditor && !loading && !schedule) return null;
785
821
  const sectionId = `scheduling-${insertAfter}`;
@@ -822,7 +858,19 @@ function SchedulingWidget({ notifyOnConnect = false, initialScheduleId, insertAf
822
858
  ),
823
859
  /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "max-w-[1280px] mx-auto flex flex-col items-center gap-16", children: [
824
860
  /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "flex flex-col items-center gap-4", children: [
825
- /* @__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" }),
861
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
862
+ "h2",
863
+ {
864
+ className: "font-display text-center m-0 text-(--color-dark,#200C02)",
865
+ style: {
866
+ fontSize: "var(--fs-section-h2, clamp(40px, 4.4vw, 64px))",
867
+ fontWeight: "var(--font-weight-heading, 400)",
868
+ lineHeight: 1.05,
869
+ letterSpacing: "-0.02em"
870
+ },
871
+ children: schedule?.name ?? "Book an appointment"
872
+ }
873
+ ),
826
874
  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 })
827
875
  ] }),
828
876
  /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "w-full flex flex-col items-end gap-3", children: [
@@ -6085,6 +6133,8 @@ var ICONS = {
6085
6133
  insertUnorderedList: '<line x1="8" y1="6" x2="21" y2="6"/><line x1="8" y1="12" x2="21" y2="12"/><line x1="8" y1="18" x2="21" y2="18"/><line x1="3" y1="6" x2="3.01" y2="6"/><line x1="3" y1="12" x2="3.01" y2="12"/><line x1="3" y1="18" x2="3.01" y2="18"/>',
6086
6134
  insertOrderedList: '<line x1="10" y1="6" x2="21" y2="6"/><line x1="10" y1="12" x2="21" y2="12"/><line x1="10" y1="18" x2="21" y2="18"/><path d="M4 6h1v4"/><path d="M4 10h2"/><path d="M6 18H4c0-1 2-2 2-3s-1-1.5-2-1"/>'
6087
6135
  };
6136
+ var TOOLBAR_PILL_PADDING = 2;
6137
+ var TOOLBAR_TOGGLE_GAP = 4;
6088
6138
  var TOOLBAR_GROUPS = [
6089
6139
  [
6090
6140
  { cmd: "bold", title: "Bold" },
@@ -6203,9 +6253,9 @@ function clampToolbarToClip(top, transform, rect, clip, approxH, gap) {
6203
6253
  const pinnedTop = Math.min(maxBottom - approxH, Math.max(minTop, clip.top + gap));
6204
6254
  return { top: pinnedTop + approxH, transform: "translateX(-50%) translateY(-100%)" };
6205
6255
  }
6206
- function calcToolbarPos(rect, parentScroll, approxW = 330) {
6256
+ function calcToolbarPos(rect, parentScroll, approxW = 306) {
6207
6257
  const GAP = 8;
6208
- const APPROX_H = 36;
6258
+ const APPROX_H = 32;
6209
6259
  const APPROX_W = approxW;
6210
6260
  const clip = getIframeVisibleClip(parentScroll);
6211
6261
  const clipTop = clip?.top ?? 0;
@@ -6263,6 +6313,15 @@ function FloatingToolbar({
6263
6313
  left,
6264
6314
  transform,
6265
6315
  zIndex: 2147483647,
6316
+ background: "#fff",
6317
+ border: "1px solid #E7E5E4",
6318
+ borderRadius: 6,
6319
+ boxShadow: "0px 2px 4px -2px rgba(0,0,0,.1),0px 4px 6px -1px rgba(0,0,0,.1)",
6320
+ display: "flex",
6321
+ alignItems: "center",
6322
+ padding: TOOLBAR_PILL_PADDING,
6323
+ gap: TOOLBAR_TOGGLE_GAP,
6324
+ fontFamily: "sans-serif",
6266
6325
  pointerEvents: "auto"
6267
6326
  },
6268
6327
  children: /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(CustomToolbar, { children: [
@@ -8179,7 +8238,32 @@ function OhhwellsBridge() {
8179
8238
  onSubmit: handleLinkPopoverSubmit
8180
8239
  },
8181
8240
  `${linkPopover.key}-${pathname}`
8182
- ) : null
8241
+ ) : null,
8242
+ sectionGap && /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(
8243
+ "div",
8244
+ {
8245
+ "data-ohw-section-insert-line": "",
8246
+ className: "fixed left-0 w-full z-2147483646 flex items-center pointer-events-none",
8247
+ style: { top: sectionGap.y, transform: "translateY(-50%)" },
8248
+ children: [
8249
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("div", { className: "flex-1 bg-primary", style: { height: 3 } }),
8250
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
8251
+ Badge,
8252
+ {
8253
+ className: "px-8 py-1 bg-primary hover:bg-primary text-primary-foreground text-xs font-medium shrink-0 rounded-full cursor-pointer pointer-events-auto",
8254
+ onClick: () => {
8255
+ window.parent.postMessage(
8256
+ { type: "ow:add-section", insertAfter: sectionGap.insertAfter, insertBefore: sectionGap.insertBefore },
8257
+ "*"
8258
+ );
8259
+ },
8260
+ children: "Add Section"
8261
+ }
8262
+ ),
8263
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("div", { className: "flex-1 bg-primary", style: { height: 3 } })
8264
+ ]
8265
+ }
8266
+ )
8183
8267
  ] }),
8184
8268
  bridgeRoot
8185
8269
  ) : null;