@orion-studios/cms 0.5.7 → 0.5.9
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/analytics/react.d.ts +3 -1
- package/dist/analytics/react.js +9 -0
- package/dist/chunk-DPKXKG2S.js +8 -0
- package/dist/{chunk-ULE565KD.js → chunk-Z52R6XR7.js} +227 -72
- package/dist/forms/react.d.ts +12 -2
- package/dist/forms/react.js +6 -3
- package/dist/server/index.d.ts +17 -5
- package/dist/server/index.js +385 -75
- package/dist/studio/index.d.ts +4 -0
- package/dist/studio/index.js +54 -41
- package/package.json +1 -1
- package/sql/bootstrap.sql +43 -0
- package/sql/migrations/20260830143322_cms_analytics_unique_visitors.sql +43 -0
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
declare const ANALYTICS_READY_EVENT = "orion-analytics-ready";
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* The site-side analytics tracker: small, first-party, and zero dependency.
|
|
3
5
|
* Mount <Analytics /> once in the site layout. It captures:
|
|
@@ -50,4 +52,4 @@ declare function AnalyticsNotFound(): null;
|
|
|
50
52
|
/** Programmatic tracking for custom site components. No-op when analytics is off. */
|
|
51
53
|
declare function trackEvent(name: string, meta?: Record<string, unknown>): void;
|
|
52
54
|
|
|
53
|
-
export { ANALYTICS_CONSENT_COOKIE, ANALYTICS_CONSENT_EVENT, ANALYTICS_VISITOR_COOKIE, Analytics, type AnalyticsConsent, AnalyticsNotFound, type AnalyticsProps, EXCLUDE_FLAG, getAnalyticsConsent, setAnalyticsConsent, trackEvent };
|
|
55
|
+
export { ANALYTICS_CONSENT_COOKIE, ANALYTICS_CONSENT_EVENT, ANALYTICS_READY_EVENT, ANALYTICS_VISITOR_COOKIE, Analytics, type AnalyticsConsent, AnalyticsNotFound, type AnalyticsProps, EXCLUDE_FLAG, getAnalyticsConsent, setAnalyticsConsent, trackEvent };
|
package/dist/analytics/react.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
"use client";
|
|
3
|
+
import {
|
|
4
|
+
ANALYTICS_READY_EVENT
|
|
5
|
+
} from "../chunk-DPKXKG2S.js";
|
|
3
6
|
|
|
4
7
|
// src/analytics/react.tsx
|
|
5
8
|
import { useEffect, useRef, useState } from "react";
|
|
@@ -105,6 +108,7 @@ function Analytics({
|
|
|
105
108
|
track({ type, name, path: window.location.pathname, meta });
|
|
106
109
|
if (type === "form" || type === "not_found") flush();
|
|
107
110
|
};
|
|
111
|
+
window.dispatchEvent(new Event(ANALYTICS_READY_EVENT));
|
|
108
112
|
const onClick = (event) => {
|
|
109
113
|
const target = event.target;
|
|
110
114
|
const tagged = target?.closest("[data-analytics]");
|
|
@@ -167,6 +171,10 @@ function Analytics({
|
|
|
167
171
|
const value = params.get(`utm_${key}`);
|
|
168
172
|
if (value) utm[key] = value;
|
|
169
173
|
}
|
|
174
|
+
if (params.has("gclid") && !utm.source) {
|
|
175
|
+
utm.source = "google";
|
|
176
|
+
utm.medium = "cpc";
|
|
177
|
+
}
|
|
170
178
|
if (Object.keys(utm).length > 0) event.utm = utm;
|
|
171
179
|
}
|
|
172
180
|
track(event);
|
|
@@ -185,6 +193,7 @@ function trackEvent(name, meta) {
|
|
|
185
193
|
export {
|
|
186
194
|
ANALYTICS_CONSENT_COOKIE,
|
|
187
195
|
ANALYTICS_CONSENT_EVENT,
|
|
196
|
+
ANALYTICS_READY_EVENT,
|
|
188
197
|
ANALYTICS_VISITOR_COOKIE,
|
|
189
198
|
Analytics,
|
|
190
199
|
AnalyticsNotFound,
|
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
'use client';
|
|
2
|
+
import {
|
|
3
|
+
ANALYTICS_READY_EVENT
|
|
4
|
+
} from "./chunk-DPKXKG2S.js";
|
|
2
5
|
|
|
3
6
|
// src/forms/react.tsx
|
|
4
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
useCallback,
|
|
9
|
+
useEffect,
|
|
10
|
+
useMemo,
|
|
11
|
+
useRef,
|
|
12
|
+
useState
|
|
13
|
+
} from "react";
|
|
5
14
|
|
|
6
15
|
// src/forms/validation.ts
|
|
7
16
|
var MAJOR_EMAIL_PROVIDERS = [
|
|
@@ -248,8 +257,135 @@ function getAutoReplyEmailFields(config) {
|
|
|
248
257
|
return [...fieldTypes.entries()].filter(([, types]) => types.size === 1 && types.has("email")).map(([name]) => name);
|
|
249
258
|
}
|
|
250
259
|
|
|
260
|
+
// src/forms/funnel.ts
|
|
261
|
+
var FORM_VIEW_MIN_VISIBLE_RATIO = 0.75;
|
|
262
|
+
var FORM_VIEW_DWELL_MS = 2e3;
|
|
263
|
+
function createFormFunnelSession(trackStage) {
|
|
264
|
+
const trackedStages = /* @__PURE__ */ new Set();
|
|
265
|
+
const trackOnce = (stage) => {
|
|
266
|
+
if (trackedStages.has(stage) || !trackStage(stage)) return false;
|
|
267
|
+
trackedStages.add(stage);
|
|
268
|
+
return true;
|
|
269
|
+
};
|
|
270
|
+
return {
|
|
271
|
+
hasViewed: () => trackedStages.has("viewed"),
|
|
272
|
+
trackStarted: () => trackOnce("start"),
|
|
273
|
+
trackViewed: () => trackOnce("viewed")
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
function isQualifiedFormVisibility(intersectionRatio, isIntersecting, visibilityState) {
|
|
277
|
+
return isIntersecting && intersectionRatio >= FORM_VIEW_MIN_VISIBLE_RATIO && visibilityState === "visible";
|
|
278
|
+
}
|
|
279
|
+
|
|
251
280
|
// src/forms/react.tsx
|
|
252
281
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
282
|
+
var analyticsTrack = () => window.__orionTrack;
|
|
283
|
+
var isActionableFieldTarget = (target) => {
|
|
284
|
+
if (!(target instanceof Element)) return false;
|
|
285
|
+
const directField = target.closest("input, select, textarea");
|
|
286
|
+
const labelledField = target.closest("label")?.control;
|
|
287
|
+
const field = directField || labelledField;
|
|
288
|
+
if (!(field instanceof HTMLInputElement || field instanceof HTMLSelectElement || field instanceof HTMLTextAreaElement) || field.name === HONEYPOT_FIELD_NAME || field.disabled || "readOnly" in field && field.readOnly) {
|
|
289
|
+
return false;
|
|
290
|
+
}
|
|
291
|
+
return true;
|
|
292
|
+
};
|
|
293
|
+
function useFormFunnel(slug, options = {}) {
|
|
294
|
+
const { preview = false } = options;
|
|
295
|
+
const [attentionTarget, setAttentionTarget] = useState(null);
|
|
296
|
+
const visibleRef = useRef(false);
|
|
297
|
+
const timerRef = useRef(null);
|
|
298
|
+
const clearViewTimer = useCallback(() => {
|
|
299
|
+
if (timerRef.current === null) return;
|
|
300
|
+
window.clearTimeout(timerRef.current);
|
|
301
|
+
timerRef.current = null;
|
|
302
|
+
}, []);
|
|
303
|
+
const trackStage = useCallback(
|
|
304
|
+
(stage) => {
|
|
305
|
+
if (preview || typeof window === "undefined") return false;
|
|
306
|
+
const track = analyticsTrack();
|
|
307
|
+
if (!track) return false;
|
|
308
|
+
track("form", `${slug}:${stage}`);
|
|
309
|
+
return true;
|
|
310
|
+
},
|
|
311
|
+
[preview, slug]
|
|
312
|
+
);
|
|
313
|
+
const session = useMemo(() => createFormFunnelSession(trackStage), [trackStage]);
|
|
314
|
+
const trackViewed = useCallback(() => {
|
|
315
|
+
if (session.trackViewed()) clearViewTimer();
|
|
316
|
+
}, [clearViewTimer, session]);
|
|
317
|
+
const startViewTimer = useCallback(() => {
|
|
318
|
+
if (session.hasViewed() || timerRef.current !== null || !visibleRef.current || document.visibilityState !== "visible" || !analyticsTrack()) {
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
timerRef.current = window.setTimeout(() => {
|
|
322
|
+
timerRef.current = null;
|
|
323
|
+
if (visibleRef.current && document.visibilityState === "visible") trackViewed();
|
|
324
|
+
}, FORM_VIEW_DWELL_MS);
|
|
325
|
+
}, [session, trackViewed]);
|
|
326
|
+
useEffect(() => {
|
|
327
|
+
visibleRef.current = false;
|
|
328
|
+
clearViewTimer();
|
|
329
|
+
if (preview || !attentionTarget || typeof IntersectionObserver === "undefined") return;
|
|
330
|
+
const observer = new IntersectionObserver(
|
|
331
|
+
([entry]) => {
|
|
332
|
+
visibleRef.current = Boolean(
|
|
333
|
+
entry && entry.isIntersecting && entry.intersectionRatio >= FORM_VIEW_MIN_VISIBLE_RATIO
|
|
334
|
+
);
|
|
335
|
+
if (entry && isQualifiedFormVisibility(
|
|
336
|
+
entry.intersectionRatio,
|
|
337
|
+
entry.isIntersecting,
|
|
338
|
+
document.visibilityState
|
|
339
|
+
)) {
|
|
340
|
+
startViewTimer();
|
|
341
|
+
} else clearViewTimer();
|
|
342
|
+
},
|
|
343
|
+
{ threshold: FORM_VIEW_MIN_VISIBLE_RATIO }
|
|
344
|
+
);
|
|
345
|
+
const onVisibilityChange = () => {
|
|
346
|
+
if (document.visibilityState !== "visible") {
|
|
347
|
+
clearViewTimer();
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
if (visibleRef.current) startViewTimer();
|
|
351
|
+
};
|
|
352
|
+
const onAnalyticsReady = () => {
|
|
353
|
+
if (visibleRef.current) startViewTimer();
|
|
354
|
+
};
|
|
355
|
+
observer.observe(attentionTarget);
|
|
356
|
+
document.addEventListener("visibilitychange", onVisibilityChange);
|
|
357
|
+
window.addEventListener(ANALYTICS_READY_EVENT, onAnalyticsReady);
|
|
358
|
+
return () => {
|
|
359
|
+
observer.disconnect();
|
|
360
|
+
document.removeEventListener("visibilitychange", onVisibilityChange);
|
|
361
|
+
window.removeEventListener(ANALYTICS_READY_EVENT, onAnalyticsReady);
|
|
362
|
+
clearViewTimer();
|
|
363
|
+
};
|
|
364
|
+
}, [attentionTarget, clearViewTimer, preview, slug, startViewTimer]);
|
|
365
|
+
const onFieldPointerDownCapture = useCallback(
|
|
366
|
+
(event) => {
|
|
367
|
+
if (!event.nativeEvent.isTrusted || !isActionableFieldTarget(event.target)) return;
|
|
368
|
+
trackViewed();
|
|
369
|
+
},
|
|
370
|
+
[trackViewed]
|
|
371
|
+
);
|
|
372
|
+
const onFieldKeyDownCapture = useCallback(
|
|
373
|
+
(event) => {
|
|
374
|
+
if (!event.nativeEvent.isTrusted || !isActionableFieldTarget(event.target)) return;
|
|
375
|
+
trackViewed();
|
|
376
|
+
},
|
|
377
|
+
[trackViewed]
|
|
378
|
+
);
|
|
379
|
+
const trackStarted = useCallback(() => {
|
|
380
|
+
session.trackStarted();
|
|
381
|
+
}, [session]);
|
|
382
|
+
return {
|
|
383
|
+
attentionRef: setAttentionTarget,
|
|
384
|
+
onFieldKeyDownCapture,
|
|
385
|
+
onFieldPointerDownCapture,
|
|
386
|
+
trackStarted
|
|
387
|
+
};
|
|
388
|
+
}
|
|
253
389
|
var DEFAULT_CLASSES = {
|
|
254
390
|
form: "ocf-form",
|
|
255
391
|
field: "ocf-field",
|
|
@@ -291,26 +427,17 @@ function FormRenderer({
|
|
|
291
427
|
const [state, setState] = useState("idle");
|
|
292
428
|
const [honeypot, setHoneypot] = useState("");
|
|
293
429
|
const renderedAt = useRef(Date.now());
|
|
294
|
-
const
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
);
|
|
301
|
-
};
|
|
302
|
-
useEffect(() => {
|
|
303
|
-
const timer = setTimeout(() => funnel("view"), 100);
|
|
304
|
-
return () => clearTimeout(timer);
|
|
305
|
-
}, [slug]);
|
|
430
|
+
const {
|
|
431
|
+
attentionRef,
|
|
432
|
+
onFieldKeyDownCapture,
|
|
433
|
+
onFieldPointerDownCapture,
|
|
434
|
+
trackStarted
|
|
435
|
+
} = useFormFunnel(slug, { preview });
|
|
306
436
|
useEffect(() => {
|
|
307
437
|
setStepIndex((current) => Math.min(current, Math.max(steps.length - 1, 0)));
|
|
308
438
|
}, [steps.length]);
|
|
309
439
|
const setValue = (name, next, type) => {
|
|
310
|
-
|
|
311
|
-
startedRef.current = true;
|
|
312
|
-
funnel("start");
|
|
313
|
-
}
|
|
440
|
+
trackStarted();
|
|
314
441
|
setValues((current) => ({
|
|
315
442
|
...current,
|
|
316
443
|
[name]: type === "phone" && typeof next === "string" ? formatPhoneUS(next) : next
|
|
@@ -393,6 +520,7 @@ function FormRenderer({
|
|
|
393
520
|
}
|
|
394
521
|
const isLastStep = stepIndex >= steps.length - 1;
|
|
395
522
|
const visibleSteps = steps.length > 0 ? [steps[stepIndex]] : [];
|
|
523
|
+
const firstActionableFieldKey = visibleSteps.flatMap((step) => step.fields || []).map((field, index) => ({ field, key: fieldKey(field, index) })).find(({ field }) => inferFieldType(field) !== "hidden")?.key;
|
|
396
524
|
const renderField = (field, index) => {
|
|
397
525
|
const name = fieldKey(field, index);
|
|
398
526
|
const type = inferFieldType(field);
|
|
@@ -402,15 +530,23 @@ function FormRenderer({
|
|
|
402
530
|
const id = `ocf-${slug}-${name}`;
|
|
403
531
|
const value = values[name];
|
|
404
532
|
if (type === "hidden") return null;
|
|
405
|
-
const wrap = (control) => /* @__PURE__ */ jsxs(
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
field
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
533
|
+
const wrap = (control) => /* @__PURE__ */ jsxs(
|
|
534
|
+
"div",
|
|
535
|
+
{
|
|
536
|
+
className: `${cls.field}${error ? ` ${cls.fieldError}` : ""}`,
|
|
537
|
+
ref: name === firstActionableFieldKey ? attentionRef : void 0,
|
|
538
|
+
children: [
|
|
539
|
+
type !== "checkbox" || options.length > 0 ? /* @__PURE__ */ jsxs("label", { className: cls.label, htmlFor: id, children: [
|
|
540
|
+
label,
|
|
541
|
+
field.required ? " *" : ""
|
|
542
|
+
] }) : null,
|
|
543
|
+
control,
|
|
544
|
+
field.help ? /* @__PURE__ */ jsx("p", { className: "ocf-help", children: field.help }) : null,
|
|
545
|
+
error ? /* @__PURE__ */ jsx("p", { className: cls.errorText, role: "alert", children: error }) : null
|
|
546
|
+
]
|
|
547
|
+
},
|
|
548
|
+
name
|
|
549
|
+
);
|
|
414
550
|
if (type === "textarea") {
|
|
415
551
|
return wrap(
|
|
416
552
|
/* @__PURE__ */ jsx(
|
|
@@ -480,22 +616,30 @@ function FormRenderer({
|
|
|
480
616
|
);
|
|
481
617
|
}
|
|
482
618
|
if (type === "checkbox") {
|
|
483
|
-
return /* @__PURE__ */ jsxs(
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
619
|
+
return /* @__PURE__ */ jsxs(
|
|
620
|
+
"div",
|
|
621
|
+
{
|
|
622
|
+
className: `${cls.field}${error ? ` ${cls.fieldError}` : ""}`,
|
|
623
|
+
ref: name === firstActionableFieldKey ? attentionRef : void 0,
|
|
624
|
+
children: [
|
|
625
|
+
/* @__PURE__ */ jsxs("label", { className: cls.checkbox, htmlFor: id, children: [
|
|
626
|
+
/* @__PURE__ */ jsx(
|
|
627
|
+
"input",
|
|
628
|
+
{
|
|
629
|
+
checked: value === true,
|
|
630
|
+
id,
|
|
631
|
+
onChange: (event) => setValue(name, event.target.checked, type),
|
|
632
|
+
type: "checkbox"
|
|
633
|
+
}
|
|
634
|
+
),
|
|
635
|
+
label,
|
|
636
|
+
field.required ? " *" : ""
|
|
637
|
+
] }),
|
|
638
|
+
error ? /* @__PURE__ */ jsx("p", { className: cls.errorText, role: "alert", children: error }) : null
|
|
639
|
+
]
|
|
640
|
+
},
|
|
641
|
+
name
|
|
642
|
+
);
|
|
499
643
|
}
|
|
500
644
|
const inputType = type === "email" ? "email" : type === "date" ? "date" : type === "number" ? "text" : "text";
|
|
501
645
|
const inputMode = type === "phone" ? "tel" : type === "email" ? "email" : type === "number" ? "decimal" : void 0;
|
|
@@ -514,40 +658,51 @@ function FormRenderer({
|
|
|
514
658
|
)
|
|
515
659
|
);
|
|
516
660
|
};
|
|
517
|
-
return /* @__PURE__ */ jsxs(
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
661
|
+
return /* @__PURE__ */ jsxs(
|
|
662
|
+
"form",
|
|
663
|
+
{
|
|
664
|
+
className: cls.form,
|
|
665
|
+
noValidate: true,
|
|
666
|
+
onKeyDownCapture: onFieldKeyDownCapture,
|
|
667
|
+
onPointerDownCapture: onFieldPointerDownCapture,
|
|
668
|
+
onSubmit: submit,
|
|
669
|
+
children: [
|
|
670
|
+
title,
|
|
671
|
+
intro,
|
|
672
|
+
/* @__PURE__ */ jsx(
|
|
673
|
+
"input",
|
|
674
|
+
{
|
|
675
|
+
"aria-hidden": "true",
|
|
676
|
+
autoComplete: "off",
|
|
677
|
+
className: "ocf-honeypot",
|
|
678
|
+
name: HONEYPOT_FIELD_NAME,
|
|
679
|
+
onChange: (event) => setHoneypot(event.target.value),
|
|
680
|
+
style: { position: "absolute", left: "-9999px", height: 0, width: 0, opacity: 0 },
|
|
681
|
+
tabIndex: -1,
|
|
682
|
+
value: honeypot
|
|
683
|
+
}
|
|
684
|
+
),
|
|
685
|
+
steps.length > 1 ? /* @__PURE__ */ jsxs("p", { className: cls.stepTitle, children: [
|
|
686
|
+
"Step ",
|
|
687
|
+
stepIndex + 1,
|
|
688
|
+
" of ",
|
|
689
|
+
steps.length,
|
|
690
|
+
steps[stepIndex]?.title ? ` \u2014 ${steps[stepIndex].title}` : ""
|
|
691
|
+
] }) : null,
|
|
692
|
+
visibleSteps.map((step) => (step.fields || []).map(renderField)),
|
|
693
|
+
state === "error" ? /* @__PURE__ */ jsx("p", { className: cls.formError, role: "alert", children: "The request could not be sent. Please try again." }) : null,
|
|
694
|
+
/* @__PURE__ */ jsxs("div", { className: "ocf-actions", children: [
|
|
695
|
+
steps.length > 1 && stepIndex > 0 ? /* @__PURE__ */ jsx("button", { className: cls.button, onClick: () => setStepIndex(stepIndex - 1), type: "button", children: "Back" }) : null,
|
|
696
|
+
!isLastStep ? /* @__PURE__ */ jsx("button", { className: cls.button, onClick: nextStep, type: "button", children: "Next" }) : /* @__PURE__ */ jsx("button", { className: cls.button, disabled: state === "submitting" || preview, type: "submit", children: state === "submitting" ? "Sending\u2026" : submitLabel })
|
|
697
|
+
] })
|
|
698
|
+
]
|
|
699
|
+
}
|
|
700
|
+
);
|
|
547
701
|
}
|
|
548
702
|
|
|
549
703
|
export {
|
|
550
704
|
FORM_FIELD_TYPES,
|
|
551
705
|
getAutoReplyEmailFields,
|
|
706
|
+
useFormFunnel,
|
|
552
707
|
FormRenderer
|
|
553
708
|
};
|
package/dist/forms/react.d.ts
CHANGED
|
@@ -1,7 +1,17 @@
|
|
|
1
1
|
import * as react from 'react';
|
|
2
|
-
import { ReactNode } from 'react';
|
|
2
|
+
import { KeyboardEvent, PointerEvent, ReactNode } from 'react';
|
|
3
3
|
import { F as FormConfig } from '../submission-CKZgx1h7.js';
|
|
4
4
|
|
|
5
|
+
type FormFunnelOptions = {
|
|
6
|
+
preview?: boolean;
|
|
7
|
+
};
|
|
8
|
+
type FormFunnelTracking = {
|
|
9
|
+
attentionRef: (node: HTMLElement | null) => void;
|
|
10
|
+
onFieldKeyDownCapture: (event: KeyboardEvent<HTMLElement>) => void;
|
|
11
|
+
onFieldPointerDownCapture: (event: PointerEvent<HTMLElement>) => void;
|
|
12
|
+
trackStarted: () => void;
|
|
13
|
+
};
|
|
14
|
+
declare function useFormFunnel(slug: string, options?: FormFunnelOptions): FormFunnelTracking;
|
|
5
15
|
/**
|
|
6
16
|
* The shared, config-driven form renderer: the same component renders a form
|
|
7
17
|
* in the Studio's form editor preview and on the public site, from the same
|
|
@@ -42,4 +52,4 @@ type FormRendererProps = {
|
|
|
42
52
|
};
|
|
43
53
|
declare function FormRenderer({ slug, config, successMessage, basePath, classNames, title, intro, submitLabel, preview, onSuccess, }: FormRendererProps): react.JSX.Element;
|
|
44
54
|
|
|
45
|
-
export { FormRenderer, type FormRendererClassNames, type FormRendererProps };
|
|
55
|
+
export { type FormFunnelOptions, type FormFunnelTracking, FormRenderer, type FormRendererClassNames, type FormRendererProps, useFormFunnel };
|
package/dist/forms/react.js
CHANGED
package/dist/server/index.d.ts
CHANGED
|
@@ -77,6 +77,8 @@ type CmsRoutesOptions = {
|
|
|
77
77
|
projectRef?: string;
|
|
78
78
|
/** Purpose-specific secret for analytics visitor and session hashing. */
|
|
79
79
|
analyticsSecret?: string;
|
|
80
|
+
/** IANA timezone used for analytics dates and hourly buckets. Defaults to UTC. */
|
|
81
|
+
analyticsTimeZone?: string;
|
|
80
82
|
/** Purpose-specific secret for auto-reply recipient hashing. */
|
|
81
83
|
autoReplyHashSecret?: string;
|
|
82
84
|
/** Cloudflare Turnstile secret — when set, public submits must include a valid `_turnstileToken`. */
|
|
@@ -183,8 +185,8 @@ declare function getPreviewPage(client: SupabaseClient, token: string, keys: Pre
|
|
|
183
185
|
* and memory backends and is trivially testable.
|
|
184
186
|
*
|
|
185
187
|
* Call and email taps are useful interaction counts, but browser events are
|
|
186
|
-
* forgeable.
|
|
187
|
-
*
|
|
188
|
+
* forgeable. Form conversions come from authoritative submission rows. Any
|
|
189
|
+
* other conversion requires a server-verified event.
|
|
188
190
|
*/
|
|
189
191
|
type StoredEvent = {
|
|
190
192
|
id?: number | string;
|
|
@@ -202,6 +204,13 @@ type StoredEvent = {
|
|
|
202
204
|
server_verified?: boolean;
|
|
203
205
|
created_at: string;
|
|
204
206
|
};
|
|
207
|
+
/** Minimal authoritative form record used by aggregate analytics. */
|
|
208
|
+
type StoredFormSubmission = {
|
|
209
|
+
id?: number | string;
|
|
210
|
+
form: string;
|
|
211
|
+
session_key: string;
|
|
212
|
+
created_at: string;
|
|
213
|
+
};
|
|
205
214
|
type AnalyticsKpis = {
|
|
206
215
|
visitors: number;
|
|
207
216
|
identifiedVisitors: number;
|
|
@@ -215,8 +224,11 @@ type AnalyticsKpis = {
|
|
|
215
224
|
portalClicks: number;
|
|
216
225
|
conversions: number;
|
|
217
226
|
conversionRate: number;
|
|
227
|
+
attributedSubmits: number;
|
|
228
|
+
unattributedSubmits: number;
|
|
218
229
|
};
|
|
219
230
|
type AnalyticsSummary = {
|
|
231
|
+
timeZone: string;
|
|
220
232
|
range: {
|
|
221
233
|
from: string;
|
|
222
234
|
to: string;
|
|
@@ -267,7 +279,7 @@ type AnalyticsSummary = {
|
|
|
267
279
|
declare function aggregateAnalytics(events: StoredEvent[], previousEvents: StoredEvent[], range: {
|
|
268
280
|
from: string;
|
|
269
281
|
to: string;
|
|
270
|
-
}): AnalyticsSummary;
|
|
282
|
+
}, submissions?: StoredFormSubmission[], previousSubmissions?: StoredFormSubmission[], timeZone?: string): AnalyticsSummary;
|
|
271
283
|
|
|
272
284
|
/**
|
|
273
285
|
* Server-side analytics ingest: validation, bot filtering, and the
|
|
@@ -301,7 +313,7 @@ type AnalyticsEventRow = {
|
|
|
301
313
|
* Daily-rotating visitor hash: same visitor+day → same key, next day → a new
|
|
302
314
|
* unrelated key. Orders one visit into a path; can't track anyone over time.
|
|
303
315
|
*/
|
|
304
|
-
declare function sessionKeyFor(ip: string, userAgent: string, secret: string, now?: Date): string;
|
|
316
|
+
declare function sessionKeyFor(ip: string, userAgent: string, secret: string, now?: Date, timeZone?: string): string;
|
|
305
317
|
/** One-way server hash for a consented first-party visitor cookie. */
|
|
306
318
|
declare function visitorKeyFor(visitorId: unknown, secret: string): string;
|
|
307
319
|
/** True when the request looks like an automated client, not a visitor. */
|
|
@@ -489,4 +501,4 @@ declare function createMemoryCms(): MemoryCms;
|
|
|
489
501
|
/** Process-wide singleton for Next.js dev servers (module state survives HMR via globalThis). */
|
|
490
502
|
declare function getMemoryCms(): MemoryCms;
|
|
491
503
|
|
|
492
|
-
export { type AnalyticsEventType, type AnalyticsKpis, type AnalyticsSummary, type CmsAction, type CmsEnv, type CmsRole, type CmsRoutesOptions, type CmsUser, type EmailMessage, type EmailSender, type IncomingEvent, MEMORY_DEV_TOKEN, type MemoryCms, PREVIEW_SESSION_COOKIE, PREVIEW_TOKEN_MAX_USES, PREVIEW_TOKEN_TTL_MS, type PreviewGrantClaims, type PreviewPage, type ResendSenderOptions, type StoredEvent, type SyncFormInput, type SyncGlobalInput, type SyncInput, type SyncMediaInput, type SyncOperation, type SyncOptions, type SyncPageInput, type SyncPlan, type SyncResult, aggregateAnalytics, can, createCmsRoutes, createDurableRateLimitStore, createMemoryCms, createPreviewToken, createResendSender, deviceFrom, formatSubmissionText, geoFrom, getMemoryCms, getPreviewPage, getServiceClient, isBotRequest, isStructuralChange, notifySubmission, parseEventBatch, readCmsEnv, resolveUser, runContentSync, sessionKeyFor, setServiceClientForTesting, validateCmsEnv, verifyPreviewGrantToken, verifyPreviewToken, visitorKeyFor };
|
|
504
|
+
export { type AnalyticsEventType, type AnalyticsKpis, type AnalyticsSummary, type CmsAction, type CmsEnv, type CmsRole, type CmsRoutesOptions, type CmsUser, type EmailMessage, type EmailSender, type IncomingEvent, MEMORY_DEV_TOKEN, type MemoryCms, PREVIEW_SESSION_COOKIE, PREVIEW_TOKEN_MAX_USES, PREVIEW_TOKEN_TTL_MS, type PreviewGrantClaims, type PreviewPage, type ResendSenderOptions, type StoredEvent, type StoredFormSubmission, type SyncFormInput, type SyncGlobalInput, type SyncInput, type SyncMediaInput, type SyncOperation, type SyncOptions, type SyncPageInput, type SyncPlan, type SyncResult, aggregateAnalytics, can, createCmsRoutes, createDurableRateLimitStore, createMemoryCms, createPreviewToken, createResendSender, deviceFrom, formatSubmissionText, geoFrom, getMemoryCms, getPreviewPage, getServiceClient, isBotRequest, isStructuralChange, notifySubmission, parseEventBatch, readCmsEnv, resolveUser, runContentSync, sessionKeyFor, setServiceClientForTesting, validateCmsEnv, verifyPreviewGrantToken, verifyPreviewToken, visitorKeyFor };
|