@liiift-studio/sanity-visitor-insights 0.10.0 → 0.11.0
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/index.d.mts +9 -4
- package/dist/index.d.ts +9 -4
- package/dist/index.js +265 -65
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +271 -71
- package/dist/index.mjs.map +1 -1
- package/dist/{ranges-B1YwjGGt.d.mts → ranges-YGStz1nR.d.mts} +51 -5
- package/dist/{ranges-B1YwjGGt.d.ts → ranges-YGStz1nR.d.ts} +51 -5
- package/dist/server.d.mts +20 -11
- package/dist/server.d.ts +20 -11
- package/dist/server.js +141 -5
- package/dist/server.js.map +1 -1
- package/dist/server.mjs +138 -5
- package/dist/server.mjs.map +1 -1
- package/dist/testing.d.mts +14 -1
- package/dist/testing.d.ts +14 -1
- package/dist/testing.js +12 -0
- package/dist/testing.js.map +1 -1
- package/dist/testing.mjs +12 -0
- package/dist/testing.mjs.map +1 -1
- package/dist/{vercel-BnuGVKWS.d.mts → vercel-C597eCC_.d.mts} +31 -1
- package/dist/{vercel-BnuGVKWS.d.ts → vercel-C597eCC_.d.ts} +31 -1
- package/package.json +3 -3
- package/src/core/core.test.ts +63 -1
- package/src/core/ranges.ts +64 -2
- package/src/reportData.ts +11 -2
- package/src/server/createHandler.ts +22 -3
- package/src/server/ga4.ts +139 -2
- package/src/server/reports/journey.ts +79 -8
- package/src/server/reports/reports.test.ts +212 -0
- package/src/studio/Figure.tsx +162 -1
- package/src/studio/VisitorInsightsTool.tsx +170 -40
- package/src/studio/panels.test.tsx +78 -9
- package/src/studio/panels.tsx +23 -22
- package/src/studio/useReport.ts +26 -4
- package/src/testing/fakes.ts +18 -1
- package/src/types.ts +8 -1
package/dist/index.d.mts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as sanity from 'sanity';
|
|
2
2
|
import React from 'react';
|
|
3
|
-
import { R as ReportEnvelope, a as ReportName, b as RangeKey, M as MetricValue, A as AcquisitionData, D as DiagnosticReport, J as JourneyData, c as MeasurementHealthData, T as TypefaceInterestData } from './ranges-
|
|
4
|
-
export { C as Coverage, d as DateRange, E as EventCutover, P as PREEXISTING, e as REPORT_NAMES, f as ReportError, S as SiteAnalyticsConfig, g as SourceName, h as SourceStatus, U as UnavailableReason, i as coverageForRange, j as isReportName, o as ok, p as partial, k as previousRange, r as resolveRange, u as unavailable, v as validateSiteConfig, l as valueOrNull } from './ranges-
|
|
3
|
+
import { R as ReportEnvelope, a as ReportName, b as RangeKey, M as MetricValue, A as AcquisitionData, D as DiagnosticReport, J as JourneyData, c as MeasurementHealthData, T as TypefaceInterestData } from './ranges-YGStz1nR.mjs';
|
|
4
|
+
export { C as Coverage, d as DateRange, E as EventCutover, P as PREEXISTING, e as REPORT_NAMES, f as ReportError, S as SiteAnalyticsConfig, g as SourceName, h as SourceStatus, U as UnavailableReason, i as coverageForRange, j as isReportName, o as ok, p as partial, k as previousRange, r as resolveRange, u as unavailable, v as validateSiteConfig, l as valueOrNull } from './ranges-YGStz1nR.mjs';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* The Visitor Insights Studio tool.
|
|
@@ -57,6 +57,11 @@ type ReportState<T> = {
|
|
|
57
57
|
};
|
|
58
58
|
/** Options for useReport. */
|
|
59
59
|
interface UseReportOptions {
|
|
60
|
+
/** Explicit dates, required when `range` is 'custom' and ignored otherwise. */
|
|
61
|
+
custom?: {
|
|
62
|
+
start: string;
|
|
63
|
+
end: string;
|
|
64
|
+
};
|
|
60
65
|
/** Base URL of the site serving the reports, e.g. `https://dardenstudio.com`. */
|
|
61
66
|
apiBaseUrl: string;
|
|
62
67
|
report: ReportName;
|
|
@@ -67,7 +72,7 @@ interface UseReportOptions {
|
|
|
67
72
|
*
|
|
68
73
|
* @returns the current state plus a `reload` for manual refresh
|
|
69
74
|
*/
|
|
70
|
-
declare function useReport<T>({ apiBaseUrl, report, range }: UseReportOptions): {
|
|
75
|
+
declare function useReport<T>({ apiBaseUrl, report, range, custom }: UseReportOptions): {
|
|
71
76
|
state: ReportState<T>;
|
|
72
77
|
reload: () => void;
|
|
73
78
|
};
|
|
@@ -165,7 +170,7 @@ declare function MeasurementHealthPanel({ data }: {
|
|
|
165
170
|
declare function AcquisitionPanel({ data }: {
|
|
166
171
|
data: AcquisitionData;
|
|
167
172
|
}): React.ReactElement;
|
|
168
|
-
/** Journey —
|
|
173
|
+
/** Journey — a funnel, drawn as a tracked sequence or as independent totals, whichever the report used. */
|
|
169
174
|
declare function JourneyPanel({ data }: {
|
|
170
175
|
data: JourneyData;
|
|
171
176
|
}): React.ReactElement;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as sanity from 'sanity';
|
|
2
2
|
import React from 'react';
|
|
3
|
-
import { R as ReportEnvelope, a as ReportName, b as RangeKey, M as MetricValue, A as AcquisitionData, D as DiagnosticReport, J as JourneyData, c as MeasurementHealthData, T as TypefaceInterestData } from './ranges-
|
|
4
|
-
export { C as Coverage, d as DateRange, E as EventCutover, P as PREEXISTING, e as REPORT_NAMES, f as ReportError, S as SiteAnalyticsConfig, g as SourceName, h as SourceStatus, U as UnavailableReason, i as coverageForRange, j as isReportName, o as ok, p as partial, k as previousRange, r as resolveRange, u as unavailable, v as validateSiteConfig, l as valueOrNull } from './ranges-
|
|
3
|
+
import { R as ReportEnvelope, a as ReportName, b as RangeKey, M as MetricValue, A as AcquisitionData, D as DiagnosticReport, J as JourneyData, c as MeasurementHealthData, T as TypefaceInterestData } from './ranges-YGStz1nR.js';
|
|
4
|
+
export { C as Coverage, d as DateRange, E as EventCutover, P as PREEXISTING, e as REPORT_NAMES, f as ReportError, S as SiteAnalyticsConfig, g as SourceName, h as SourceStatus, U as UnavailableReason, i as coverageForRange, j as isReportName, o as ok, p as partial, k as previousRange, r as resolveRange, u as unavailable, v as validateSiteConfig, l as valueOrNull } from './ranges-YGStz1nR.js';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* The Visitor Insights Studio tool.
|
|
@@ -57,6 +57,11 @@ type ReportState<T> = {
|
|
|
57
57
|
};
|
|
58
58
|
/** Options for useReport. */
|
|
59
59
|
interface UseReportOptions {
|
|
60
|
+
/** Explicit dates, required when `range` is 'custom' and ignored otherwise. */
|
|
61
|
+
custom?: {
|
|
62
|
+
start: string;
|
|
63
|
+
end: string;
|
|
64
|
+
};
|
|
60
65
|
/** Base URL of the site serving the reports, e.g. `https://dardenstudio.com`. */
|
|
61
66
|
apiBaseUrl: string;
|
|
62
67
|
report: ReportName;
|
|
@@ -67,7 +72,7 @@ interface UseReportOptions {
|
|
|
67
72
|
*
|
|
68
73
|
* @returns the current state plus a `reload` for manual refresh
|
|
69
74
|
*/
|
|
70
|
-
declare function useReport<T>({ apiBaseUrl, report, range }: UseReportOptions): {
|
|
75
|
+
declare function useReport<T>({ apiBaseUrl, report, range, custom }: UseReportOptions): {
|
|
71
76
|
state: ReportState<T>;
|
|
72
77
|
reload: () => void;
|
|
73
78
|
};
|
|
@@ -165,7 +170,7 @@ declare function MeasurementHealthPanel({ data }: {
|
|
|
165
170
|
declare function AcquisitionPanel({ data }: {
|
|
166
171
|
data: AcquisitionData;
|
|
167
172
|
}): React.ReactElement;
|
|
168
|
-
/** Journey —
|
|
173
|
+
/** Journey — a funnel, drawn as a tracked sequence or as independent totals, whichever the report used. */
|
|
169
174
|
declare function JourneyPanel({ data }: {
|
|
170
175
|
data: JourneyData;
|
|
171
176
|
}): React.ReactElement;
|
package/dist/index.js
CHANGED
|
@@ -68,7 +68,7 @@ var import_sanity_ui_compat3 = require("@liiift-studio/sanity-ui-compat");
|
|
|
68
68
|
var import_react = require("react");
|
|
69
69
|
var import_sanity = require("sanity");
|
|
70
70
|
var API_VERSION = "2024-03-01";
|
|
71
|
-
function useReport({ apiBaseUrl, report, range }) {
|
|
71
|
+
function useReport({ apiBaseUrl, report, range, custom }) {
|
|
72
72
|
const client = (0, import_sanity.useClient)({ apiVersion: API_VERSION });
|
|
73
73
|
const [state, setState] = (0, import_react.useState)({ status: "idle" });
|
|
74
74
|
const [nonce, setNonce] = (0, import_react.useState)(0);
|
|
@@ -90,14 +90,24 @@ function useReport({ apiBaseUrl, report, range }) {
|
|
|
90
90
|
}
|
|
91
91
|
try {
|
|
92
92
|
const base = typeof apiBaseUrl === "string" ? apiBaseUrl.replace(/\/$/, "") : "";
|
|
93
|
-
const
|
|
93
|
+
const query = new URLSearchParams({ range });
|
|
94
|
+
if (range === "custom" && custom) {
|
|
95
|
+
query.set("start", custom.start);
|
|
96
|
+
query.set("end", custom.end);
|
|
97
|
+
}
|
|
98
|
+
const url = `${base}/api/visitor-insights/${report}?${query.toString()}`;
|
|
94
99
|
const response = await fetch(url, {
|
|
95
100
|
headers: { Authorization: `Bearer ${token}` },
|
|
96
101
|
signal: controller.signal
|
|
97
102
|
});
|
|
98
103
|
if (requestIdRef.current !== requestId) return;
|
|
99
104
|
if (!response.ok) {
|
|
100
|
-
|
|
105
|
+
let detail = response.status === 401 ? "Not authorised \u2014 sign in to the Studio again." : `Request failed (${response.status})`;
|
|
106
|
+
if (response.status === 400) {
|
|
107
|
+
const body = await response.json().catch(() => null);
|
|
108
|
+
if (body?.error) detail = body.error;
|
|
109
|
+
}
|
|
110
|
+
if (requestIdRef.current !== requestId) return;
|
|
101
111
|
setState({ status: "error", message: detail });
|
|
102
112
|
return;
|
|
103
113
|
}
|
|
@@ -111,7 +121,7 @@ function useReport({ apiBaseUrl, report, range }) {
|
|
|
111
121
|
}
|
|
112
122
|
void run();
|
|
113
123
|
return () => controller.abort();
|
|
114
|
-
}, [client, apiBaseUrl, report, range, nonce]);
|
|
124
|
+
}, [client, apiBaseUrl, report, range, custom?.start, custom?.end, nonce]);
|
|
115
125
|
return { state, reload };
|
|
116
126
|
}
|
|
117
127
|
|
|
@@ -175,6 +185,83 @@ function ComparisonBar({ label, metric, max, tone = "default" }) {
|
|
|
175
185
|
)
|
|
176
186
|
] });
|
|
177
187
|
}
|
|
188
|
+
function FunnelChart({ stages, measurement }) {
|
|
189
|
+
const [activeKey, setActiveKey] = (0, import_react2.useState)(null);
|
|
190
|
+
const refs = (0, import_react2.useRef)([]);
|
|
191
|
+
const entry = stages[0]?.value ?? 0;
|
|
192
|
+
if (stages.length === 0) return null;
|
|
193
|
+
const onKeyDown = (event, index) => {
|
|
194
|
+
let next = null;
|
|
195
|
+
if (event.key === "ArrowDown" || event.key === "ArrowRight") next = Math.min(index + 1, stages.length - 1);
|
|
196
|
+
if (event.key === "ArrowUp" || event.key === "ArrowLeft") next = Math.max(index - 1, 0);
|
|
197
|
+
if (event.key === "Home") next = 0;
|
|
198
|
+
if (event.key === "End") next = stages.length - 1;
|
|
199
|
+
if (next === null) return;
|
|
200
|
+
event.preventDefault();
|
|
201
|
+
refs.current[next]?.focus();
|
|
202
|
+
};
|
|
203
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("ol", { style: funnelList, children: stages.map((stage, index) => {
|
|
204
|
+
const previous = index > 0 ? stages[index - 1] : null;
|
|
205
|
+
const share = entry > 0 ? stage.value / entry : 0;
|
|
206
|
+
const width = Math.max(1.5, Math.min(1, share) * 100);
|
|
207
|
+
const active = activeKey === stage.key;
|
|
208
|
+
const delta = previous ? previous.value - stage.value : 0;
|
|
209
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("li", { style: funnelItem, children: [
|
|
210
|
+
previous && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: funnelGap, "aria-hidden": "true", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_sanity_ui_compat.Text, { size: 0, muted: true, children: gapLabel(delta, measurement) }) }),
|
|
211
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
212
|
+
"div",
|
|
213
|
+
{
|
|
214
|
+
ref: (el) => {
|
|
215
|
+
refs.current[index] = el;
|
|
216
|
+
},
|
|
217
|
+
tabIndex: 0,
|
|
218
|
+
role: "listitem",
|
|
219
|
+
style: funnelStage(active),
|
|
220
|
+
onMouseEnter: () => setActiveKey(stage.key),
|
|
221
|
+
onMouseLeave: () => setActiveKey(null),
|
|
222
|
+
onFocus: () => setActiveKey(stage.key),
|
|
223
|
+
onBlur: () => setActiveKey(null),
|
|
224
|
+
onKeyDown: (e) => onKeyDown(e, index),
|
|
225
|
+
children: [
|
|
226
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: barHeader, children: [
|
|
227
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_sanity_ui_compat.Text, { size: 1, weight: "medium", children: stage.label }),
|
|
228
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_sanity_ui_compat.Text, { size: 1, weight: "semibold", children: formatCount(stage.value) })
|
|
229
|
+
] }),
|
|
230
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_sanity_ui_compat.Card, { "aria-hidden": "true", radius: 2, tone: "transparent", border: true, style: funnelTrack, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_sanity_ui_compat.Card, { tone: "primary", radius: 2, style: { width: `${width}%`, height: "100%" } }) }),
|
|
231
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_sanity_ui_compat.Text, { size: 0, muted: true, children: [
|
|
232
|
+
index === 0 ? "entry step" : `${formatPercent(share, 1)} of ${stages[0]?.label.toLowerCase()}`,
|
|
233
|
+
index > 1 && stage.conversionFromPrevious !== null && previous ? ` \xB7 ${formatPercent(stage.conversionFromPrevious, 1)} of ${previous.label.toLowerCase()}` : ""
|
|
234
|
+
] })
|
|
235
|
+
]
|
|
236
|
+
}
|
|
237
|
+
)
|
|
238
|
+
] }, stage.key);
|
|
239
|
+
}) });
|
|
240
|
+
}
|
|
241
|
+
function gapLabel(delta, measurement) {
|
|
242
|
+
if (delta === 0) return measurement === "sequence" ? "no drop-off" : "no difference";
|
|
243
|
+
if (delta > 0) return measurement === "sequence" ? `\u2212${formatCount(delta)} did not continue` : `${formatCount(delta)} fewer`;
|
|
244
|
+
return `${formatCount(-delta)} more \u2014 not a subset of the step above`;
|
|
245
|
+
}
|
|
246
|
+
var funnelList = { listStyle: "none", margin: 0, padding: 0 };
|
|
247
|
+
var funnelItem = { display: "grid", gap: 4 };
|
|
248
|
+
var funnelGap = {
|
|
249
|
+
display: "flex",
|
|
250
|
+
justifyContent: "flex-end",
|
|
251
|
+
padding: "4px 2px"
|
|
252
|
+
};
|
|
253
|
+
function funnelStage(active) {
|
|
254
|
+
return {
|
|
255
|
+
display: "grid",
|
|
256
|
+
gap: 6,
|
|
257
|
+
padding: "8px 10px",
|
|
258
|
+
borderRadius: 3,
|
|
259
|
+
border: `1px solid ${active ? "currentColor" : "transparent"}`,
|
|
260
|
+
background: active ? "var(--card-bg-color, rgba(128,128,128,0.08))" : "transparent",
|
|
261
|
+
cursor: "default"
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
var funnelTrack = { height: 10, overflow: "hidden" };
|
|
178
265
|
var chartFrame = { position: "relative", width: "100%" };
|
|
179
266
|
var readout = {
|
|
180
267
|
position: "absolute",
|
|
@@ -663,17 +750,16 @@ function JourneyPanel({ data }) {
|
|
|
663
750
|
const allSteps = data.steps ?? [];
|
|
664
751
|
const shown = allSteps.filter((step) => step.count.status !== "unavailable");
|
|
665
752
|
const hiddenSteps = allSteps.filter((step) => step.count.status === "unavailable");
|
|
666
|
-
const
|
|
753
|
+
const stages = shown.flatMap(
|
|
754
|
+
(step) => step.count.status === "unavailable" ? [] : [{ key: step.key, label: step.label, value: step.count.value, conversionFromPrevious: step.conversionFromPrevious }]
|
|
755
|
+
);
|
|
756
|
+
const tracked = data.measurement === "sequence";
|
|
667
757
|
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_sanity_ui_compat2.Stack, { space: 4, children: [
|
|
668
|
-
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Card, { padding: 3, radius: 2, tone: "caution", border: true, children: /* @__PURE__ */ (0, import_jsx_runtime2.
|
|
669
|
-
|
|
670
|
-
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
" of ",
|
|
674
|
-
shown[index - 1]?.label.toLowerCase()
|
|
675
|
-
] })
|
|
676
|
-
] }, step.key)) }),
|
|
758
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Card, { padding: 3, radius: 2, tone: tracked ? "transparent" : "caution", border: true, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_sanity_ui_compat2.Stack, { space: 2, children: [
|
|
759
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Text, { size: 1, weight: "medium", children: tracked ? "Tracked funnel" : "Independent per-step totals" }),
|
|
760
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Text, { size: 1, muted: tracked, children: data.approximationNote })
|
|
761
|
+
] }) }),
|
|
762
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(FunnelChart, { stages, measurement: data.measurement ?? "independent-totals" }),
|
|
677
763
|
hiddenSteps.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_sanity_ui_compat2.Text, { size: 1, muted: true, children: [
|
|
678
764
|
"Not shown: ",
|
|
679
765
|
hiddenSteps.map((step) => step.label.toLowerCase()).join(", "),
|
|
@@ -794,6 +880,12 @@ function DiagnosticsPanel({ data }) {
|
|
|
794
880
|
|
|
795
881
|
// src/studio/VisitorInsightsTool.tsx
|
|
796
882
|
var import_jsx_runtime3 = require("react/jsx-runtime");
|
|
883
|
+
var controlRow = {
|
|
884
|
+
display: "flex",
|
|
885
|
+
gap: 6,
|
|
886
|
+
alignItems: "center",
|
|
887
|
+
flexWrap: "wrap"
|
|
888
|
+
};
|
|
797
889
|
var tabStrip = {
|
|
798
890
|
display: "flex",
|
|
799
891
|
gap: 2,
|
|
@@ -817,10 +909,39 @@ function tabStyle(selected) {
|
|
|
817
909
|
whiteSpace: "nowrap"
|
|
818
910
|
};
|
|
819
911
|
}
|
|
912
|
+
function rangeButton(selected) {
|
|
913
|
+
return {
|
|
914
|
+
appearance: "none",
|
|
915
|
+
background: "transparent",
|
|
916
|
+
border: `1px solid ${selected ? "currentColor" : "var(--card-border-color, rgba(128,128,128,0.3))"}`,
|
|
917
|
+
borderRadius: 3,
|
|
918
|
+
color: "inherit",
|
|
919
|
+
opacity: selected ? 1 : 0.7,
|
|
920
|
+
font: "inherit",
|
|
921
|
+
fontSize: "0.85em",
|
|
922
|
+
fontWeight: selected ? 600 : 400,
|
|
923
|
+
padding: "5px 10px",
|
|
924
|
+
cursor: "pointer",
|
|
925
|
+
whiteSpace: "nowrap"
|
|
926
|
+
};
|
|
927
|
+
}
|
|
928
|
+
var pickerRow = { display: "flex", gap: 10, alignItems: "flex-end", flexWrap: "wrap" };
|
|
929
|
+
var pickerField = { display: "grid", gap: 2 };
|
|
930
|
+
var dateInput = {
|
|
931
|
+
font: "inherit",
|
|
932
|
+
fontSize: "0.85em",
|
|
933
|
+
padding: "4px 6px",
|
|
934
|
+
borderRadius: 3,
|
|
935
|
+
border: "1px solid var(--card-border-color, rgba(128,128,128,0.3))",
|
|
936
|
+
background: "transparent",
|
|
937
|
+
color: "inherit",
|
|
938
|
+
colorScheme: "light dark"
|
|
939
|
+
};
|
|
820
940
|
var RANGES = [
|
|
821
|
-
{ key: "week", label: "
|
|
822
|
-
{ key: "
|
|
823
|
-
{ key: "
|
|
941
|
+
{ key: "week", label: "Week", span: "the last 7 days" },
|
|
942
|
+
{ key: "month", label: "Month", span: "the last 30 days" },
|
|
943
|
+
{ key: "quarter", label: "Quarter", span: "the last 91 days" },
|
|
944
|
+
{ key: "year", label: "Year", span: "the last 365 days" }
|
|
824
945
|
];
|
|
825
946
|
var PANELS = [
|
|
826
947
|
{ report: "acquisition", label: "Acquisition", blurb: "Where visitors come from" },
|
|
@@ -829,8 +950,14 @@ var PANELS = [
|
|
|
829
950
|
{ report: "measurement-health", label: "Measurement health", blurb: "How much of reality each source actually sees" },
|
|
830
951
|
{ report: "diagnostics", label: "Diagnostics", blurb: "What to fix before trusting the numbers above" }
|
|
831
952
|
];
|
|
832
|
-
function RangeSelector({
|
|
953
|
+
function RangeSelector({
|
|
954
|
+
value,
|
|
955
|
+
custom,
|
|
956
|
+
onChange,
|
|
957
|
+
onCustomChange
|
|
958
|
+
}) {
|
|
833
959
|
const refs = (0, import_react3.useRef)([]);
|
|
960
|
+
const [pickerOpen, setPickerOpen] = (0, import_react3.useState)(value === "custom");
|
|
834
961
|
const onKeyDown = (0, import_react3.useCallback)(
|
|
835
962
|
(event) => {
|
|
836
963
|
const currentIndex = RANGES.findIndex((r) => r.key === value);
|
|
@@ -843,33 +970,92 @@ function RangeSelector({ value, onChange }) {
|
|
|
843
970
|
event.preventDefault();
|
|
844
971
|
const next = RANGES[nextIndex];
|
|
845
972
|
if (!next) return;
|
|
973
|
+
setPickerOpen(false);
|
|
846
974
|
onChange(next.key);
|
|
847
975
|
refs.current[nextIndex]?.focus();
|
|
848
976
|
},
|
|
849
977
|
[value, onChange]
|
|
850
978
|
);
|
|
851
|
-
return /* @__PURE__ */ (0, import_jsx_runtime3.
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
979
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_sanity_ui_compat3.Stack, { space: 2, children: [
|
|
980
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { role: "radiogroup", "aria-label": "Date range", style: controlRow, onKeyDown, children: [
|
|
981
|
+
RANGES.map((range, index) => {
|
|
982
|
+
const selected = range.key === value;
|
|
983
|
+
return (
|
|
984
|
+
// Plain buttons rather than the UI kit's: the compat shim's DOM fallback does not
|
|
985
|
+
// forward `text`, which renders the whole selector as blank boxes on any Studio
|
|
986
|
+
// version where it cannot resolve Button.
|
|
987
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
988
|
+
"button",
|
|
989
|
+
{
|
|
990
|
+
type: "button",
|
|
991
|
+
ref: (el) => {
|
|
992
|
+
refs.current[index] = el;
|
|
993
|
+
},
|
|
994
|
+
role: "radio",
|
|
995
|
+
"aria-checked": selected,
|
|
996
|
+
tabIndex: selected ? 0 : -1,
|
|
997
|
+
title: `Ending today, covering ${range.span}`,
|
|
998
|
+
style: rangeButton(selected),
|
|
999
|
+
onClick: () => {
|
|
1000
|
+
setPickerOpen(false);
|
|
1001
|
+
onChange(range.key);
|
|
1002
|
+
},
|
|
1003
|
+
children: range.label
|
|
1004
|
+
},
|
|
1005
|
+
range.key
|
|
1006
|
+
)
|
|
1007
|
+
);
|
|
1008
|
+
}),
|
|
1009
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
1010
|
+
"button",
|
|
1011
|
+
{
|
|
1012
|
+
type: "button",
|
|
1013
|
+
style: rangeButton(value === "custom"),
|
|
1014
|
+
"aria-expanded": pickerOpen,
|
|
1015
|
+
onClick: () => setPickerOpen((open) => !open),
|
|
1016
|
+
children: value === "custom" ? `${custom.start} to ${custom.end}` : "Custom\u2026"
|
|
1017
|
+
}
|
|
1018
|
+
)
|
|
1019
|
+
] }),
|
|
1020
|
+
pickerOpen && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: pickerRow, children: [
|
|
1021
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("label", { style: pickerField, children: [
|
|
1022
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 0, muted: true, children: "From" }),
|
|
1023
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
1024
|
+
"input",
|
|
1025
|
+
{
|
|
1026
|
+
type: "date",
|
|
1027
|
+
value: custom.start,
|
|
1028
|
+
max: custom.end || void 0,
|
|
1029
|
+
style: dateInput,
|
|
1030
|
+
onChange: (e) => onCustomChange({ ...custom, start: e.currentTarget.value })
|
|
1031
|
+
}
|
|
1032
|
+
)
|
|
1033
|
+
] }),
|
|
1034
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("label", { style: pickerField, children: [
|
|
1035
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 0, muted: true, children: "To" }),
|
|
1036
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
1037
|
+
"input",
|
|
1038
|
+
{
|
|
1039
|
+
type: "date",
|
|
1040
|
+
value: custom.end,
|
|
1041
|
+
min: custom.start || void 0,
|
|
1042
|
+
style: dateInput,
|
|
1043
|
+
onChange: (e) => onCustomChange({ ...custom, end: e.currentTarget.value })
|
|
1044
|
+
}
|
|
1045
|
+
)
|
|
1046
|
+
] }),
|
|
1047
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
1048
|
+
"button",
|
|
1049
|
+
{
|
|
1050
|
+
type: "button",
|
|
1051
|
+
style: rangeButton(true),
|
|
1052
|
+
disabled: !custom.start || !custom.end,
|
|
1053
|
+
onClick: () => onChange("custom"),
|
|
1054
|
+
children: "Apply"
|
|
1055
|
+
}
|
|
1056
|
+
)
|
|
1057
|
+
] })
|
|
1058
|
+
] });
|
|
873
1059
|
}
|
|
874
1060
|
var SOURCE_LABEL = {
|
|
875
1061
|
ga4: "Google Analytics",
|
|
@@ -933,9 +1119,14 @@ function PanelTabs({ value, onChange }) {
|
|
|
933
1119
|
);
|
|
934
1120
|
}) });
|
|
935
1121
|
}
|
|
936
|
-
function ReportPanel({
|
|
937
|
-
|
|
938
|
-
|
|
1122
|
+
function ReportPanel({
|
|
1123
|
+
report,
|
|
1124
|
+
apiBaseUrl,
|
|
1125
|
+
range,
|
|
1126
|
+
custom
|
|
1127
|
+
}) {
|
|
1128
|
+
const { state, reload } = useReport({ apiBaseUrl, report, range, custom });
|
|
1129
|
+
const liveMessage = state.status === "loading" ? "Loading report" : state.status === "ready" ? `${report} updated for ${range === "custom" ? `${custom.start} to ${custom.end}` : `the selected ${range}`}` : state.status === "error" ? `Report failed: ${state.message}` : "";
|
|
939
1130
|
return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_sanity_ui_compat3.Stack, { space: 4, children: [
|
|
940
1131
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Box, { "aria-live": "polite", style: { position: "absolute", width: 1, height: 1, overflow: "hidden", clip: "rect(0 0 0 0)" }, children: liveMessage }),
|
|
941
1132
|
state.status === "loading" && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_sanity_ui_compat3.Flex, { align: "center", gap: 3, padding: 4, children: [
|
|
@@ -965,36 +1156,44 @@ function ReportPanel({ report, apiBaseUrl, range }) {
|
|
|
965
1156
|
] })
|
|
966
1157
|
] });
|
|
967
1158
|
}
|
|
1159
|
+
function isoDaysAgo(days) {
|
|
1160
|
+
const date = /* @__PURE__ */ new Date();
|
|
1161
|
+
date.setDate(date.getDate() - days);
|
|
1162
|
+
return date.toISOString().slice(0, 10);
|
|
1163
|
+
}
|
|
968
1164
|
function VisitorInsightsTool(props) {
|
|
969
1165
|
const apiBaseUrl = props.tool?.options?.apiBaseUrl ?? props.apiBaseUrl ?? "";
|
|
970
1166
|
const siteLabel = props.tool?.options?.siteLabel ?? props.siteLabel ?? "";
|
|
971
1167
|
const [range, setRange] = (0, import_react3.useState)("week");
|
|
1168
|
+
const [custom, setCustom] = (0, import_react3.useState)(() => ({ start: isoDaysAgo(30), end: isoDaysAgo(0) }));
|
|
972
1169
|
const [activePanel, setActivePanel] = (0, import_react3.useState)("acquisition");
|
|
973
1170
|
const active = PANELS.find((p) => p.report === activePanel) ?? PANELS[0];
|
|
974
|
-
return
|
|
975
|
-
|
|
976
|
-
|
|
1171
|
+
return (
|
|
1172
|
+
// Named for assistive technology even though the name is not printed: a Studio user with
|
|
1173
|
+
// several tabs open hears which site's figures these are, without the heading repeating
|
|
1174
|
+
// what the Studio navigation already says.
|
|
1175
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Container, { width: 4, padding: 4, as: "section", "aria-label": siteLabel ? `Visitor insights for ${siteLabel}` : "Visitor insights", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_sanity_ui_compat3.Stack, { space: 5, children: [
|
|
1176
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_sanity_ui_compat3.Flex, { align: "flex-start", justify: "space-between", gap: 4, wrap: "wrap", children: [
|
|
977
1177
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Heading, { size: 2, children: "Visitor insights" }),
|
|
978
|
-
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
1178
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(RangeSelector, { value: range, custom, onChange: setRange, onCustomChange: setCustom })
|
|
979
1179
|
] }),
|
|
980
|
-
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
] }) });
|
|
1180
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(PanelTabs, { value: activePanel, onChange: setActivePanel }),
|
|
1181
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
1182
|
+
import_sanity_ui_compat3.Box,
|
|
1183
|
+
{
|
|
1184
|
+
role: "tabpanel",
|
|
1185
|
+
id: `panel-${activePanel}`,
|
|
1186
|
+
"aria-labelledby": `tab-${activePanel}`,
|
|
1187
|
+
tabIndex: 0,
|
|
1188
|
+
style: { position: "relative" },
|
|
1189
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_sanity_ui_compat3.Stack, { space: 4, children: [
|
|
1190
|
+
active && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 1, muted: true, children: active.blurb }),
|
|
1191
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(ReportPanel, { report: activePanel, apiBaseUrl, range, custom })
|
|
1192
|
+
] })
|
|
1193
|
+
}
|
|
1194
|
+
)
|
|
1195
|
+
] }) })
|
|
1196
|
+
);
|
|
998
1197
|
}
|
|
999
1198
|
|
|
1000
1199
|
// src/types.ts
|
|
@@ -1128,6 +1327,7 @@ function daysBetween(start, end) {
|
|
|
1128
1327
|
}
|
|
1129
1328
|
var RANGE_DAYS = {
|
|
1130
1329
|
week: 7,
|
|
1330
|
+
month: 30,
|
|
1131
1331
|
quarter: 91,
|
|
1132
1332
|
year: 365
|
|
1133
1333
|
};
|