@liiift-studio/sanity-visitor-insights 0.8.2 → 0.9.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.js +186 -45
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +169 -38
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/studio/Figure.tsx +197 -36
- package/src/studio/panels.test.tsx +69 -3
package/dist/index.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
2
3
|
var __defProp = Object.defineProperty;
|
|
3
4
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
5
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
5
7
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
8
|
var __export = (target, all) => {
|
|
7
9
|
for (var name in all)
|
|
@@ -15,6 +17,14 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
15
17
|
}
|
|
16
18
|
return to;
|
|
17
19
|
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
18
28
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
29
|
|
|
20
30
|
// src/index.ts
|
|
@@ -51,7 +61,7 @@ var import_sanity2 = require("sanity");
|
|
|
51
61
|
var import_icons = require("@liiift-studio/sanity-ui-compat/icons");
|
|
52
62
|
|
|
53
63
|
// src/studio/VisitorInsightsTool.tsx
|
|
54
|
-
var
|
|
64
|
+
var import_react3 = require("react");
|
|
55
65
|
var import_sanity_ui_compat3 = require("@liiift-studio/sanity-ui-compat");
|
|
56
66
|
|
|
57
67
|
// src/studio/useReport.ts
|
|
@@ -106,6 +116,7 @@ function useReport({ apiBaseUrl, report, range }) {
|
|
|
106
116
|
}
|
|
107
117
|
|
|
108
118
|
// src/studio/Figure.tsx
|
|
119
|
+
var import_react2 = __toESM(require("react"));
|
|
109
120
|
var import_sanity_ui_compat = require("@liiift-studio/sanity-ui-compat");
|
|
110
121
|
var import_jsx_runtime = require("react/jsx-runtime");
|
|
111
122
|
var REASON_TEXT = {
|
|
@@ -164,6 +175,18 @@ function ComparisonBar({ label, metric, max, tone = "default" }) {
|
|
|
164
175
|
)
|
|
165
176
|
] });
|
|
166
177
|
}
|
|
178
|
+
var chartFrame = { position: "relative", width: "100%" };
|
|
179
|
+
var readout = {
|
|
180
|
+
position: "absolute",
|
|
181
|
+
top: 0,
|
|
182
|
+
right: 0,
|
|
183
|
+
padding: "6px 10px",
|
|
184
|
+
borderRadius: 3,
|
|
185
|
+
background: "var(--card-bg-color, rgba(0,0,0,0.55))",
|
|
186
|
+
border: "1px solid var(--card-border-color, rgba(128,128,128,0.35))",
|
|
187
|
+
pointerEvents: "none",
|
|
188
|
+
maxWidth: "70%"
|
|
189
|
+
};
|
|
167
190
|
var barHeader = {
|
|
168
191
|
display: "flex",
|
|
169
192
|
alignItems: "center",
|
|
@@ -191,17 +214,30 @@ function NoticeList({ notices }) {
|
|
|
191
214
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_sanity_ui_compat.Text, { size: 1, children: notice })
|
|
192
215
|
] }) }) }, notice)) });
|
|
193
216
|
}
|
|
217
|
+
var CHART = { w: 760, h: 240, left: 52, right: 12, top: 16, bottom: 30 };
|
|
218
|
+
function niceCeiling(value) {
|
|
219
|
+
if (value <= 0) return 1;
|
|
220
|
+
const magnitude = 10 ** Math.floor(Math.log10(value));
|
|
221
|
+
const normalised = value / magnitude;
|
|
222
|
+
const step = normalised <= 1 ? 1 : normalised <= 2 ? 2 : normalised <= 5 ? 5 : 10;
|
|
223
|
+
return step * magnitude;
|
|
224
|
+
}
|
|
225
|
+
function shortDate(iso) {
|
|
226
|
+
const parsed = /* @__PURE__ */ new Date(`${iso}T00:00:00Z`);
|
|
227
|
+
if (Number.isNaN(parsed.getTime())) return iso;
|
|
228
|
+
return `${parsed.getUTCDate()} ${parsed.toLocaleString("en-GB", { month: "short", timeZone: "UTC" })}`;
|
|
229
|
+
}
|
|
194
230
|
function TrendChart({ points, label = "Daily pageviews, GA4 against Vercel" }) {
|
|
231
|
+
const [hovered, setHovered] = import_react2.default.useState(null);
|
|
195
232
|
if (points.length < 3) return null;
|
|
196
|
-
const W = 720;
|
|
197
|
-
const H = 132;
|
|
198
|
-
const PAD = 4;
|
|
199
233
|
const values = points.flatMap((p) => [p.ga4, p.vercel]).filter((v) => typeof v === "number");
|
|
200
234
|
if (values.length === 0) return null;
|
|
201
|
-
const max = Math.max(...values, 1);
|
|
202
|
-
const
|
|
203
|
-
const
|
|
204
|
-
const
|
|
235
|
+
const max = niceCeiling(Math.max(...values, 1));
|
|
236
|
+
const plotW = CHART.w - CHART.left - CHART.right;
|
|
237
|
+
const plotH = CHART.h - CHART.top - CHART.bottom;
|
|
238
|
+
const x = (i) => CHART.left + i / Math.max(1, points.length - 1) * plotW;
|
|
239
|
+
const y = (v) => CHART.top + plotH - v / max * plotH;
|
|
240
|
+
const line = (pick) => {
|
|
205
241
|
let d = "";
|
|
206
242
|
let penDown = false;
|
|
207
243
|
points.forEach((p, i) => {
|
|
@@ -215,40 +251,145 @@ function TrendChart({ points, label = "Daily pageviews, GA4 against Vercel" }) {
|
|
|
215
251
|
});
|
|
216
252
|
return d.trim();
|
|
217
253
|
};
|
|
218
|
-
const
|
|
219
|
-
|
|
220
|
-
const
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
"
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
254
|
+
const bands = [];
|
|
255
|
+
let run = [];
|
|
256
|
+
const flushRun = () => {
|
|
257
|
+
if (run.length > 1) {
|
|
258
|
+
const top = run.map((i) => `${x(i).toFixed(1)} ${y(points[i].vercel).toFixed(1)}`);
|
|
259
|
+
const bottom = [...run].reverse().map((i) => `${x(i).toFixed(1)} ${y(points[i].ga4).toFixed(1)}`);
|
|
260
|
+
bands.push(`M${top.join(" L")} L${bottom.join(" L")} Z`);
|
|
261
|
+
}
|
|
262
|
+
run = [];
|
|
263
|
+
};
|
|
264
|
+
points.forEach((p, i) => {
|
|
265
|
+
if (typeof p.ga4 === "number" && typeof p.vercel === "number") run.push(i);
|
|
266
|
+
else flushRun();
|
|
267
|
+
});
|
|
268
|
+
flushRun();
|
|
269
|
+
const ticks = [0, 0.25, 0.5, 0.75, 1].map((f) => ({ value: max * f, y: y(max * f) }));
|
|
270
|
+
const labelEvery = Math.max(1, Math.ceil(points.length / 6));
|
|
271
|
+
const dateTicks = points.map((p, i) => ({ i, date: p.date })).filter(({ i }) => i % labelEvery === 0 || i === points.length - 1);
|
|
272
|
+
const active = hovered !== null ? points[hovered] : null;
|
|
273
|
+
const activeGap = active && typeof active.ga4 === "number" && typeof active.vercel === "number" && active.vercel > 0 ? (active.vercel - active.ga4) / active.vercel : null;
|
|
274
|
+
const onKeyDown = (event) => {
|
|
275
|
+
const last = points.length - 1;
|
|
276
|
+
const current = hovered ?? 0;
|
|
277
|
+
const move = (next) => {
|
|
278
|
+
event.preventDefault();
|
|
279
|
+
setHovered(Math.max(0, Math.min(last, next)));
|
|
280
|
+
};
|
|
281
|
+
if (event.key === "ArrowRight") return move(hovered === null ? 0 : current + 1);
|
|
282
|
+
if (event.key === "ArrowLeft") return move(hovered === null ? last : current - 1);
|
|
283
|
+
if (event.key === "Home") return move(0);
|
|
284
|
+
if (event.key === "End") return move(last);
|
|
285
|
+
if (event.key === "Escape") {
|
|
286
|
+
setHovered(null);
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
};
|
|
290
|
+
const onMove = (event) => {
|
|
291
|
+
const box = event.currentTarget.getBoundingClientRect();
|
|
292
|
+
if (box.width === 0) return;
|
|
293
|
+
const svgX = (event.clientX - box.left) / box.width * CHART.w;
|
|
294
|
+
const ratio = (svgX - CHART.left) / plotW;
|
|
295
|
+
const index = Math.round(ratio * (points.length - 1));
|
|
296
|
+
setHovered(index >= 0 && index < points.length ? index : null);
|
|
297
|
+
};
|
|
298
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_sanity_ui_compat.Stack, { space: 3, children: [
|
|
299
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: chartFrame, children: [
|
|
300
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
301
|
+
"svg",
|
|
302
|
+
{
|
|
303
|
+
viewBox: `0 0 ${CHART.w} ${CHART.h}`,
|
|
304
|
+
width: "100%",
|
|
305
|
+
style: { display: "block", height: "auto" },
|
|
306
|
+
role: "img",
|
|
307
|
+
"aria-label": `${label}, ${points[0]?.date} to ${points[points.length - 1]?.date}. Peak ${formatCount(max)} pageviews in a day.`,
|
|
308
|
+
onMouseMove: onMove,
|
|
309
|
+
onMouseLeave: () => setHovered(null),
|
|
310
|
+
onKeyDown,
|
|
311
|
+
onBlur: () => setHovered(null),
|
|
312
|
+
tabIndex: 0,
|
|
313
|
+
"aria-describedby": active ? "trend-readout" : void 0,
|
|
314
|
+
children: [
|
|
315
|
+
ticks.map((tick) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("g", { children: [
|
|
316
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
317
|
+
"line",
|
|
318
|
+
{
|
|
319
|
+
x1: CHART.left,
|
|
320
|
+
x2: CHART.w - CHART.right,
|
|
321
|
+
y1: tick.y,
|
|
322
|
+
y2: tick.y,
|
|
323
|
+
stroke: "currentColor",
|
|
324
|
+
strokeOpacity: tick.value === 0 ? 0.35 : 0.12,
|
|
325
|
+
strokeWidth: "1"
|
|
326
|
+
}
|
|
327
|
+
),
|
|
328
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
329
|
+
"text",
|
|
330
|
+
{
|
|
331
|
+
x: CHART.left - 8,
|
|
332
|
+
y: tick.y + 4,
|
|
333
|
+
textAnchor: "end",
|
|
334
|
+
fontSize: "11",
|
|
335
|
+
fill: "currentColor",
|
|
336
|
+
fillOpacity: "0.55",
|
|
337
|
+
children: formatCount(tick.value)
|
|
338
|
+
}
|
|
339
|
+
)
|
|
340
|
+
] }, tick.value)),
|
|
341
|
+
dateTicks.map(({ i, date }) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
342
|
+
"text",
|
|
343
|
+
{
|
|
344
|
+
x: x(i),
|
|
345
|
+
y: CHART.h - 10,
|
|
346
|
+
textAnchor: "middle",
|
|
347
|
+
fontSize: "11",
|
|
348
|
+
fill: "currentColor",
|
|
349
|
+
fillOpacity: "0.55",
|
|
350
|
+
children: shortDate(date)
|
|
351
|
+
},
|
|
352
|
+
date
|
|
353
|
+
)),
|
|
354
|
+
bands.map((d) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d, fill: "currentColor", fillOpacity: "0.09", stroke: "none" }, d.slice(0, 24))),
|
|
355
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: line((p) => p.vercel), fill: "none", stroke: "currentColor", strokeOpacity: "0.9", strokeWidth: "2" }),
|
|
356
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: line((p) => p.ga4), fill: "none", stroke: "currentColor", strokeOpacity: "0.9", strokeWidth: "2", strokeDasharray: "5 3" }),
|
|
357
|
+
hovered !== null && active && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("g", { children: [
|
|
358
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
359
|
+
"line",
|
|
360
|
+
{
|
|
361
|
+
x1: x(hovered),
|
|
362
|
+
x2: x(hovered),
|
|
363
|
+
y1: CHART.top,
|
|
364
|
+
y2: CHART.top + plotH,
|
|
365
|
+
stroke: "currentColor",
|
|
366
|
+
strokeOpacity: "0.4",
|
|
367
|
+
strokeWidth: "1"
|
|
368
|
+
}
|
|
369
|
+
),
|
|
370
|
+
typeof active.vercel === "number" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("circle", { cx: x(hovered), cy: y(active.vercel), r: "4", fill: "currentColor" }),
|
|
371
|
+
typeof active.ga4 === "number" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("circle", { cx: x(hovered), cy: y(active.ga4), r: "4", fill: "currentColor" })
|
|
372
|
+
] })
|
|
373
|
+
]
|
|
374
|
+
}
|
|
375
|
+
),
|
|
376
|
+
active && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: readout, id: "trend-readout", role: "status", "aria-live": "polite", children: [
|
|
377
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_sanity_ui_compat.Text, { size: 1, weight: "medium", children: shortDate(active.date) }),
|
|
378
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_sanity_ui_compat.Text, { size: 1, muted: true, children: [
|
|
379
|
+
"Vercel ",
|
|
380
|
+
typeof active.vercel === "number" ? formatCount(active.vercel) : "\u2014",
|
|
381
|
+
" \xB7 ",
|
|
382
|
+
"GA4 ",
|
|
383
|
+
typeof active.ga4 === "number" ? formatCount(active.ga4) : "\u2014",
|
|
384
|
+
activeGap !== null ? ` \xB7 gap ${formatPercent(activeGap, 0)}` : ""
|
|
385
|
+
] })
|
|
386
|
+
] })
|
|
387
|
+
] }),
|
|
239
388
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: legendRow, children: [
|
|
240
389
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_sanity_ui_compat.Text, { size: 0, muted: true, children: "\u2501\u2501 Vercel" }),
|
|
241
390
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_sanity_ui_compat.Text, { size: 0, muted: true, children: "\u2504\u2504 GA4" }),
|
|
242
|
-
/* @__PURE__ */ (0, import_jsx_runtime.
|
|
243
|
-
|
|
244
|
-
" to ",
|
|
245
|
-
last
|
|
246
|
-
] }),
|
|
247
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_sanity_ui_compat.Text, { size: 0, muted: true, children: [
|
|
248
|
-
"peak ",
|
|
249
|
-
formatCount(max),
|
|
250
|
-
"/day"
|
|
251
|
-
] })
|
|
391
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_sanity_ui_compat.Text, { size: 0, muted: true, children: "shaded band is the gap" }),
|
|
392
|
+
!active && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_sanity_ui_compat.Text, { size: 0, muted: true, children: "hover, or focus and use arrow keys, for a day" })
|
|
252
393
|
] })
|
|
253
394
|
] });
|
|
254
395
|
}
|
|
@@ -437,8 +578,8 @@ var PANELS = [
|
|
|
437
578
|
{ report: "diagnostics", label: "Diagnostics", blurb: "What to fix before trusting the numbers above" }
|
|
438
579
|
];
|
|
439
580
|
function RangeSelector({ value, onChange }) {
|
|
440
|
-
const refs = (0,
|
|
441
|
-
const onKeyDown = (0,
|
|
581
|
+
const refs = (0, import_react3.useRef)([]);
|
|
582
|
+
const onKeyDown = (0, import_react3.useCallback)(
|
|
442
583
|
(event) => {
|
|
443
584
|
const currentIndex = RANGES.findIndex((r) => r.key === value);
|
|
444
585
|
let nextIndex = null;
|
|
@@ -494,8 +635,8 @@ function SourceStatusRow({ sources }) {
|
|
|
494
635
|
] }) });
|
|
495
636
|
}
|
|
496
637
|
function PanelTabs({ value, onChange }) {
|
|
497
|
-
const refs = (0,
|
|
498
|
-
const onKeyDown = (0,
|
|
638
|
+
const refs = (0, import_react3.useRef)([]);
|
|
639
|
+
const onKeyDown = (0, import_react3.useCallback)(
|
|
499
640
|
(event) => {
|
|
500
641
|
const currentIndex = PANELS.findIndex((p) => p.report === value);
|
|
501
642
|
let nextIndex = null;
|
|
@@ -571,8 +712,8 @@ function ReportPanel({ report, apiBaseUrl, range }) {
|
|
|
571
712
|
function VisitorInsightsTool(props) {
|
|
572
713
|
const apiBaseUrl = props.tool?.options?.apiBaseUrl ?? props.apiBaseUrl ?? "";
|
|
573
714
|
const siteLabel = props.tool?.options?.siteLabel ?? props.siteLabel ?? "";
|
|
574
|
-
const [range, setRange] = (0,
|
|
575
|
-
const [activePanel, setActivePanel] = (0,
|
|
715
|
+
const [range, setRange] = (0, import_react3.useState)("week");
|
|
716
|
+
const [activePanel, setActivePanel] = (0, import_react3.useState)("measurement-health");
|
|
576
717
|
const active = PANELS.find((p) => p.report === activePanel) ?? PANELS[0];
|
|
577
718
|
return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Container, { width: 4, padding: 4, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_sanity_ui_compat3.Stack, { space: 5, children: [
|
|
578
719
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_sanity_ui_compat3.Flex, { align: "flex-start", justify: "space-between", gap: 4, wrap: "wrap", children: [
|