@ohhwells/bridge 0.1.23 → 0.1.24
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 +58 -0
- package/dist/index.cjs +136 -81
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +137 -82
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -177,6 +177,64 @@ Add `data-ohw-editable` and `data-ohw-key` to any element the studio owner shoul
|
|
|
177
177
|
|
|
178
178
|
---
|
|
179
179
|
|
|
180
|
+
## SchedulingWidget (vibe-coder placement)
|
|
181
|
+
|
|
182
|
+
`SchedulingWidget` lets a template builder embed a scheduling/booking section directly in their template JSX, without going through the "Add Section" flow. The canvas editor treats it exactly like a dynamically-inserted scheduling widget — the studio owner can connect a schedule, switch it, and clear it.
|
|
183
|
+
|
|
184
|
+
### Basic usage
|
|
185
|
+
|
|
186
|
+
```tsx
|
|
187
|
+
import { SchedulingWidget } from '@ohhwells/bridge'
|
|
188
|
+
|
|
189
|
+
export default function ClassesPage() {
|
|
190
|
+
return (
|
|
191
|
+
<>
|
|
192
|
+
<PageHeader ... />
|
|
193
|
+
<ClassLibrary ... />
|
|
194
|
+
<SchedulingWidget />
|
|
195
|
+
<WordmarkBand ... />
|
|
196
|
+
</>
|
|
197
|
+
)
|
|
198
|
+
}
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
No props are required. The widget auto-generates a stable identity via React's `useId()` so the bridge can track which schedule is connected to it across saves.
|
|
202
|
+
|
|
203
|
+
### Props
|
|
204
|
+
|
|
205
|
+
| Prop | Type | Default | Description |
|
|
206
|
+
|---|---|---|---|
|
|
207
|
+
| `insertAfter` | `string` | auto | Stable identifier used as the tracker key. Only set this manually if you need two `SchedulingWidget`s on the same page. |
|
|
208
|
+
| `initialScheduleId` | `string \| null` | `undefined` | Set by the bridge internally after hydration. Do not pass this yourself. |
|
|
209
|
+
| `notifyOnConnect` | `boolean` | `false` | Set by the bridge internally. Do not pass this yourself. |
|
|
210
|
+
|
|
211
|
+
### How it works
|
|
212
|
+
|
|
213
|
+
1. The widget renders immediately with a loading skeleton.
|
|
214
|
+
2. It sends `ow:request-schedule-config` to the bridge (running in the same window).
|
|
215
|
+
3. The bridge looks up the tracker for a saved `scheduleId` for this widget:
|
|
216
|
+
- **Found** → responds with `ow:schedule-config { scheduleId }` → widget loads that schedule.
|
|
217
|
+
- **Not found (first time)** → bridge adopts the widget (adds it to the tracker, notifies the canvas editor), responds with `scheduleId: null` → widget shows empty state with an "Add Schedule" button.
|
|
218
|
+
4. In the canvas editor, the studio owner clicks "Add Schedule" → selects a schedule → the widget updates and the connection is saved.
|
|
219
|
+
5. On the live site, the widget fetches the saved schedule by ID and renders it.
|
|
220
|
+
|
|
221
|
+
### Multiple widgets on one page
|
|
222
|
+
|
|
223
|
+
Each `SchedulingWidget` must have a unique `insertAfter` to be tracked independently:
|
|
224
|
+
|
|
225
|
+
```tsx
|
|
226
|
+
<SchedulingWidget insertAfter="classes-morning" />
|
|
227
|
+
<SchedulingWidget insertAfter="classes-evening" />
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
If you omit `insertAfter` on both, `useId()` auto-generates different stable IDs for each, so they are tracked separately anyway.
|
|
231
|
+
|
|
232
|
+
### Empty state on live site
|
|
233
|
+
|
|
234
|
+
If the studio owner has never connected a schedule to this widget, it renders nothing on the live site (no empty placeholder is shown to visitors).
|
|
235
|
+
|
|
236
|
+
---
|
|
237
|
+
|
|
180
238
|
## Editable states (advanced)
|
|
181
239
|
|
|
182
240
|
For elements with multiple display states (e.g. a contact form with default/success/error views), wrap each state in a `data-ohw-state-view` and mark the container with `data-ohw-editable-state`:
|
package/dist/index.cjs
CHANGED
|
@@ -204,13 +204,6 @@ function EmailCaptureModal({ title, subtitle, onSubmit, onClose }) {
|
|
|
204
204
|
var import_jsx_runtime2 = require("react/jsx-runtime");
|
|
205
205
|
var API_URL = process.env.NEXT_PUBLIC_FLOWOPS_API_URL ?? "https://flowops-backend-staging-2yfdh7pwpq-as.a.run.app";
|
|
206
206
|
var DAY_ABBR = ["SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT"];
|
|
207
|
-
function getApiDomain() {
|
|
208
|
-
const h = window.location.hostname;
|
|
209
|
-
for (const base of ["ohhwells.site", "theflowops-staging.com", "theflowops.com"]) {
|
|
210
|
-
if (h.endsWith(`.${base}`)) return h.slice(0, -(base.length + 1));
|
|
211
|
-
}
|
|
212
|
-
return h;
|
|
213
|
-
}
|
|
214
207
|
function classRunsOnDate(cls, date, type) {
|
|
215
208
|
if (type === "FIXED") {
|
|
216
209
|
const d = String(date.getDate()).padStart(2, "0");
|
|
@@ -571,7 +564,9 @@ function CalendarFoldIcon() {
|
|
|
571
564
|
}
|
|
572
565
|
);
|
|
573
566
|
}
|
|
574
|
-
function SchedulingWidget({ notifyOnConnect = false, initialScheduleId, insertAfter }) {
|
|
567
|
+
function SchedulingWidget({ notifyOnConnect = false, initialScheduleId, insertAfter: insertAfterProp }) {
|
|
568
|
+
const autoId = (0, import_react2.useId)();
|
|
569
|
+
const insertAfter = insertAfterProp ?? autoId;
|
|
575
570
|
const [schedule, setSchedule] = (0, import_react2.useState)(null);
|
|
576
571
|
const [loading, setLoading] = (0, import_react2.useState)(true);
|
|
577
572
|
const [inEditor, setInEditor] = (0, import_react2.useState)(false);
|
|
@@ -601,88 +596,99 @@ function SchedulingWidget({ notifyOnConnect = false, initialScheduleId, insertAf
|
|
|
601
596
|
if (typeof window === "undefined") return;
|
|
602
597
|
const isInEditor = window.self !== window.top;
|
|
603
598
|
setInEditor(isInEditor);
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
599
|
+
function loadWithId(effectiveId) {
|
|
600
|
+
if (isInEditor) {
|
|
601
|
+
const startEmpty = effectiveId === null;
|
|
602
|
+
if (startEmpty) setLoading(false);
|
|
603
|
+
let initialized = startEmpty;
|
|
604
|
+
const timer = !startEmpty ? setTimeout(() => {
|
|
605
|
+
if (!initialized) {
|
|
606
|
+
initialized = true;
|
|
607
|
+
setLoading(false);
|
|
608
|
+
}
|
|
609
|
+
}, 5e3) : null;
|
|
610
|
+
const handler = (e) => {
|
|
611
|
+
if (e.data?.type === "ow:clear-scheduling-widget") {
|
|
612
|
+
if (!e.data.insertAfter || e.data.insertAfter !== insertAfter) return;
|
|
613
|
+
setSchedule(null);
|
|
614
|
+
setLoading(false);
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
617
|
+
if (e.data?.type === "ow:switch-schedule") {
|
|
618
|
+
if (!e.data.insertAfter || e.data.insertAfter !== insertAfter) return;
|
|
619
|
+
switchScheduleIdRef.current = e.data.scheduleId ?? null;
|
|
620
|
+
initialized = false;
|
|
621
|
+
setLoading(true);
|
|
622
|
+
window.parent.postMessage({ type: "ow:request-user-schedules", insertAfter }, "*");
|
|
623
|
+
return;
|
|
624
|
+
}
|
|
625
|
+
if (e.data?.type !== "ow:user-schedules-response") return;
|
|
626
|
+
if (e.data.insertAfter && e.data.insertAfter !== insertAfter) return;
|
|
627
|
+
if (initialized && switchScheduleIdRef.current === null) return;
|
|
610
628
|
initialized = true;
|
|
629
|
+
if (timer) clearTimeout(timer);
|
|
630
|
+
const schedules = e.data.schedules ?? [];
|
|
631
|
+
const isSwitching = switchScheduleIdRef.current !== null;
|
|
632
|
+
const target = isSwitching ? schedules.find((s) => s.id === switchScheduleIdRef.current) ?? schedules[0] ?? null : effectiveId ? schedules.find((s) => s.id === effectiveId) ?? schedules[0] ?? null : schedules[0] ?? null;
|
|
633
|
+
switchScheduleIdRef.current = null;
|
|
634
|
+
setSchedule(target);
|
|
611
635
|
setLoading(false);
|
|
636
|
+
if (notifyOnConnect && target && !isSwitching) {
|
|
637
|
+
window.parent.postMessage({
|
|
638
|
+
type: "ow:schedule-connected",
|
|
639
|
+
schedule: { id: target.id, name: target.name },
|
|
640
|
+
insertAfter
|
|
641
|
+
}, "*");
|
|
642
|
+
window.postMessage({ type: "ow:schedule-linked", scheduleId: target.id, insertAfter }, "*");
|
|
643
|
+
}
|
|
644
|
+
};
|
|
645
|
+
window.addEventListener("message", handler);
|
|
646
|
+
if (!startEmpty) window.parent.postMessage({ type: "ow:request-user-schedules", insertAfter }, "*");
|
|
647
|
+
return () => {
|
|
648
|
+
window.removeEventListener("message", handler);
|
|
649
|
+
if (timer) clearTimeout(timer);
|
|
650
|
+
};
|
|
651
|
+
}
|
|
652
|
+
if (!effectiveId) {
|
|
653
|
+
setLoading(false);
|
|
654
|
+
return;
|
|
655
|
+
}
|
|
656
|
+
;
|
|
657
|
+
(async () => {
|
|
658
|
+
try {
|
|
659
|
+
const res = await fetch(`${API_URL}/api/schedule/id/${effectiveId}`);
|
|
660
|
+
const data = await res.json();
|
|
661
|
+
setSchedule(data?.id ? data : null);
|
|
662
|
+
} catch {
|
|
612
663
|
}
|
|
613
|
-
}, 5e3) : null;
|
|
614
|
-
const handler = (e) => {
|
|
615
|
-
if (e.data?.type === "ow:clear-scheduling-widget") {
|
|
616
|
-
if (!e.data.insertAfter || e.data.insertAfter !== insertAfter) return;
|
|
617
|
-
setSchedule(null);
|
|
618
|
-
setLoading(false);
|
|
619
|
-
return;
|
|
620
|
-
}
|
|
621
|
-
if (e.data?.type === "ow:switch-schedule") {
|
|
622
|
-
if (!e.data.insertAfter || e.data.insertAfter !== insertAfter) return;
|
|
623
|
-
switchScheduleIdRef.current = e.data.scheduleId ?? null;
|
|
624
|
-
initialized = false;
|
|
625
|
-
setLoading(true);
|
|
626
|
-
window.parent.postMessage({ type: "ow:request-user-schedules", insertAfter }, "*");
|
|
627
|
-
return;
|
|
628
|
-
}
|
|
629
|
-
if (e.data?.type !== "ow:user-schedules-response") return;
|
|
630
|
-
if (e.data.insertAfter && e.data.insertAfter !== insertAfter) return;
|
|
631
|
-
if (initialized && switchScheduleIdRef.current === null) return;
|
|
632
|
-
initialized = true;
|
|
633
|
-
if (timer) clearTimeout(timer);
|
|
634
|
-
const schedules = e.data.schedules ?? [];
|
|
635
|
-
const isSwitching = switchScheduleIdRef.current !== null;
|
|
636
|
-
const target = isSwitching ? schedules.find((s) => s.id === switchScheduleIdRef.current) ?? schedules[0] ?? null : initialScheduleId ? schedules.find((s) => s.id === initialScheduleId) ?? schedules[0] ?? null : schedules[0] ?? null;
|
|
637
|
-
switchScheduleIdRef.current = null;
|
|
638
|
-
setSchedule(target);
|
|
639
664
|
setLoading(false);
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
665
|
+
})();
|
|
666
|
+
}
|
|
667
|
+
if (initialScheduleId === void 0) {
|
|
668
|
+
let done = false;
|
|
669
|
+
let innerCleanup;
|
|
670
|
+
const timer = setTimeout(() => {
|
|
671
|
+
if (!done) {
|
|
672
|
+
done = true;
|
|
673
|
+
setLoading(false);
|
|
647
674
|
}
|
|
675
|
+
}, 2e3);
|
|
676
|
+
const configHandler = (e) => {
|
|
677
|
+
if (e.data?.type !== "ow:schedule-config" || e.data.insertAfter !== insertAfter || done) return;
|
|
678
|
+
done = true;
|
|
679
|
+
clearTimeout(timer);
|
|
680
|
+
window.removeEventListener("message", configHandler);
|
|
681
|
+
innerCleanup = loadWithId(e.data.scheduleId ?? null);
|
|
648
682
|
};
|
|
649
|
-
window.addEventListener("message",
|
|
650
|
-
|
|
683
|
+
window.addEventListener("message", configHandler);
|
|
684
|
+
window.postMessage({ type: "ow:request-schedule-config", insertAfter }, "*");
|
|
651
685
|
return () => {
|
|
652
|
-
window.removeEventListener("message",
|
|
653
|
-
|
|
686
|
+
window.removeEventListener("message", configHandler);
|
|
687
|
+
clearTimeout(timer);
|
|
688
|
+
innerCleanup?.();
|
|
654
689
|
};
|
|
655
690
|
}
|
|
656
|
-
|
|
657
|
-
setLoading(false);
|
|
658
|
-
return;
|
|
659
|
-
}
|
|
660
|
-
;
|
|
661
|
-
(async () => {
|
|
662
|
-
try {
|
|
663
|
-
if (initialScheduleId) {
|
|
664
|
-
const res = await fetch(`${API_URL}/api/schedule/id/${initialScheduleId}`);
|
|
665
|
-
const data = await res.json();
|
|
666
|
-
setSchedule(data?.id ? data : null);
|
|
667
|
-
} else {
|
|
668
|
-
const domain = getApiDomain();
|
|
669
|
-
const sitemapRes = await fetch(`${API_URL}/api/schedule/sitemap/${domain}`);
|
|
670
|
-
const slugs = await sitemapRes.json();
|
|
671
|
-
if (!Array.isArray(slugs) || !slugs.length) {
|
|
672
|
-
setLoading(false);
|
|
673
|
-
return;
|
|
674
|
-
}
|
|
675
|
-
const { slug } = [...slugs].sort(
|
|
676
|
-
(a, b) => new Date(b.updated_at).getTime() - new Date(a.updated_at).getTime()
|
|
677
|
-
)[0];
|
|
678
|
-
const res = await fetch(`${API_URL}/api/schedule/${domain}/${slug}`);
|
|
679
|
-
const data = await res.json();
|
|
680
|
-
setSchedule(data?.id ? data : null);
|
|
681
|
-
}
|
|
682
|
-
} catch {
|
|
683
|
-
}
|
|
684
|
-
setLoading(false);
|
|
685
|
-
})();
|
|
691
|
+
return loadWithId(initialScheduleId) ?? void 0;
|
|
686
692
|
}, []);
|
|
687
693
|
const timezoneLabel = schedule?.timezone ? (() => {
|
|
688
694
|
try {
|
|
@@ -4382,10 +4388,14 @@ function initSectionsFromContent(content, removeExisting = false) {
|
|
|
4382
4388
|
const inEditor = typeof window !== "undefined" && window.self !== window.top;
|
|
4383
4389
|
if (inEditor) getSectionsTracker().textContent = raw;
|
|
4384
4390
|
const pageEntries = getPageSchedulingEntries(raw);
|
|
4391
|
+
const preExisting = pageEntries.filter((e) => !isSchedulingWidgetMissing(e));
|
|
4385
4392
|
const notifyForEntry = inEditor ? (e) => !e.scheduleId : false;
|
|
4386
4393
|
mountSchedulingEntries(pageEntries, notifyForEntry);
|
|
4387
4394
|
requestAnimationFrame(() => retryMissingSchedulingMounts(pageEntries, notifyForEntry));
|
|
4388
4395
|
setTimeout(() => retryMissingSchedulingMounts(pageEntries, notifyForEntry), 250);
|
|
4396
|
+
for (const entry of preExisting) {
|
|
4397
|
+
window.postMessage({ type: "ow:schedule-config", insertAfter: entry.insertAfter, scheduleId: entry.scheduleId ?? null }, "*");
|
|
4398
|
+
}
|
|
4389
4399
|
} catch {
|
|
4390
4400
|
}
|
|
4391
4401
|
}
|
|
@@ -4839,6 +4849,8 @@ function OhhwellsBridge() {
|
|
|
4839
4849
|
});
|
|
4840
4850
|
const postToParentRef = (0, import_react3.useRef)(postToParent);
|
|
4841
4851
|
postToParentRef.current = postToParent;
|
|
4852
|
+
const sectionsLoadedRef = (0, import_react3.useRef)(false);
|
|
4853
|
+
const pendingScheduleConfigRequests = (0, import_react3.useRef)([]);
|
|
4842
4854
|
const [toolbarRect, setToolbarRect] = (0, import_react3.useState)(null);
|
|
4843
4855
|
const [toggleState, setToggleState] = (0, import_react3.useState)(null);
|
|
4844
4856
|
const [maxBadge, setMaxBadge] = (0, import_react3.useState)(null);
|
|
@@ -4871,6 +4883,29 @@ function OhhwellsBridge() {
|
|
|
4871
4883
|
const refreshStateRules = (0, import_react3.useCallback)(() => {
|
|
4872
4884
|
editStylesRef.current?.forceHover && (editStylesRef.current.forceHover.textContent = collectStateRules());
|
|
4873
4885
|
}, []);
|
|
4886
|
+
const processConfigRequest = (0, import_react3.useCallback)((insertAfterVal) => {
|
|
4887
|
+
const tracker = getSectionsTracker();
|
|
4888
|
+
let entries = [];
|
|
4889
|
+
try {
|
|
4890
|
+
entries = JSON.parse(tracker.textContent || "[]");
|
|
4891
|
+
} catch {
|
|
4892
|
+
}
|
|
4893
|
+
const path = window.location.pathname;
|
|
4894
|
+
const entry = entries.find(
|
|
4895
|
+
(e) => e.type === "scheduling" && e.insertAfter === insertAfterVal && (!e.pagePath || e.pagePath === path)
|
|
4896
|
+
);
|
|
4897
|
+
if (entry) {
|
|
4898
|
+
window.postMessage({ type: "ow:schedule-config", insertAfter: insertAfterVal, scheduleId: entry.scheduleId ?? null }, "*");
|
|
4899
|
+
return;
|
|
4900
|
+
}
|
|
4901
|
+
const newEntry = { type: "scheduling", insertAfter: insertAfterVal, pagePath: path };
|
|
4902
|
+
entries.push(newEntry);
|
|
4903
|
+
tracker.textContent = JSON.stringify(entries);
|
|
4904
|
+
if (isEditMode) {
|
|
4905
|
+
postToParentRef.current({ type: "ow:change", nodes: [{ key: "__ohw_sections", text: tracker.textContent }] });
|
|
4906
|
+
}
|
|
4907
|
+
window.postMessage({ type: "ow:schedule-config", insertAfter: insertAfterVal, scheduleId: null }, "*");
|
|
4908
|
+
}, [isEditMode]);
|
|
4874
4909
|
const deactivate = (0, import_react3.useCallback)(() => {
|
|
4875
4910
|
const el = activeElRef.current;
|
|
4876
4911
|
if (!el) return;
|
|
@@ -4942,6 +4977,8 @@ function OhhwellsBridge() {
|
|
|
4942
4977
|
});
|
|
4943
4978
|
}
|
|
4944
4979
|
initSectionsFromContent(content, true);
|
|
4980
|
+
sectionsLoadedRef.current = true;
|
|
4981
|
+
pendingScheduleConfigRequests.current.splice(0).forEach(processConfigRequest);
|
|
4945
4982
|
return imageLoads.length > 0 ? Promise.all(imageLoads).then(() => {
|
|
4946
4983
|
}) : Promise.resolve();
|
|
4947
4984
|
};
|
|
@@ -5591,6 +5628,8 @@ function OhhwellsBridge() {
|
|
|
5591
5628
|
}
|
|
5592
5629
|
if (sectionsJson) {
|
|
5593
5630
|
initSectionsFromContent({ __ohw_sections: sectionsJson }, true);
|
|
5631
|
+
sectionsLoadedRef.current = true;
|
|
5632
|
+
pendingScheduleConfigRequests.current.splice(0).forEach(processConfigRequest);
|
|
5594
5633
|
}
|
|
5595
5634
|
postToParentRef.current({ type: "ow:hydrate-done" });
|
|
5596
5635
|
};
|
|
@@ -5854,6 +5893,22 @@ function OhhwellsBridge() {
|
|
|
5854
5893
|
if (imageShowTimerRef.current) clearTimeout(imageShowTimerRef.current);
|
|
5855
5894
|
};
|
|
5856
5895
|
}, [isEditMode, refreshStateRules]);
|
|
5896
|
+
(0, import_react3.useEffect)(() => {
|
|
5897
|
+
const handler = (e) => {
|
|
5898
|
+
if (e.data?.type !== "ow:request-schedule-config") return;
|
|
5899
|
+
const insertAfterVal = e.data.insertAfter;
|
|
5900
|
+
if (!insertAfterVal) return;
|
|
5901
|
+
if (!sectionsLoadedRef.current) {
|
|
5902
|
+
if (!pendingScheduleConfigRequests.current.includes(insertAfterVal)) {
|
|
5903
|
+
pendingScheduleConfigRequests.current.push(insertAfterVal);
|
|
5904
|
+
}
|
|
5905
|
+
return;
|
|
5906
|
+
}
|
|
5907
|
+
processConfigRequest(insertAfterVal);
|
|
5908
|
+
};
|
|
5909
|
+
window.addEventListener("message", handler);
|
|
5910
|
+
return () => window.removeEventListener("message", handler);
|
|
5911
|
+
}, [processConfigRequest]);
|
|
5857
5912
|
(0, import_react3.useEffect)(() => {
|
|
5858
5913
|
if (!isEditMode) return;
|
|
5859
5914
|
document.querySelectorAll("[data-ohw-active-state]").forEach((el) => {
|