@coursebuilder/analytics 1.1.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/LICENSE +21 -0
- package/dist/api/index.d.ts +158 -0
- package/dist/api/index.js +317 -0
- package/dist/api/index.js.map +1 -0
- package/dist/catalog.d.ts +14 -0
- package/dist/catalog.js +209 -0
- package/dist/catalog.js.map +1 -0
- package/dist/components/index.d.ts +172 -0
- package/dist/components/index.js +1258 -0
- package/dist/components/index.js.map +1 -0
- package/dist/engine.d.ts +20 -0
- package/dist/engine.js +350 -0
- package/dist/engine.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +353 -0
- package/dist/index.js.map +1 -0
- package/dist/providers/database.d.ts +79 -0
- package/dist/providers/database.js +533 -0
- package/dist/providers/database.js.map +1 -0
- package/dist/providers/derived.d.ts +45 -0
- package/dist/providers/derived.js +32 -0
- package/dist/providers/derived.js.map +1 -0
- package/dist/providers/ga4.d.ts +43 -0
- package/dist/providers/ga4.js +220 -0
- package/dist/providers/ga4.js.map +1 -0
- package/dist/providers/index.d.ts +8 -0
- package/dist/providers/index.js +1239 -0
- package/dist/providers/index.js.map +1 -0
- package/dist/providers/mux.d.ts +103 -0
- package/dist/providers/mux.js +241 -0
- package/dist/providers/mux.js.map +1 -0
- package/dist/providers/survey.d.ts +102 -0
- package/dist/providers/survey.js +233 -0
- package/dist/providers/survey.js.map +1 -0
- package/dist/types.d.ts +303 -0
- package/dist/types.js +1 -0
- package/dist/types.js.map +1 -0
- package/package.json +101 -0
- package/src/api/catalog-handler.ts +321 -0
- package/src/api/index.ts +4 -0
- package/src/api/token-handler.ts +71 -0
- package/src/catalog.ts +223 -0
- package/src/components/country-chart.tsx +114 -0
- package/src/components/index.ts +5 -0
- package/src/components/omnibus-dashboard.tsx +1460 -0
- package/src/components/revenue-chart.tsx +251 -0
- package/src/components/use-chart-colors.ts +75 -0
- package/src/engine.ts +201 -0
- package/src/index.ts +7 -0
- package/src/providers/database.ts +795 -0
- package/src/providers/derived.ts +79 -0
- package/src/providers/ga4.ts +173 -0
- package/src/providers/index.ts +44 -0
- package/src/providers/mux.ts +438 -0
- package/src/providers/survey.ts +487 -0
- package/src/types.ts +333 -0
|
@@ -0,0 +1,1258 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
3
|
+
|
|
4
|
+
// src/components/omnibus-dashboard.tsx
|
|
5
|
+
import React3, { useState as useState2, useTransition } from "react";
|
|
6
|
+
import { CheckIcon, ChevronDownIcon, ClipboardIcon, ClipboardListIcon, ClockIcon, DollarSignIcon, ExternalLinkIcon, FilmIcon, GlobeIcon, LinkIcon, Loader2Icon, MousePointerClickIcon, PlayIcon, ShoppingCartIcon, TrendingUpIcon } from "lucide-react";
|
|
7
|
+
import { parseAsStringLiteral, useQueryState } from "nuqs";
|
|
8
|
+
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@coursebuilder/ui";
|
|
9
|
+
|
|
10
|
+
// src/components/country-chart.tsx
|
|
11
|
+
import React from "react";
|
|
12
|
+
import { Bar, BarChart, CartesianGrid, ResponsiveContainer, Tooltip, XAxis, YAxis } from "recharts";
|
|
13
|
+
|
|
14
|
+
// src/components/use-chart-colors.ts
|
|
15
|
+
import { useEffect, useState } from "react";
|
|
16
|
+
var FALLBACK = {
|
|
17
|
+
primary: "#d4a053",
|
|
18
|
+
primaryMuted: "rgba(212, 160, 83, 0.2)",
|
|
19
|
+
foreground: "#e5e5e5",
|
|
20
|
+
mutedForeground: "#999",
|
|
21
|
+
gridLine: "#333",
|
|
22
|
+
cardBg: "#1a1a1a",
|
|
23
|
+
hoverBg: "#222"
|
|
24
|
+
};
|
|
25
|
+
function getCSSVar(name) {
|
|
26
|
+
return getComputedStyle(document.documentElement).getPropertyValue(name).trim();
|
|
27
|
+
}
|
|
28
|
+
__name(getCSSVar, "getCSSVar");
|
|
29
|
+
function oklchToUsable(raw) {
|
|
30
|
+
if (!raw)
|
|
31
|
+
return "";
|
|
32
|
+
return raw.startsWith("oklch") ? raw : raw;
|
|
33
|
+
}
|
|
34
|
+
__name(oklchToUsable, "oklchToUsable");
|
|
35
|
+
function useChartColors() {
|
|
36
|
+
const [colors, setColors] = useState(FALLBACK);
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
function update() {
|
|
39
|
+
const primary = getCSSVar("--primary");
|
|
40
|
+
const muted = getCSSVar("--muted");
|
|
41
|
+
const mutedFg = getCSSVar("--muted-foreground");
|
|
42
|
+
const fg = getCSSVar("--foreground");
|
|
43
|
+
const border = getCSSVar("--border");
|
|
44
|
+
const card = getCSSVar("--card");
|
|
45
|
+
if (!primary)
|
|
46
|
+
return;
|
|
47
|
+
setColors({
|
|
48
|
+
primary: oklchToUsable(primary),
|
|
49
|
+
primaryMuted: oklchToUsable(primary).replace(")", " / 0.2)"),
|
|
50
|
+
foreground: oklchToUsable(fg),
|
|
51
|
+
mutedForeground: oklchToUsable(mutedFg),
|
|
52
|
+
gridLine: oklchToUsable(border),
|
|
53
|
+
cardBg: oklchToUsable(card),
|
|
54
|
+
hoverBg: oklchToUsable(muted)
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
__name(update, "update");
|
|
58
|
+
update();
|
|
59
|
+
const observer = new MutationObserver(update);
|
|
60
|
+
observer.observe(document.documentElement, {
|
|
61
|
+
attributes: true,
|
|
62
|
+
attributeFilter: [
|
|
63
|
+
"class"
|
|
64
|
+
]
|
|
65
|
+
});
|
|
66
|
+
return () => observer.disconnect();
|
|
67
|
+
}, []);
|
|
68
|
+
return colors;
|
|
69
|
+
}
|
|
70
|
+
__name(useChartColors, "useChartColors");
|
|
71
|
+
|
|
72
|
+
// src/components/country-chart.tsx
|
|
73
|
+
function countryFlag(code) {
|
|
74
|
+
if (!code || code.length !== 2)
|
|
75
|
+
return "\u{1F30D}";
|
|
76
|
+
const offset = 127462;
|
|
77
|
+
const a = code.toUpperCase().charCodeAt(0) - 65 + offset;
|
|
78
|
+
const b = code.toUpperCase().charCodeAt(1) - 65 + offset;
|
|
79
|
+
return String.fromCodePoint(a, b);
|
|
80
|
+
}
|
|
81
|
+
__name(countryFlag, "countryFlag");
|
|
82
|
+
function formatCurrency(value) {
|
|
83
|
+
if (value >= 1e3)
|
|
84
|
+
return `$${(value / 1e3).toFixed(1)}k`;
|
|
85
|
+
return `$${value.toFixed(0)}`;
|
|
86
|
+
}
|
|
87
|
+
__name(formatCurrency, "formatCurrency");
|
|
88
|
+
function CustomTooltip({ active, payload }) {
|
|
89
|
+
if (!active || !payload?.length)
|
|
90
|
+
return null;
|
|
91
|
+
const d = payload[0]?.payload;
|
|
92
|
+
if (!d)
|
|
93
|
+
return null;
|
|
94
|
+
return /* @__PURE__ */ React.createElement("div", {
|
|
95
|
+
className: "border-border/50 bg-card rounded-lg border px-3 py-2 shadow-xl"
|
|
96
|
+
}, /* @__PURE__ */ React.createElement("p", {
|
|
97
|
+
className: "text-foreground text-sm font-medium"
|
|
98
|
+
}, countryFlag(d.country), " ", d.country), /* @__PURE__ */ React.createElement("p", {
|
|
99
|
+
className: "text-foreground text-lg font-bold tabular-nums"
|
|
100
|
+
}, "$", d.revenue.toLocaleString(void 0, {
|
|
101
|
+
minimumFractionDigits: 0,
|
|
102
|
+
maximumFractionDigits: 0
|
|
103
|
+
})), /* @__PURE__ */ React.createElement("p", {
|
|
104
|
+
className: "text-muted-foreground text-xs"
|
|
105
|
+
}, d.count, " purchases"));
|
|
106
|
+
}
|
|
107
|
+
__name(CustomTooltip, "CustomTooltip");
|
|
108
|
+
function CountryChart({ data }) {
|
|
109
|
+
const colors = useChartColors();
|
|
110
|
+
const chartData = data.slice(0, 10).map((d) => ({
|
|
111
|
+
...d,
|
|
112
|
+
label: `${countryFlag(d.country)} ${d.country}`
|
|
113
|
+
}));
|
|
114
|
+
return /* @__PURE__ */ React.createElement("div", {
|
|
115
|
+
className: "h-[280px] w-full"
|
|
116
|
+
}, /* @__PURE__ */ React.createElement(ResponsiveContainer, {
|
|
117
|
+
width: "100%",
|
|
118
|
+
height: "100%"
|
|
119
|
+
}, /* @__PURE__ */ React.createElement(BarChart, {
|
|
120
|
+
data: chartData,
|
|
121
|
+
layout: "vertical",
|
|
122
|
+
margin: {
|
|
123
|
+
top: 4,
|
|
124
|
+
right: 8,
|
|
125
|
+
left: 4,
|
|
126
|
+
bottom: 4
|
|
127
|
+
}
|
|
128
|
+
}, /* @__PURE__ */ React.createElement(CartesianGrid, {
|
|
129
|
+
strokeDasharray: "3 3",
|
|
130
|
+
stroke: colors.gridLine,
|
|
131
|
+
horizontal: false
|
|
132
|
+
}), /* @__PURE__ */ React.createElement(XAxis, {
|
|
133
|
+
type: "number",
|
|
134
|
+
tick: {
|
|
135
|
+
fill: colors.mutedForeground,
|
|
136
|
+
fontSize: 11
|
|
137
|
+
},
|
|
138
|
+
axisLine: {
|
|
139
|
+
stroke: colors.gridLine
|
|
140
|
+
},
|
|
141
|
+
tickLine: false,
|
|
142
|
+
tickFormatter: formatCurrency
|
|
143
|
+
}), /* @__PURE__ */ React.createElement(YAxis, {
|
|
144
|
+
type: "category",
|
|
145
|
+
dataKey: "label",
|
|
146
|
+
tick: {
|
|
147
|
+
fill: colors.mutedForeground,
|
|
148
|
+
fontSize: 12
|
|
149
|
+
},
|
|
150
|
+
axisLine: false,
|
|
151
|
+
tickLine: false,
|
|
152
|
+
width: 60
|
|
153
|
+
}), /* @__PURE__ */ React.createElement(Tooltip, {
|
|
154
|
+
content: /* @__PURE__ */ React.createElement(CustomTooltip, null),
|
|
155
|
+
cursor: {
|
|
156
|
+
fill: colors.hoverBg
|
|
157
|
+
}
|
|
158
|
+
}), /* @__PURE__ */ React.createElement(Bar, {
|
|
159
|
+
dataKey: "revenue",
|
|
160
|
+
radius: [
|
|
161
|
+
0,
|
|
162
|
+
4,
|
|
163
|
+
4,
|
|
164
|
+
0
|
|
165
|
+
],
|
|
166
|
+
maxBarSize: 24,
|
|
167
|
+
fill: "#22c55e",
|
|
168
|
+
fillOpacity: 0.7
|
|
169
|
+
}))));
|
|
170
|
+
}
|
|
171
|
+
__name(CountryChart, "CountryChart");
|
|
172
|
+
|
|
173
|
+
// src/components/revenue-chart.tsx
|
|
174
|
+
import React2, { useMemo } from "react";
|
|
175
|
+
import { Area, AreaChart, CartesianGrid as CartesianGrid2, ResponsiveContainer as ResponsiveContainer2, Tooltip as Tooltip2, XAxis as XAxis2, YAxis as YAxis2 } from "recharts";
|
|
176
|
+
function formatDate(dateStr) {
|
|
177
|
+
const d = /* @__PURE__ */ new Date(dateStr + "T00:00:00");
|
|
178
|
+
return d.toLocaleDateString("en-US", {
|
|
179
|
+
month: "short",
|
|
180
|
+
day: "numeric"
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
__name(formatDate, "formatDate");
|
|
184
|
+
function formatCurrency2(value) {
|
|
185
|
+
if (value >= 1e3)
|
|
186
|
+
return `$${(value / 1e3).toFixed(1)}k`;
|
|
187
|
+
return `$${value.toFixed(0)}`;
|
|
188
|
+
}
|
|
189
|
+
__name(formatCurrency2, "formatCurrency");
|
|
190
|
+
function CustomTooltip2({ active, payload, label }) {
|
|
191
|
+
if (!active || !payload?.length || !label)
|
|
192
|
+
return null;
|
|
193
|
+
const merged = payload[0]?.payload;
|
|
194
|
+
if (!merged)
|
|
195
|
+
return null;
|
|
196
|
+
const current = merged.revenue ?? 0;
|
|
197
|
+
const previous = merged.prevRevenue ?? 0;
|
|
198
|
+
const hasPrev = previous > 0;
|
|
199
|
+
let delta = "";
|
|
200
|
+
let deltaColor = "";
|
|
201
|
+
if (hasPrev && current > 0) {
|
|
202
|
+
const pct = (current - previous) / previous * 100;
|
|
203
|
+
delta = pct >= 0 ? `+${pct.toFixed(0)}%` : `${pct.toFixed(0)}%`;
|
|
204
|
+
deltaColor = pct >= 0 ? "text-emerald-500" : "text-red-400";
|
|
205
|
+
}
|
|
206
|
+
return /* @__PURE__ */ React2.createElement("div", {
|
|
207
|
+
className: "border-border/50 bg-card rounded-lg border px-3 py-2 shadow-xl"
|
|
208
|
+
}, /* @__PURE__ */ React2.createElement("p", {
|
|
209
|
+
className: "text-muted-foreground text-xs"
|
|
210
|
+
}, formatDate(label)), /* @__PURE__ */ React2.createElement("div", {
|
|
211
|
+
className: "mt-1 flex items-baseline gap-2"
|
|
212
|
+
}, /* @__PURE__ */ React2.createElement("p", {
|
|
213
|
+
className: "text-foreground text-lg font-bold tabular-nums"
|
|
214
|
+
}, "$", current.toLocaleString(void 0, {
|
|
215
|
+
minimumFractionDigits: 0,
|
|
216
|
+
maximumFractionDigits: 0
|
|
217
|
+
})), delta && /* @__PURE__ */ React2.createElement("span", {
|
|
218
|
+
className: `text-xs font-semibold ${deltaColor}`
|
|
219
|
+
}, delta)), /* @__PURE__ */ React2.createElement("p", {
|
|
220
|
+
className: "text-muted-foreground text-xs"
|
|
221
|
+
}, merged.count ?? 0, " purchases"), hasPrev && /* @__PURE__ */ React2.createElement("p", {
|
|
222
|
+
className: "text-muted-foreground border-border/30 mt-1 border-t pt-1 text-[11px]"
|
|
223
|
+
}, "prev: $", previous.toLocaleString(void 0, {
|
|
224
|
+
minimumFractionDigits: 0,
|
|
225
|
+
maximumFractionDigits: 0
|
|
226
|
+
}), " ", "\xB7 ", merged.prevCount ?? 0, " purchases"));
|
|
227
|
+
}
|
|
228
|
+
__name(CustomTooltip2, "CustomTooltip");
|
|
229
|
+
function RevenueChart({ data, previousData = [] }) {
|
|
230
|
+
const colors = useChartColors();
|
|
231
|
+
const merged = useMemo(() => {
|
|
232
|
+
if (previousData.length === 0) {
|
|
233
|
+
return data.map((d) => ({
|
|
234
|
+
...d,
|
|
235
|
+
prevRevenue: null,
|
|
236
|
+
prevCount: null
|
|
237
|
+
}));
|
|
238
|
+
}
|
|
239
|
+
return data.map((d, i) => {
|
|
240
|
+
const prev = previousData[i];
|
|
241
|
+
return {
|
|
242
|
+
...d,
|
|
243
|
+
prevRevenue: prev?.revenue ?? null,
|
|
244
|
+
prevCount: prev?.count ?? null
|
|
245
|
+
};
|
|
246
|
+
});
|
|
247
|
+
}, [
|
|
248
|
+
data,
|
|
249
|
+
previousData
|
|
250
|
+
]);
|
|
251
|
+
const maxRevenue = useMemo(() => Math.max(...merged.map((d) => d.revenue), ...merged.map((d) => d.prevRevenue ?? 0), 0), [
|
|
252
|
+
merged
|
|
253
|
+
]);
|
|
254
|
+
const nonZeroVals = useMemo(() => merged.map((d) => d.revenue).filter((v) => v > 0).sort((a, b) => a - b), [
|
|
255
|
+
merged
|
|
256
|
+
]);
|
|
257
|
+
const useLog = useMemo(() => {
|
|
258
|
+
if (nonZeroVals.length < 3)
|
|
259
|
+
return false;
|
|
260
|
+
const median = nonZeroVals[Math.floor(nonZeroVals.length / 2)];
|
|
261
|
+
return maxRevenue > median * 10;
|
|
262
|
+
}, [
|
|
263
|
+
nonZeroVals,
|
|
264
|
+
maxRevenue
|
|
265
|
+
]);
|
|
266
|
+
const logFloor = nonZeroVals.length > 0 ? Math.floor(nonZeroVals[0] * 0.7) : 100;
|
|
267
|
+
const hasPrev = previousData.length > 0;
|
|
268
|
+
return /* @__PURE__ */ React2.createElement("div", {
|
|
269
|
+
className: "h-[280px] w-full"
|
|
270
|
+
}, /* @__PURE__ */ React2.createElement(ResponsiveContainer2, {
|
|
271
|
+
width: "100%",
|
|
272
|
+
height: "100%"
|
|
273
|
+
}, /* @__PURE__ */ React2.createElement(AreaChart, {
|
|
274
|
+
data: merged,
|
|
275
|
+
margin: {
|
|
276
|
+
top: 8,
|
|
277
|
+
right: 8,
|
|
278
|
+
left: -12,
|
|
279
|
+
bottom: 0
|
|
280
|
+
}
|
|
281
|
+
}, /* @__PURE__ */ React2.createElement("defs", null, /* @__PURE__ */ React2.createElement("linearGradient", {
|
|
282
|
+
id: "revenueGradient",
|
|
283
|
+
x1: "0",
|
|
284
|
+
y1: "0",
|
|
285
|
+
x2: "0",
|
|
286
|
+
y2: "1"
|
|
287
|
+
}, /* @__PURE__ */ React2.createElement("stop", {
|
|
288
|
+
offset: "0%",
|
|
289
|
+
stopColor: "#22c55e",
|
|
290
|
+
stopOpacity: 0.3
|
|
291
|
+
}), /* @__PURE__ */ React2.createElement("stop", {
|
|
292
|
+
offset: "100%",
|
|
293
|
+
stopColor: "#22c55e",
|
|
294
|
+
stopOpacity: 0
|
|
295
|
+
}))), /* @__PURE__ */ React2.createElement(CartesianGrid2, {
|
|
296
|
+
strokeDasharray: "3 3",
|
|
297
|
+
stroke: colors.gridLine,
|
|
298
|
+
vertical: false
|
|
299
|
+
}), /* @__PURE__ */ React2.createElement(XAxis2, {
|
|
300
|
+
dataKey: "date",
|
|
301
|
+
tickFormatter: formatDate,
|
|
302
|
+
tick: {
|
|
303
|
+
fill: colors.mutedForeground,
|
|
304
|
+
fontSize: 11
|
|
305
|
+
},
|
|
306
|
+
axisLine: {
|
|
307
|
+
stroke: colors.gridLine
|
|
308
|
+
},
|
|
309
|
+
tickLine: false,
|
|
310
|
+
interval: "preserveStartEnd",
|
|
311
|
+
minTickGap: 40
|
|
312
|
+
}), /* @__PURE__ */ React2.createElement(YAxis2, {
|
|
313
|
+
tick: {
|
|
314
|
+
fill: colors.mutedForeground,
|
|
315
|
+
fontSize: 11
|
|
316
|
+
},
|
|
317
|
+
axisLine: false,
|
|
318
|
+
tickLine: false,
|
|
319
|
+
scale: useLog ? "log" : "auto",
|
|
320
|
+
domain: useLog ? [
|
|
321
|
+
logFloor,
|
|
322
|
+
"auto"
|
|
323
|
+
] : [
|
|
324
|
+
0,
|
|
325
|
+
Math.ceil(maxRevenue * 1.1)
|
|
326
|
+
],
|
|
327
|
+
allowDataOverflow: useLog,
|
|
328
|
+
tickFormatter: formatCurrency2
|
|
329
|
+
}), /* @__PURE__ */ React2.createElement(Tooltip2, {
|
|
330
|
+
content: /* @__PURE__ */ React2.createElement(CustomTooltip2, null),
|
|
331
|
+
cursor: {
|
|
332
|
+
stroke: "#22c55e",
|
|
333
|
+
strokeWidth: 1,
|
|
334
|
+
strokeDasharray: "4 4"
|
|
335
|
+
}
|
|
336
|
+
}), hasPrev && /* @__PURE__ */ React2.createElement(Area, {
|
|
337
|
+
type: "linear",
|
|
338
|
+
dataKey: "prevRevenue",
|
|
339
|
+
stroke: colors.mutedForeground,
|
|
340
|
+
strokeWidth: 1.5,
|
|
341
|
+
strokeDasharray: "4 3",
|
|
342
|
+
fill: "none",
|
|
343
|
+
dot: false,
|
|
344
|
+
activeDot: false,
|
|
345
|
+
connectNulls: true
|
|
346
|
+
}), /* @__PURE__ */ React2.createElement(Area, {
|
|
347
|
+
type: "linear",
|
|
348
|
+
dataKey: "revenue",
|
|
349
|
+
stroke: "#22c55e",
|
|
350
|
+
strokeWidth: 2,
|
|
351
|
+
fill: "url(#revenueGradient)",
|
|
352
|
+
dot: false,
|
|
353
|
+
activeDot: {
|
|
354
|
+
r: 5,
|
|
355
|
+
fill: "#22c55e",
|
|
356
|
+
stroke: colors.cardBg,
|
|
357
|
+
strokeWidth: 2
|
|
358
|
+
}
|
|
359
|
+
}))), hasPrev && /* @__PURE__ */ React2.createElement("div", {
|
|
360
|
+
className: "mt-2 flex items-center gap-4 text-[11px]"
|
|
361
|
+
}, /* @__PURE__ */ React2.createElement("span", {
|
|
362
|
+
className: "flex items-center gap-1.5"
|
|
363
|
+
}, /* @__PURE__ */ React2.createElement("span", {
|
|
364
|
+
className: "inline-block h-0.5 w-4 rounded bg-emerald-500"
|
|
365
|
+
}), /* @__PURE__ */ React2.createElement("span", {
|
|
366
|
+
className: "text-muted-foreground"
|
|
367
|
+
}, "Current period")), /* @__PURE__ */ React2.createElement("span", {
|
|
368
|
+
className: "flex items-center gap-1.5"
|
|
369
|
+
}, /* @__PURE__ */ React2.createElement("span", {
|
|
370
|
+
className: "inline-block h-0.5 w-4 rounded",
|
|
371
|
+
style: {
|
|
372
|
+
background: colors.mutedForeground,
|
|
373
|
+
backgroundImage: "repeating-linear-gradient(90deg, transparent, transparent 3px, currentColor 3px, currentColor 5px)"
|
|
374
|
+
}
|
|
375
|
+
}), /* @__PURE__ */ React2.createElement("span", {
|
|
376
|
+
className: "text-muted-foreground"
|
|
377
|
+
}, "Previous period"))));
|
|
378
|
+
}
|
|
379
|
+
__name(RevenueChart, "RevenueChart");
|
|
380
|
+
|
|
381
|
+
// src/components/omnibus-dashboard.tsx
|
|
382
|
+
function fmt$(v) {
|
|
383
|
+
return new Intl.NumberFormat("en-US", {
|
|
384
|
+
style: "currency",
|
|
385
|
+
currency: "USD",
|
|
386
|
+
minimumFractionDigits: 0,
|
|
387
|
+
maximumFractionDigits: 0
|
|
388
|
+
}).format(v);
|
|
389
|
+
}
|
|
390
|
+
__name(fmt$, "fmt$");
|
|
391
|
+
function fmtK(v) {
|
|
392
|
+
if (v >= 1e6)
|
|
393
|
+
return `${(v / 1e6).toFixed(1)}M`;
|
|
394
|
+
if (v >= 1e3)
|
|
395
|
+
return `${(v / 1e3).toFixed(1)}K`;
|
|
396
|
+
return v.toLocaleString();
|
|
397
|
+
}
|
|
398
|
+
__name(fmtK, "fmtK");
|
|
399
|
+
function fmtWatchMs(ms) {
|
|
400
|
+
const hours = ms / 1e3 / 60 / 60;
|
|
401
|
+
if (hours >= 1e3)
|
|
402
|
+
return `${(hours / 1e3).toFixed(1)}k hrs`;
|
|
403
|
+
if (hours >= 1)
|
|
404
|
+
return `${hours.toFixed(0)} hrs`;
|
|
405
|
+
return `${(ms / 1e3 / 60).toFixed(0)} min`;
|
|
406
|
+
}
|
|
407
|
+
__name(fmtWatchMs, "fmtWatchMs");
|
|
408
|
+
function fmtAgo(date) {
|
|
409
|
+
const ms = Date.now() - new Date(date).getTime();
|
|
410
|
+
const h = Math.floor(ms / 36e5);
|
|
411
|
+
if (h < 1)
|
|
412
|
+
return "just now";
|
|
413
|
+
if (h < 24)
|
|
414
|
+
return `${h}h ago`;
|
|
415
|
+
const d = Math.floor(h / 24);
|
|
416
|
+
return d === 1 ? "yesterday" : `${d}d ago`;
|
|
417
|
+
}
|
|
418
|
+
__name(fmtAgo, "fmtAgo");
|
|
419
|
+
function countryFlag2(code) {
|
|
420
|
+
if (!code || code.length !== 2)
|
|
421
|
+
return "\u{1F30D}";
|
|
422
|
+
const offset = 127462;
|
|
423
|
+
const a = code.toUpperCase().charCodeAt(0) - 65 + offset;
|
|
424
|
+
const b = code.toUpperCase().charCodeAt(1) - 65 + offset;
|
|
425
|
+
return String.fromCodePoint(a, b);
|
|
426
|
+
}
|
|
427
|
+
__name(countryFlag2, "countryFlag");
|
|
428
|
+
function Stat({ label, value, sub, icon: Icon, accent }) {
|
|
429
|
+
return /* @__PURE__ */ React3.createElement("div", {
|
|
430
|
+
className: `rounded-xl border p-4 transition-[transform,box-shadow] duration-[160ms] ease-[cubic-bezier(0.23,1,0.32,1)] active:scale-[0.97] motion-safe:hover:-translate-y-0.5 motion-safe:hover:shadow-md [@media(hover:hover)]:cursor-default ${accent ? "border-emerald-500/20 bg-emerald-500/[0.03]" : "border-border/50 bg-card/50"}`
|
|
431
|
+
}, /* @__PURE__ */ React3.createElement("div", {
|
|
432
|
+
className: "flex items-center justify-between"
|
|
433
|
+
}, /* @__PURE__ */ React3.createElement("span", {
|
|
434
|
+
className: "text-muted-foreground text-[11px] font-medium uppercase tracking-wider"
|
|
435
|
+
}, label), /* @__PURE__ */ React3.createElement(Icon, {
|
|
436
|
+
className: "text-muted-foreground/40 group-hover:text-muted-foreground/70 h-3.5 w-3.5 transition-colors duration-200"
|
|
437
|
+
})), /* @__PURE__ */ React3.createElement("div", {
|
|
438
|
+
className: "mt-1.5"
|
|
439
|
+
}, /* @__PURE__ */ React3.createElement("span", {
|
|
440
|
+
className: "text-foreground text-xl font-bold tabular-nums tracking-tight"
|
|
441
|
+
}, value)), sub && /* @__PURE__ */ React3.createElement("p", {
|
|
442
|
+
className: "text-muted-foreground mt-0.5 text-[11px] leading-relaxed"
|
|
443
|
+
}, sub));
|
|
444
|
+
}
|
|
445
|
+
__name(Stat, "Stat");
|
|
446
|
+
function AgentApiCard({ appName = "aihero" }) {
|
|
447
|
+
const [state, setState] = useState2("idle");
|
|
448
|
+
const baseUrl = typeof window !== "undefined" ? window.location.origin : "https://www.aihero.dev";
|
|
449
|
+
const endpoint = `${baseUrl}/api/analytics`;
|
|
450
|
+
const handleCopy = /* @__PURE__ */ __name(async () => {
|
|
451
|
+
setState("generating");
|
|
452
|
+
try {
|
|
453
|
+
const [tokenRes, catalogRes] = await Promise.all([
|
|
454
|
+
fetch("/api/analytics/token", {
|
|
455
|
+
method: "POST"
|
|
456
|
+
}),
|
|
457
|
+
fetch("/api/analytics").then((r) => r.json()).catch(() => null)
|
|
458
|
+
]);
|
|
459
|
+
if (!tokenRes.ok)
|
|
460
|
+
throw new Error("Failed to generate token");
|
|
461
|
+
const { token, ttlLabel, expiresAt } = await tokenRes.json();
|
|
462
|
+
const surfaces = catalogRes?.surfaces ?? [];
|
|
463
|
+
const categories = /* @__PURE__ */ new Map();
|
|
464
|
+
for (const s of surfaces) {
|
|
465
|
+
categories.set(s.category, (categories.get(s.category) ?? 0) + 1);
|
|
466
|
+
}
|
|
467
|
+
const categoryLine = [
|
|
468
|
+
...categories.entries()
|
|
469
|
+
].map(([cat, count]) => `${cat} (${count})`).join(", ");
|
|
470
|
+
const picks = [
|
|
471
|
+
{
|
|
472
|
+
name: "",
|
|
473
|
+
description: "surface catalog"
|
|
474
|
+
}
|
|
475
|
+
];
|
|
476
|
+
const seen = /* @__PURE__ */ new Set();
|
|
477
|
+
const preferred = [
|
|
478
|
+
"summary",
|
|
479
|
+
"attribution/coverage",
|
|
480
|
+
"traffic",
|
|
481
|
+
"youtube/videos",
|
|
482
|
+
"surveys",
|
|
483
|
+
"correlation/traffic-revenue",
|
|
484
|
+
"correlation/youtube-revenue",
|
|
485
|
+
"attribution/email-campaigns"
|
|
486
|
+
];
|
|
487
|
+
for (const name of preferred) {
|
|
488
|
+
const s = surfaces.find((x) => x.name === name);
|
|
489
|
+
if (s && !seen.has(s.category)) {
|
|
490
|
+
seen.add(s.category);
|
|
491
|
+
picks.push({
|
|
492
|
+
name: s.name,
|
|
493
|
+
description: s.description
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
for (const s of surfaces) {
|
|
498
|
+
if (!seen.has(s.category)) {
|
|
499
|
+
seen.add(s.category);
|
|
500
|
+
picks.push({
|
|
501
|
+
name: s.name,
|
|
502
|
+
description: s.description
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
const exampleLines = picks.map((p) => p.name ? `GET ${endpoint}?surface=${p.name}&range=30d \u2192 ${p.description}` : `GET ${endpoint} \u2192 ${p.description}`).join("\n");
|
|
507
|
+
const hasYouTube = surfaces.some((s) => s.category === "youtube");
|
|
508
|
+
const ytNote = hasYouTube ? `
|
|
509
|
+
Important:
|
|
510
|
+
- YouTube surfaces are useful for correlation and content analysis, not live dashboard ops
|
|
511
|
+
- YouTube Analytics data lags by about 48 hours, so call out the delay when interpreting fresh periods
|
|
512
|
+
` : "";
|
|
513
|
+
const prompt = `# ${appName} Analytics API
|
|
514
|
+
Base: ${endpoint}
|
|
515
|
+
Auth: Bearer ${token}
|
|
516
|
+
Token expires: ${new Date(expiresAt).toLocaleString()} (${ttlLabel})
|
|
517
|
+
|
|
518
|
+
${exampleLines}
|
|
519
|
+
${ytNote}
|
|
520
|
+
Example:
|
|
521
|
+
curl -H "Authorization: Bearer ${token}" "${endpoint}?surface=summary&range=30d"
|
|
522
|
+
|
|
523
|
+
Categories: ${categoryLine}
|
|
524
|
+
Every response has contextual next_actions. Errors have codes + fix hints.`;
|
|
525
|
+
await navigator.clipboard.writeText(prompt);
|
|
526
|
+
setState("copied");
|
|
527
|
+
setTimeout(() => setState("idle"), 3e3);
|
|
528
|
+
} catch {
|
|
529
|
+
setState("idle");
|
|
530
|
+
}
|
|
531
|
+
}, "handleCopy");
|
|
532
|
+
return /* @__PURE__ */ React3.createElement("div", {
|
|
533
|
+
className: "border-border/30 flex flex-wrap items-center justify-between gap-2 rounded-lg border px-3 py-2 sm:px-4 sm:py-2.5"
|
|
534
|
+
}, /* @__PURE__ */ React3.createElement("div", {
|
|
535
|
+
className: "flex min-w-0 items-center gap-2 text-xs"
|
|
536
|
+
}, /* @__PURE__ */ React3.createElement("span", {
|
|
537
|
+
className: "text-muted-foreground/60 shrink-0"
|
|
538
|
+
}, "\u26A1"), /* @__PURE__ */ React3.createElement("a", {
|
|
539
|
+
href: "/api/analytics",
|
|
540
|
+
target: "_blank",
|
|
541
|
+
rel: "noopener noreferrer",
|
|
542
|
+
className: "text-muted-foreground hover:text-foreground shrink-0 font-medium underline-offset-2 transition-colors hover:underline"
|
|
543
|
+
}, "/api/analytics"), /* @__PURE__ */ React3.createElement("span", {
|
|
544
|
+
className: "text-muted-foreground/40 hidden sm:inline"
|
|
545
|
+
}, "\xB7"), /* @__PURE__ */ React3.createElement("span", {
|
|
546
|
+
className: "text-muted-foreground/60 hidden sm:inline"
|
|
547
|
+
}, "HATEOAS catalog")), /* @__PURE__ */ React3.createElement("button", {
|
|
548
|
+
onClick: handleCopy,
|
|
549
|
+
disabled: state === "generating",
|
|
550
|
+
className: `flex shrink-0 items-center gap-1.5 rounded-md px-3 py-1.5 text-[11px] font-semibold transition-[transform,background-color,color,box-shadow] duration-[160ms] ease-[cubic-bezier(0.23,1,0.32,1)] active:scale-[0.97] disabled:pointer-events-none ${state === "copied" ? "bg-emerald-500/10 text-emerald-600 dark:text-emerald-400" : state === "generating" ? "bg-muted text-muted-foreground" : "bg-primary/10 text-primary hover:bg-primary/20 shadow-sm"}`
|
|
551
|
+
}, state === "generating" ? /* @__PURE__ */ React3.createElement(React3.Fragment, null, /* @__PURE__ */ React3.createElement(Loader2Icon, {
|
|
552
|
+
className: "h-3 w-3 animate-spin"
|
|
553
|
+
}), "Generating token\u2026") : state === "copied" ? /* @__PURE__ */ React3.createElement(React3.Fragment, null, /* @__PURE__ */ React3.createElement(CheckIcon, {
|
|
554
|
+
className: "h-3 w-3"
|
|
555
|
+
}), "Copied with token") : /* @__PURE__ */ React3.createElement(React3.Fragment, null, /* @__PURE__ */ React3.createElement(ClipboardIcon, {
|
|
556
|
+
className: "h-3 w-3"
|
|
557
|
+
}), "Copy agent prompt")));
|
|
558
|
+
}
|
|
559
|
+
__name(AgentApiCard, "AgentApiCard");
|
|
560
|
+
function TopVideosCard({ label, subtitle, icon: Icon, iconColor, videos }) {
|
|
561
|
+
if (videos.length === 0)
|
|
562
|
+
return null;
|
|
563
|
+
return /* @__PURE__ */ React3.createElement(Card, {
|
|
564
|
+
className: "flex-1"
|
|
565
|
+
}, /* @__PURE__ */ React3.createElement(CardHeader, {
|
|
566
|
+
className: "pb-3"
|
|
567
|
+
}, /* @__PURE__ */ React3.createElement("div", {
|
|
568
|
+
className: "flex items-center gap-2.5"
|
|
569
|
+
}, /* @__PURE__ */ React3.createElement("div", {
|
|
570
|
+
className: `flex h-7 w-7 items-center justify-center rounded-lg ${iconColor}`
|
|
571
|
+
}, /* @__PURE__ */ React3.createElement(Icon, {
|
|
572
|
+
className: "h-3.5 w-3.5"
|
|
573
|
+
})), /* @__PURE__ */ React3.createElement("div", null, /* @__PURE__ */ React3.createElement(CardTitle, {
|
|
574
|
+
className: "text-sm font-semibold"
|
|
575
|
+
}, label), /* @__PURE__ */ React3.createElement(CardDescription, {
|
|
576
|
+
className: "text-[11px]"
|
|
577
|
+
}, subtitle)))), /* @__PURE__ */ React3.createElement(CardContent, {
|
|
578
|
+
className: "space-y-0 pt-0"
|
|
579
|
+
}, videos.map((v, i) => {
|
|
580
|
+
const Row = v.href ? "a" : "div";
|
|
581
|
+
const linkProps = v.href ? {
|
|
582
|
+
href: v.href,
|
|
583
|
+
target: "_blank",
|
|
584
|
+
rel: "noopener noreferrer"
|
|
585
|
+
} : {};
|
|
586
|
+
return /* @__PURE__ */ React3.createElement(Row, {
|
|
587
|
+
key: v.title,
|
|
588
|
+
...linkProps,
|
|
589
|
+
className: "hover:bg-muted/40 group -mx-2 flex items-center gap-3 rounded-lg px-2 py-2.5 transition-[background-color] duration-[150ms] ease-[cubic-bezier(0.23,1,0.32,1)]"
|
|
590
|
+
}, /* @__PURE__ */ React3.createElement("span", {
|
|
591
|
+
className: "text-muted-foreground/50 w-4 text-right text-xs font-medium tabular-nums"
|
|
592
|
+
}, i + 1), v.thumbnailUrl ? /* @__PURE__ */ React3.createElement("img", {
|
|
593
|
+
src: v.thumbnailUrl,
|
|
594
|
+
alt: "",
|
|
595
|
+
width: 72,
|
|
596
|
+
height: 40,
|
|
597
|
+
loading: "lazy",
|
|
598
|
+
className: "hidden h-10 w-[72px] shrink-0 rounded object-cover sm:block"
|
|
599
|
+
}) : /* @__PURE__ */ React3.createElement("div", {
|
|
600
|
+
className: "bg-muted hidden h-10 w-[72px] shrink-0 items-center justify-center rounded sm:flex"
|
|
601
|
+
}, /* @__PURE__ */ React3.createElement(PlayIcon, {
|
|
602
|
+
className: "text-muted-foreground/40 h-4 w-4",
|
|
603
|
+
"aria-hidden": "true"
|
|
604
|
+
})), /* @__PURE__ */ React3.createElement("div", {
|
|
605
|
+
className: "min-w-0 flex-1"
|
|
606
|
+
}, /* @__PURE__ */ React3.createElement("p", {
|
|
607
|
+
className: "text-foreground/90 group-hover:text-foreground line-clamp-2 text-[13px] font-medium leading-snug transition-colors"
|
|
608
|
+
}, v.title), /* @__PURE__ */ React3.createElement("div", {
|
|
609
|
+
className: "text-muted-foreground mt-1 flex flex-wrap items-center gap-x-3 gap-y-0.5 text-[11px]"
|
|
610
|
+
}, /* @__PURE__ */ React3.createElement("span", {
|
|
611
|
+
className: "flex items-center gap-1"
|
|
612
|
+
}, /* @__PURE__ */ React3.createElement(ClockIcon, {
|
|
613
|
+
className: "h-3 w-3",
|
|
614
|
+
"aria-hidden": "true"
|
|
615
|
+
}), v.watchTime), /* @__PURE__ */ React3.createElement("span", null, fmtK(v.views), " views"), v.badge && /* @__PURE__ */ React3.createElement("span", {
|
|
616
|
+
className: "text-emerald-600 dark:text-emerald-400"
|
|
617
|
+
}, v.badge))), v.href && /* @__PURE__ */ React3.createElement(ExternalLinkIcon, {
|
|
618
|
+
className: "text-muted-foreground/0 group-hover:text-muted-foreground/60 h-3.5 w-3.5 shrink-0 transition-colors",
|
|
619
|
+
"aria-hidden": "true"
|
|
620
|
+
}));
|
|
621
|
+
})));
|
|
622
|
+
}
|
|
623
|
+
__name(TopVideosCard, "TopVideosCard");
|
|
624
|
+
function ShortlinksCard({ shortlinks, totalClicks }) {
|
|
625
|
+
const [expanded, setExpanded] = useState2(false);
|
|
626
|
+
if (shortlinks.length === 0)
|
|
627
|
+
return null;
|
|
628
|
+
const visible = expanded ? shortlinks : shortlinks.slice(0, 10);
|
|
629
|
+
const hasMore = shortlinks.length > 10;
|
|
630
|
+
const totalSignups = shortlinks.reduce((s, l) => s + l.signups, 0);
|
|
631
|
+
const totalPurchases = shortlinks.reduce((s, l) => s + l.purchases, 0);
|
|
632
|
+
return /* @__PURE__ */ React3.createElement(Card, null, /* @__PURE__ */ React3.createElement(CardHeader, null, /* @__PURE__ */ React3.createElement("div", {
|
|
633
|
+
className: "flex items-center gap-2.5"
|
|
634
|
+
}, /* @__PURE__ */ React3.createElement(LinkIcon, {
|
|
635
|
+
className: "text-muted-foreground h-4 w-4"
|
|
636
|
+
}), /* @__PURE__ */ React3.createElement("div", null, /* @__PURE__ */ React3.createElement(CardTitle, {
|
|
637
|
+
className: "text-sm font-semibold"
|
|
638
|
+
}, "Top Shortlinks"), /* @__PURE__ */ React3.createElement(CardDescription, {
|
|
639
|
+
className: "text-[11px]"
|
|
640
|
+
}, fmtK(totalClicks), " clicks \xB7 ", totalSignups.toLocaleString(), " ", "signups \xB7 ", totalPurchases.toLocaleString(), " purchases")))), /* @__PURE__ */ React3.createElement(CardContent, null, /* @__PURE__ */ React3.createElement("div", {
|
|
641
|
+
className: "overflow-x-auto"
|
|
642
|
+
}, /* @__PURE__ */ React3.createElement("table", {
|
|
643
|
+
className: "w-full"
|
|
644
|
+
}, /* @__PURE__ */ React3.createElement("thead", null, /* @__PURE__ */ React3.createElement("tr", {
|
|
645
|
+
className: "text-muted-foreground border-border/50 border-b text-left text-[11px] uppercase tracking-wider"
|
|
646
|
+
}, /* @__PURE__ */ React3.createElement("th", {
|
|
647
|
+
className: "pb-2.5 pr-4 font-medium"
|
|
648
|
+
}, "Link"), /* @__PURE__ */ React3.createElement("th", {
|
|
649
|
+
className: "pb-2.5 pr-4 font-medium"
|
|
650
|
+
}, "Destination"), /* @__PURE__ */ React3.createElement("th", {
|
|
651
|
+
className: "pb-2.5 text-right font-medium"
|
|
652
|
+
}, "Clicks"), /* @__PURE__ */ React3.createElement("th", {
|
|
653
|
+
className: "hidden pb-2.5 text-right font-medium sm:table-cell"
|
|
654
|
+
}, "Signups"), /* @__PURE__ */ React3.createElement("th", {
|
|
655
|
+
className: "hidden pb-2.5 text-right font-medium sm:table-cell"
|
|
656
|
+
}, "Purchases"))), /* @__PURE__ */ React3.createElement("tbody", {
|
|
657
|
+
className: "divide-border/30 divide-y"
|
|
658
|
+
}, visible.map((link) => /* @__PURE__ */ React3.createElement("tr", {
|
|
659
|
+
key: link.shortlinkId,
|
|
660
|
+
className: "group"
|
|
661
|
+
}, /* @__PURE__ */ React3.createElement("td", {
|
|
662
|
+
className: "py-2.5 pr-4 text-sm font-medium"
|
|
663
|
+
}, "/s/", link.slug), /* @__PURE__ */ React3.createElement("td", {
|
|
664
|
+
className: "text-muted-foreground max-w-xs truncate py-2.5 pr-4 text-sm"
|
|
665
|
+
}, /* @__PURE__ */ React3.createElement("a", {
|
|
666
|
+
href: link.url,
|
|
667
|
+
target: "_blank",
|
|
668
|
+
rel: "noopener noreferrer",
|
|
669
|
+
className: "hover:text-foreground inline-flex items-center gap-1 transition-colors"
|
|
670
|
+
}, link.url.replace(/^https?:\/\/(www\.)?/, "").substring(0, 50), /* @__PURE__ */ React3.createElement(ExternalLinkIcon, {
|
|
671
|
+
className: "h-3 w-3 opacity-0 transition-opacity group-hover:opacity-100",
|
|
672
|
+
"aria-hidden": "true"
|
|
673
|
+
}))), /* @__PURE__ */ React3.createElement("td", {
|
|
674
|
+
className: "text-foreground py-2.5 text-right text-sm font-semibold tabular-nums"
|
|
675
|
+
}, link.clicks.toLocaleString()), /* @__PURE__ */ React3.createElement("td", {
|
|
676
|
+
className: "hidden py-2.5 text-right text-sm tabular-nums sm:table-cell"
|
|
677
|
+
}, link.signups > 0 ? link.signups.toLocaleString() : "\u2013"), /* @__PURE__ */ React3.createElement("td", {
|
|
678
|
+
className: `hidden py-2.5 text-right text-sm font-semibold tabular-nums sm:table-cell ${link.purchases > 0 ? "text-emerald-600 dark:text-emerald-400" : "text-muted-foreground"}`
|
|
679
|
+
}, link.purchases > 0 ? link.purchases.toLocaleString() : "\u2013")))))), hasMore && /* @__PURE__ */ React3.createElement("button", {
|
|
680
|
+
onClick: () => setExpanded(!expanded),
|
|
681
|
+
className: "text-muted-foreground hover:text-foreground mt-3 flex w-full items-center justify-center gap-1 text-xs transition-[color,transform] duration-[160ms] ease-[cubic-bezier(0.23,1,0.32,1)] active:scale-[0.97]"
|
|
682
|
+
}, /* @__PURE__ */ React3.createElement(ChevronDownIcon, {
|
|
683
|
+
className: `h-3 w-3 transition-transform duration-[200ms] ease-[cubic-bezier(0.23,1,0.32,1)] ${expanded ? "rotate-180" : ""}`
|
|
684
|
+
}), expanded ? "Show less" : `Show ${shortlinks.length - 10} more`)));
|
|
685
|
+
}
|
|
686
|
+
__name(ShortlinksCard, "ShortlinksCard");
|
|
687
|
+
var RANGES = [
|
|
688
|
+
{
|
|
689
|
+
value: "24h",
|
|
690
|
+
label: "24h"
|
|
691
|
+
},
|
|
692
|
+
{
|
|
693
|
+
value: "7d",
|
|
694
|
+
label: "7d"
|
|
695
|
+
},
|
|
696
|
+
{
|
|
697
|
+
value: "30d",
|
|
698
|
+
label: "30d"
|
|
699
|
+
},
|
|
700
|
+
{
|
|
701
|
+
value: "90d",
|
|
702
|
+
label: "90d"
|
|
703
|
+
},
|
|
704
|
+
{
|
|
705
|
+
value: "all",
|
|
706
|
+
label: "All"
|
|
707
|
+
}
|
|
708
|
+
];
|
|
709
|
+
var rangeParser = parseAsStringLiteral([
|
|
710
|
+
"24h",
|
|
711
|
+
"7d",
|
|
712
|
+
"30d",
|
|
713
|
+
"90d",
|
|
714
|
+
"all"
|
|
715
|
+
]).withDefault("30d");
|
|
716
|
+
function OmnibusDashboard({ data, initialRange, appName, surveyDrilldownHref }) {
|
|
717
|
+
const [range, setRange] = useQueryState("range", rangeParser);
|
|
718
|
+
const [isPending, startTransition] = useTransition();
|
|
719
|
+
const rangeLabel = range === "24h" ? "24 hours" : range === "7d" ? "7 days" : range === "90d" ? "90 days" : range === "all" ? "all time" : "30 days";
|
|
720
|
+
const signupCount = data.attribution.find((a) => a.type === "signup")?.count ?? 0;
|
|
721
|
+
const purchaseAttrCount = data.attribution.find((a) => a.type === "purchase")?.count ?? 0;
|
|
722
|
+
const totalClicks = data.shortlinks.reduce((s, l) => s + l.clicks, 0);
|
|
723
|
+
const hasRevenue = data.summary.totalRevenue > 0 || data.summary.purchaseCount > 0;
|
|
724
|
+
const hasRevenueChart = data.daily.some((d) => d.revenue > 0);
|
|
725
|
+
const hasByProduct = data.byProduct.length > 0;
|
|
726
|
+
const hasByCountry = data.byCountry.length > 0;
|
|
727
|
+
const hasRecentPurchases = data.recentPurchases.length > 0;
|
|
728
|
+
const hasAttribution = data.attribution.length > 0;
|
|
729
|
+
const hasAttributionCoverage = data.attributionCoverage != null;
|
|
730
|
+
const hasMux = data.mux != null;
|
|
731
|
+
const hasTraffic = data.traffic != null;
|
|
732
|
+
const hasSurveys = data.surveySegments != null && data.surveySegments.length > 0;
|
|
733
|
+
const hasSurveyCorrelation = data.surveyCorrelation != null;
|
|
734
|
+
const hasShortlinks = data.shortlinks.length > 0;
|
|
735
|
+
return /* @__PURE__ */ React3.createElement("div", {
|
|
736
|
+
className: "flex flex-col gap-5 lg:gap-7"
|
|
737
|
+
}, /* @__PURE__ */ React3.createElement("div", {
|
|
738
|
+
className: "flex flex-col gap-3 sm:flex-row sm:items-end sm:justify-between"
|
|
739
|
+
}, /* @__PURE__ */ React3.createElement("div", {
|
|
740
|
+
className: "min-w-0"
|
|
741
|
+
}, /* @__PURE__ */ React3.createElement("h1", {
|
|
742
|
+
className: "text-pretty text-xl font-bold tracking-tight sm:text-2xl"
|
|
743
|
+
}, "Analytics"), /* @__PURE__ */ React3.createElement("p", {
|
|
744
|
+
className: "text-muted-foreground truncate text-[13px]"
|
|
745
|
+
}, "Revenue \xB7 Attribution \xB7 Video \xB7 Traffic \xB7 Surveys \u2014 ", rangeLabel)), /* @__PURE__ */ React3.createElement("div", {
|
|
746
|
+
className: "flex shrink-0 items-center gap-2"
|
|
747
|
+
}, isPending && /* @__PURE__ */ React3.createElement("span", {
|
|
748
|
+
className: "text-muted-foreground animate-pulse text-xs"
|
|
749
|
+
}, "Loading\u2026"), /* @__PURE__ */ React3.createElement("div", {
|
|
750
|
+
className: "border-border/40 bg-muted/20 inline-flex items-center gap-0.5 overflow-x-auto rounded-lg border p-0.5"
|
|
751
|
+
}, RANGES.map(({ value, label }) => /* @__PURE__ */ React3.createElement("button", {
|
|
752
|
+
key: value,
|
|
753
|
+
onClick: () => startTransition(() => {
|
|
754
|
+
setRange(value);
|
|
755
|
+
}),
|
|
756
|
+
disabled: isPending,
|
|
757
|
+
className: `shrink-0 rounded-md px-2.5 py-1 text-[11px] font-medium transition-[background-color,color,transform] duration-[150ms] ease-[cubic-bezier(0.23,1,0.32,1)] active:scale-[0.95] ${range === value ? "bg-foreground text-background shadow-sm" : "text-muted-foreground hover:text-foreground"} ${isPending ? "cursor-wait opacity-60" : ""}`
|
|
758
|
+
}, label))))), /* @__PURE__ */ React3.createElement("div", {
|
|
759
|
+
className: `flex flex-col gap-5 transition-opacity duration-200 lg:gap-7 ${isPending ? "pointer-events-none opacity-50" : ""}`
|
|
760
|
+
}, /* @__PURE__ */ React3.createElement(AgentApiCard, {
|
|
761
|
+
appName
|
|
762
|
+
}), /* @__PURE__ */ React3.createElement("div", {
|
|
763
|
+
className: "grid grid-cols-2 gap-2.5 lg:grid-cols-3 xl:grid-cols-5"
|
|
764
|
+
}, hasRevenue && /* @__PURE__ */ React3.createElement(Stat, {
|
|
765
|
+
label: "Revenue",
|
|
766
|
+
value: fmt$(data.summary.totalRevenue),
|
|
767
|
+
sub: `${data.summary.purchaseCount} purchases \xB7 ${fmt$(data.summary.avgOrderValue)} avg`,
|
|
768
|
+
icon: DollarSignIcon,
|
|
769
|
+
accent: true
|
|
770
|
+
}), hasMux && /* @__PURE__ */ React3.createElement(Stat, {
|
|
771
|
+
label: "Site Watch Time",
|
|
772
|
+
value: fmtWatchMs(data.mux.overview.totalPlayingTimeMs),
|
|
773
|
+
sub: `${fmtK(data.mux.overview.totalViews)} views \xB7 ${fmtK(data.mux.overview.uniqueViewers)} viewers`,
|
|
774
|
+
icon: FilmIcon
|
|
775
|
+
}), hasTraffic && /* @__PURE__ */ React3.createElement(Stat, {
|
|
776
|
+
label: "Sessions",
|
|
777
|
+
value: fmtK(data.traffic.sessions),
|
|
778
|
+
sub: `${fmtK(data.traffic.totalUsers)} users \xB7 ${fmtK(data.traffic.pageviews)} pages`,
|
|
779
|
+
icon: GlobeIcon
|
|
780
|
+
}), purchaseAttrCount > 0 && /* @__PURE__ */ React3.createElement(Stat, {
|
|
781
|
+
label: "Conversions",
|
|
782
|
+
value: `${purchaseAttrCount.toLocaleString()}`,
|
|
783
|
+
sub: `${signupCount} signups \xB7 ${totalClicks > 0 ? (purchaseAttrCount / signupCount * 100).toFixed(1) : 0}% signup\u2192purchase`,
|
|
784
|
+
icon: TrendingUpIcon
|
|
785
|
+
}), hasShortlinks && /* @__PURE__ */ React3.createElement(Stat, {
|
|
786
|
+
label: "Link Clicks",
|
|
787
|
+
value: fmtK(totalClicks),
|
|
788
|
+
sub: `${data.shortlinks.length} active \xB7 ${signupCount} signups`,
|
|
789
|
+
icon: MousePointerClickIcon
|
|
790
|
+
}), hasSurveys && /* @__PURE__ */ React3.createElement(Stat, {
|
|
791
|
+
label: "Surveys",
|
|
792
|
+
value: `${data.surveySegments.length}`,
|
|
793
|
+
sub: `${data.surveySegments.reduce((s, q) => s + q.responses, 0).toLocaleString()} responses`,
|
|
794
|
+
icon: ClipboardListIcon
|
|
795
|
+
})), hasMux && data.mux.topVideos.length > 0 && /* @__PURE__ */ React3.createElement(TopVideosCard, {
|
|
796
|
+
label: "Site Videos",
|
|
797
|
+
subtitle: `${fmtWatchMs(data.mux.overview.totalPlayingTimeMs)} watch time \xB7 ${fmtK(data.mux.overview.uniqueViewers)} viewers`,
|
|
798
|
+
icon: FilmIcon,
|
|
799
|
+
iconColor: "bg-violet-500/10 text-violet-500",
|
|
800
|
+
videos: data.mux.topVideos.slice(0, 5).map((v) => ({
|
|
801
|
+
title: v.title,
|
|
802
|
+
thumbnailUrl: data.muxThumbnails[v.title] ?? null,
|
|
803
|
+
watchTime: fmtWatchMs(v.playingTimeMs),
|
|
804
|
+
views: v.views,
|
|
805
|
+
href: null
|
|
806
|
+
}))
|
|
807
|
+
}), purchaseAttrCount > 0 && /* @__PURE__ */ React3.createElement(Card, null, /* @__PURE__ */ React3.createElement(CardHeader, {
|
|
808
|
+
className: "pb-3"
|
|
809
|
+
}, /* @__PURE__ */ React3.createElement("div", {
|
|
810
|
+
className: "flex items-center gap-2.5"
|
|
811
|
+
}, /* @__PURE__ */ React3.createElement(LinkIcon, {
|
|
812
|
+
className: "text-muted-foreground h-4 w-4"
|
|
813
|
+
}), /* @__PURE__ */ React3.createElement("div", null, /* @__PURE__ */ React3.createElement(CardTitle, {
|
|
814
|
+
className: "text-sm font-semibold"
|
|
815
|
+
}, "Attribution Trail"), /* @__PURE__ */ React3.createElement(CardDescription, {
|
|
816
|
+
className: "text-[11px]"
|
|
817
|
+
}, "Shortlink conversions \xB7 first-touch UTMs accumulating")))), /* @__PURE__ */ React3.createElement(CardContent, null, /* @__PURE__ */ React3.createElement("div", {
|
|
818
|
+
className: "grid gap-3 sm:grid-cols-3"
|
|
819
|
+
}, /* @__PURE__ */ React3.createElement("div", {
|
|
820
|
+
className: "bg-muted/20 rounded-lg p-3.5"
|
|
821
|
+
}, /* @__PURE__ */ React3.createElement("span", {
|
|
822
|
+
className: "text-muted-foreground block text-[11px] font-medium uppercase tracking-wider"
|
|
823
|
+
}, "Click \u2192 Signup"), /* @__PURE__ */ React3.createElement("span", {
|
|
824
|
+
className: "text-foreground mt-1 block text-xl font-bold tabular-nums"
|
|
825
|
+
}, signupCount.toLocaleString())), /* @__PURE__ */ React3.createElement("div", {
|
|
826
|
+
className: "bg-muted/20 rounded-lg p-3.5"
|
|
827
|
+
}, /* @__PURE__ */ React3.createElement("span", {
|
|
828
|
+
className: "text-muted-foreground block text-[11px] font-medium uppercase tracking-wider"
|
|
829
|
+
}, "Click \u2192 Purchase"), /* @__PURE__ */ React3.createElement("span", {
|
|
830
|
+
className: "text-foreground mt-1 block text-xl font-bold tabular-nums"
|
|
831
|
+
}, purchaseAttrCount.toLocaleString())), /* @__PURE__ */ React3.createElement("div", {
|
|
832
|
+
className: "bg-muted/20 rounded-lg p-3.5"
|
|
833
|
+
}, /* @__PURE__ */ React3.createElement("span", {
|
|
834
|
+
className: "text-muted-foreground block text-[11px] font-medium uppercase tracking-wider"
|
|
835
|
+
}, "Conversion Rate"), /* @__PURE__ */ React3.createElement("span", {
|
|
836
|
+
className: "text-foreground mt-1 block text-xl font-bold tabular-nums"
|
|
837
|
+
}, signupCount > 0 ? `${(purchaseAttrCount / signupCount * 100).toFixed(1)}%` : "\u2014"))))), hasAttributionCoverage && data.attributionCoverage.totalPurchases > 0 && /* @__PURE__ */ React3.createElement(Card, null, /* @__PURE__ */ React3.createElement(CardHeader, {
|
|
838
|
+
className: "pb-3"
|
|
839
|
+
}, /* @__PURE__ */ React3.createElement("div", {
|
|
840
|
+
className: "flex items-center gap-2.5"
|
|
841
|
+
}, /* @__PURE__ */ React3.createElement(TrendingUpIcon, {
|
|
842
|
+
className: "text-muted-foreground h-4 w-4"
|
|
843
|
+
}), /* @__PURE__ */ React3.createElement("div", null, /* @__PURE__ */ React3.createElement(CardTitle, {
|
|
844
|
+
className: "text-sm font-semibold"
|
|
845
|
+
}, "Attribution Coverage"), /* @__PURE__ */ React3.createElement(CardDescription, {
|
|
846
|
+
className: "text-[11px]"
|
|
847
|
+
}, "Attributed vs dark revenue")))), /* @__PURE__ */ React3.createElement(CardContent, null, /* @__PURE__ */ React3.createElement("div", {
|
|
848
|
+
className: "grid gap-3 sm:grid-cols-3"
|
|
849
|
+
}, /* @__PURE__ */ React3.createElement("div", {
|
|
850
|
+
className: "bg-muted/20 rounded-lg p-3.5"
|
|
851
|
+
}, /* @__PURE__ */ React3.createElement("span", {
|
|
852
|
+
className: "text-muted-foreground block text-[11px] font-medium uppercase tracking-wider"
|
|
853
|
+
}, "Attributed"), /* @__PURE__ */ React3.createElement("span", {
|
|
854
|
+
className: "text-foreground mt-1 block text-xl font-bold tabular-nums"
|
|
855
|
+
}, fmt$(data.attributionCoverage.attributedRevenue)), /* @__PURE__ */ React3.createElement("span", {
|
|
856
|
+
className: "text-muted-foreground text-[11px]"
|
|
857
|
+
}, (data.attributionCoverage.attributionRate * 100).toFixed(1), "% of total")), /* @__PURE__ */ React3.createElement("div", {
|
|
858
|
+
className: "bg-muted/20 rounded-lg p-3.5"
|
|
859
|
+
}, /* @__PURE__ */ React3.createElement("span", {
|
|
860
|
+
className: "text-muted-foreground block text-[11px] font-medium uppercase tracking-wider"
|
|
861
|
+
}, "Dark / Unknown"), /* @__PURE__ */ React3.createElement("span", {
|
|
862
|
+
className: "text-foreground mt-1 block text-xl font-bold tabular-nums"
|
|
863
|
+
}, fmt$(data.attributionCoverage.unattributedRevenue))), /* @__PURE__ */ React3.createElement("div", {
|
|
864
|
+
className: "bg-muted/20 rounded-lg p-3.5"
|
|
865
|
+
}, /* @__PURE__ */ React3.createElement("span", {
|
|
866
|
+
className: "text-muted-foreground block text-[11px] font-medium uppercase tracking-wider"
|
|
867
|
+
}, "Total Purchases"), /* @__PURE__ */ React3.createElement("span", {
|
|
868
|
+
className: "text-foreground mt-1 block text-xl font-bold tabular-nums"
|
|
869
|
+
}, data.attributionCoverage.totalPurchases.toLocaleString()))))), hasSurveys && /* @__PURE__ */ React3.createElement(Card, null, /* @__PURE__ */ React3.createElement(CardHeader, {
|
|
870
|
+
className: "pb-3"
|
|
871
|
+
}, /* @__PURE__ */ React3.createElement("div", {
|
|
872
|
+
className: "flex items-center justify-between"
|
|
873
|
+
}, /* @__PURE__ */ React3.createElement("div", {
|
|
874
|
+
className: "flex items-center gap-2.5"
|
|
875
|
+
}, /* @__PURE__ */ React3.createElement("div", {
|
|
876
|
+
className: "flex h-7 w-7 items-center justify-center rounded-lg bg-indigo-500/10 text-indigo-500"
|
|
877
|
+
}, /* @__PURE__ */ React3.createElement(ClipboardListIcon, {
|
|
878
|
+
className: "h-3.5 w-3.5"
|
|
879
|
+
})), /* @__PURE__ */ React3.createElement("div", null, /* @__PURE__ */ React3.createElement(CardTitle, {
|
|
880
|
+
className: "text-sm font-semibold"
|
|
881
|
+
}, "Survey Segments"), /* @__PURE__ */ React3.createElement(CardDescription, {
|
|
882
|
+
className: "text-[11px]"
|
|
883
|
+
}, "How users self-categorize across survey questions"))), surveyDrilldownHref && /* @__PURE__ */ React3.createElement("a", {
|
|
884
|
+
href: surveyDrilldownHref,
|
|
885
|
+
className: "text-muted-foreground hover:text-foreground text-[11px] font-medium transition-colors"
|
|
886
|
+
}, "View all surveys \u2192"))), /* @__PURE__ */ React3.createElement(CardContent, {
|
|
887
|
+
className: "space-y-6"
|
|
888
|
+
}, data.surveySegments.slice(0, 8).map((q) => {
|
|
889
|
+
const totalForQuestion = q.answerDistribution.reduce((s, a) => s + a.count, 0);
|
|
890
|
+
return /* @__PURE__ */ React3.createElement("div", {
|
|
891
|
+
key: q.questionId
|
|
892
|
+
}, /* @__PURE__ */ React3.createElement("div", {
|
|
893
|
+
className: "mb-2.5 flex items-start justify-between gap-2"
|
|
894
|
+
}, /* @__PURE__ */ React3.createElement("p", {
|
|
895
|
+
className: "text-foreground text-[13px] font-medium leading-snug"
|
|
896
|
+
}, q.question), /* @__PURE__ */ React3.createElement("span", {
|
|
897
|
+
className: "text-muted-foreground shrink-0 text-[11px] tabular-nums"
|
|
898
|
+
}, totalForQuestion.toLocaleString(), " responses")), /* @__PURE__ */ React3.createElement("div", {
|
|
899
|
+
className: "space-y-1.5"
|
|
900
|
+
}, q.answerDistribution.slice(0, 6).map((a) => {
|
|
901
|
+
const pct = totalForQuestion > 0 ? a.count / totalForQuestion * 100 : 0;
|
|
902
|
+
return /* @__PURE__ */ React3.createElement("div", {
|
|
903
|
+
key: a.answer,
|
|
904
|
+
className: "group flex items-center gap-2.5"
|
|
905
|
+
}, /* @__PURE__ */ React3.createElement("span", {
|
|
906
|
+
className: "text-muted-foreground w-[120px] shrink-0 truncate text-right text-[12px]",
|
|
907
|
+
title: a.answer
|
|
908
|
+
}, a.answer), /* @__PURE__ */ React3.createElement("div", {
|
|
909
|
+
className: "bg-muted/50 relative h-5 flex-1 overflow-hidden rounded"
|
|
910
|
+
}, /* @__PURE__ */ React3.createElement("div", {
|
|
911
|
+
className: "absolute inset-y-0 left-0 rounded bg-indigo-500/20 transition-all duration-300",
|
|
912
|
+
style: {
|
|
913
|
+
width: `${Math.max(pct, 1)}%`
|
|
914
|
+
}
|
|
915
|
+
}), /* @__PURE__ */ React3.createElement("div", {
|
|
916
|
+
className: "relative flex h-full items-center px-2"
|
|
917
|
+
}, /* @__PURE__ */ React3.createElement("span", {
|
|
918
|
+
className: "text-foreground/70 text-[11px] font-medium tabular-nums"
|
|
919
|
+
}, pct.toFixed(0), "%"))), /* @__PURE__ */ React3.createElement("span", {
|
|
920
|
+
className: "text-muted-foreground w-10 shrink-0 text-right text-[11px] tabular-nums"
|
|
921
|
+
}, a.count.toLocaleString()));
|
|
922
|
+
}), q.answerDistribution.length > 6 && /* @__PURE__ */ React3.createElement("p", {
|
|
923
|
+
className: "text-muted-foreground/60 pl-[132px] text-[11px]"
|
|
924
|
+
}, "+", q.answerDistribution.length - 6, " more answers")));
|
|
925
|
+
}))), hasSurveyCorrelation && /* @__PURE__ */ React3.createElement(Card, null, /* @__PURE__ */ React3.createElement(CardHeader, {
|
|
926
|
+
className: "pb-3"
|
|
927
|
+
}, /* @__PURE__ */ React3.createElement("div", {
|
|
928
|
+
className: "flex items-center gap-2.5"
|
|
929
|
+
}, /* @__PURE__ */ React3.createElement("div", {
|
|
930
|
+
className: "flex h-7 w-7 items-center justify-center rounded-lg bg-emerald-500/10 text-emerald-500"
|
|
931
|
+
}, /* @__PURE__ */ React3.createElement(TrendingUpIcon, {
|
|
932
|
+
className: "h-3.5 w-3.5"
|
|
933
|
+
})), /* @__PURE__ */ React3.createElement("div", null, /* @__PURE__ */ React3.createElement(CardTitle, {
|
|
934
|
+
className: "text-sm font-semibold"
|
|
935
|
+
}, "Survey \u2192 Purchase Correlation"), /* @__PURE__ */ React3.createElement(CardDescription, {
|
|
936
|
+
className: "text-[11px]"
|
|
937
|
+
}, "Which survey answers predict purchases? We match survey respondents against the purchases table to find conversion rates per answer.")))), /* @__PURE__ */ React3.createElement(CardContent, {
|
|
938
|
+
className: "space-y-5"
|
|
939
|
+
}, /* @__PURE__ */ React3.createElement("p", {
|
|
940
|
+
className: "text-muted-foreground text-[12px] leading-relaxed"
|
|
941
|
+
}, "We look at every user who answered a survey and check if they ever made a purchase.", /* @__PURE__ */ React3.createElement("strong", {
|
|
942
|
+
className: "text-foreground"
|
|
943
|
+
}, " Conversion"), " = respondents who purchased / total respondents.", /* @__PURE__ */ React3.createElement("strong", {
|
|
944
|
+
className: "text-foreground"
|
|
945
|
+
}, " Baseline"), " = purchase rate of users who ", /* @__PURE__ */ React3.createElement("em", null, "never"), " took a survey, so you can see if survey-takers convert at a higher or lower rate.", /* @__PURE__ */ React3.createElement("strong", {
|
|
946
|
+
className: "text-foreground"
|
|
947
|
+
}, " Revenue"), " = total lifetime spend of respondents who purchased."), (() => {
|
|
948
|
+
const sc = data.surveyCorrelation;
|
|
949
|
+
const totalRespondents = sc.totalRespondents ?? 0;
|
|
950
|
+
const purchased = sc.respondentsWhoPurchased ?? 0;
|
|
951
|
+
const convRate = sc.overallConversionRate ?? 0;
|
|
952
|
+
const revenue = sc.totalRevenueFromRespondents ?? 0;
|
|
953
|
+
const baseline = sc.baselineConversionRate ?? 0;
|
|
954
|
+
const pre = sc.prePurchaseRespondents ?? 0;
|
|
955
|
+
const post = sc.postPurchaseRespondents ?? 0;
|
|
956
|
+
const never = sc.neverPurchasedRespondents ?? 0;
|
|
957
|
+
return /* @__PURE__ */ React3.createElement(React3.Fragment, null, /* @__PURE__ */ React3.createElement("div", {
|
|
958
|
+
className: "grid grid-cols-2 gap-2 sm:grid-cols-5"
|
|
959
|
+
}, /* @__PURE__ */ React3.createElement("div", {
|
|
960
|
+
className: "bg-muted/20 rounded-lg p-3"
|
|
961
|
+
}, /* @__PURE__ */ React3.createElement("span", {
|
|
962
|
+
className: "text-muted-foreground block text-[11px] font-medium uppercase tracking-wider"
|
|
963
|
+
}, "Respondents"), /* @__PURE__ */ React3.createElement("span", {
|
|
964
|
+
className: "text-foreground mt-1 block text-lg font-bold tabular-nums"
|
|
965
|
+
}, totalRespondents.toLocaleString()), /* @__PURE__ */ React3.createElement("span", {
|
|
966
|
+
className: "text-muted-foreground/70 text-[10px]"
|
|
967
|
+
}, "unique users who answered")), /* @__PURE__ */ React3.createElement("div", {
|
|
968
|
+
className: "bg-muted/20 rounded-lg p-3"
|
|
969
|
+
}, /* @__PURE__ */ React3.createElement("span", {
|
|
970
|
+
className: "text-muted-foreground block text-[11px] font-medium uppercase tracking-wider"
|
|
971
|
+
}, "Purchased"), /* @__PURE__ */ React3.createElement("span", {
|
|
972
|
+
className: "text-foreground mt-1 block text-lg font-bold tabular-nums"
|
|
973
|
+
}, purchased.toLocaleString()), /* @__PURE__ */ React3.createElement("span", {
|
|
974
|
+
className: "text-muted-foreground/70 text-[10px]"
|
|
975
|
+
}, "of those also bought")), /* @__PURE__ */ React3.createElement("div", {
|
|
976
|
+
className: "bg-muted/20 rounded-lg p-3"
|
|
977
|
+
}, /* @__PURE__ */ React3.createElement("span", {
|
|
978
|
+
className: "text-muted-foreground block text-[11px] font-medium uppercase tracking-wider"
|
|
979
|
+
}, "Conversion"), /* @__PURE__ */ React3.createElement("span", {
|
|
980
|
+
className: "text-foreground mt-1 block text-lg font-bold tabular-nums"
|
|
981
|
+
}, (convRate * 100).toFixed(1), "%"), /* @__PURE__ */ React3.createElement("span", {
|
|
982
|
+
className: "text-muted-foreground/70 text-[10px]"
|
|
983
|
+
}, "respondent \u2192 purchaser")), /* @__PURE__ */ React3.createElement("div", {
|
|
984
|
+
className: "bg-muted/20 rounded-lg p-3"
|
|
985
|
+
}, /* @__PURE__ */ React3.createElement("span", {
|
|
986
|
+
className: "text-muted-foreground block text-[11px] font-medium uppercase tracking-wider"
|
|
987
|
+
}, "Revenue"), /* @__PURE__ */ React3.createElement("span", {
|
|
988
|
+
className: "text-foreground mt-1 block text-lg font-bold tabular-nums"
|
|
989
|
+
}, fmt$(revenue)), /* @__PURE__ */ React3.createElement("span", {
|
|
990
|
+
className: "text-muted-foreground/70 text-[10px]"
|
|
991
|
+
}, "lifetime spend of purchasers")), /* @__PURE__ */ React3.createElement("div", {
|
|
992
|
+
className: "bg-muted/20 rounded-lg p-3"
|
|
993
|
+
}, /* @__PURE__ */ React3.createElement("span", {
|
|
994
|
+
className: "text-muted-foreground block text-[11px] font-medium uppercase tracking-wider"
|
|
995
|
+
}, "Baseline"), /* @__PURE__ */ React3.createElement("span", {
|
|
996
|
+
className: "text-foreground mt-1 block text-lg font-bold tabular-nums"
|
|
997
|
+
}, (baseline * 100).toFixed(1), "%"), /* @__PURE__ */ React3.createElement("span", {
|
|
998
|
+
className: "text-muted-foreground/70 text-[10px]"
|
|
999
|
+
}, "users who never took a survey"))), /* @__PURE__ */ React3.createElement("div", {
|
|
1000
|
+
className: "border-border/30 rounded-lg border px-4 py-3"
|
|
1001
|
+
}, /* @__PURE__ */ React3.createElement("p", {
|
|
1002
|
+
className: "text-muted-foreground mb-2 text-[11px]"
|
|
1003
|
+
}, "Of the ", purchased.toLocaleString(), " purchasers, when did they first respond relative to their first purchase?"), /* @__PURE__ */ React3.createElement("div", {
|
|
1004
|
+
className: "flex flex-wrap items-center gap-x-6 gap-y-2"
|
|
1005
|
+
}, /* @__PURE__ */ React3.createElement("div", {
|
|
1006
|
+
className: "flex items-center gap-2"
|
|
1007
|
+
}, /* @__PURE__ */ React3.createElement("span", {
|
|
1008
|
+
className: "inline-block h-2 w-2 rounded-full bg-emerald-500"
|
|
1009
|
+
}), /* @__PURE__ */ React3.createElement("span", {
|
|
1010
|
+
className: "text-[12px]"
|
|
1011
|
+
}, "Responded first, then bought:", " ", /* @__PURE__ */ React3.createElement("span", {
|
|
1012
|
+
className: "text-foreground font-semibold tabular-nums"
|
|
1013
|
+
}, pre.toLocaleString()))), /* @__PURE__ */ React3.createElement("div", {
|
|
1014
|
+
className: "flex items-center gap-2"
|
|
1015
|
+
}, /* @__PURE__ */ React3.createElement("span", {
|
|
1016
|
+
className: "inline-block h-2 w-2 rounded-full bg-blue-500"
|
|
1017
|
+
}), /* @__PURE__ */ React3.createElement("span", {
|
|
1018
|
+
className: "text-[12px]"
|
|
1019
|
+
}, "Bought first, then responded:", " ", /* @__PURE__ */ React3.createElement("span", {
|
|
1020
|
+
className: "text-foreground font-semibold tabular-nums"
|
|
1021
|
+
}, post.toLocaleString()))), /* @__PURE__ */ React3.createElement("div", {
|
|
1022
|
+
className: "flex items-center gap-2"
|
|
1023
|
+
}, /* @__PURE__ */ React3.createElement("span", {
|
|
1024
|
+
className: "inline-block h-2 w-2 rounded-full bg-gray-400"
|
|
1025
|
+
}), /* @__PURE__ */ React3.createElement("span", {
|
|
1026
|
+
className: "text-[12px]"
|
|
1027
|
+
}, "Responded but never bought:", " ", /* @__PURE__ */ React3.createElement("span", {
|
|
1028
|
+
className: "text-foreground font-semibold tabular-nums"
|
|
1029
|
+
}, never.toLocaleString()))))));
|
|
1030
|
+
})(), (() => {
|
|
1031
|
+
const allRows = data.surveyCorrelation.byQuestion ?? [];
|
|
1032
|
+
if (allRows.length === 0)
|
|
1033
|
+
return null;
|
|
1034
|
+
const seen = {};
|
|
1035
|
+
const questionIds = [];
|
|
1036
|
+
const answerCounts = {};
|
|
1037
|
+
for (const r of allRows) {
|
|
1038
|
+
if (!seen[r.questionId]) {
|
|
1039
|
+
seen[r.questionId] = true;
|
|
1040
|
+
questionIds.push(r.questionId);
|
|
1041
|
+
answerCounts[r.questionId] = 0;
|
|
1042
|
+
}
|
|
1043
|
+
answerCounts[r.questionId]++;
|
|
1044
|
+
}
|
|
1045
|
+
const filteredQIds = questionIds.filter((qId) => (answerCounts[qId] ?? 0) <= 10);
|
|
1046
|
+
if (filteredQIds.length === 0)
|
|
1047
|
+
return null;
|
|
1048
|
+
return /* @__PURE__ */ React3.createElement("div", {
|
|
1049
|
+
className: "space-y-6"
|
|
1050
|
+
}, /* @__PURE__ */ React3.createElement("p", {
|
|
1051
|
+
className: "text-muted-foreground text-[11px]"
|
|
1052
|
+
}, "Per answer: of everyone who chose this option, what % went on to purchase?"), filteredQIds.map((qId) => {
|
|
1053
|
+
const answers = allRows.filter((r) => r.questionId === qId && r.answer !== "[free text]").sort((a, b) => (b.respondents ?? 0) - (a.respondents ?? 0));
|
|
1054
|
+
if (answers.length === 0)
|
|
1055
|
+
return null;
|
|
1056
|
+
const title = answers[0]?.questionTitle ?? qId;
|
|
1057
|
+
return /* @__PURE__ */ React3.createElement("div", {
|
|
1058
|
+
key: qId
|
|
1059
|
+
}, /* @__PURE__ */ React3.createElement("p", {
|
|
1060
|
+
className: "text-foreground mb-2 text-[13px] font-semibold leading-snug"
|
|
1061
|
+
}, title), /* @__PURE__ */ React3.createElement("div", {
|
|
1062
|
+
className: "space-y-1.5"
|
|
1063
|
+
}, answers.map((row) => {
|
|
1064
|
+
const pct = (row.conversionRate ?? 0) * 100;
|
|
1065
|
+
const purchased = row.purchasers ?? 0;
|
|
1066
|
+
const responded = row.respondents ?? 0;
|
|
1067
|
+
return /* @__PURE__ */ React3.createElement("div", {
|
|
1068
|
+
key: row.answer,
|
|
1069
|
+
className: "group flex items-center gap-2.5"
|
|
1070
|
+
}, /* @__PURE__ */ React3.createElement("span", {
|
|
1071
|
+
className: "text-muted-foreground w-[120px] shrink-0 truncate text-right text-[12px]",
|
|
1072
|
+
title: row.answer
|
|
1073
|
+
}, row.answer), /* @__PURE__ */ React3.createElement("div", {
|
|
1074
|
+
className: "bg-muted/50 relative h-5 flex-1 overflow-hidden rounded"
|
|
1075
|
+
}, /* @__PURE__ */ React3.createElement("div", {
|
|
1076
|
+
className: "absolute inset-y-0 left-0 rounded bg-emerald-500/20 transition-all duration-300",
|
|
1077
|
+
style: {
|
|
1078
|
+
width: `${Math.max(pct, 1)}%`
|
|
1079
|
+
}
|
|
1080
|
+
}), /* @__PURE__ */ React3.createElement("div", {
|
|
1081
|
+
className: "relative flex h-full items-center px-2"
|
|
1082
|
+
}, /* @__PURE__ */ React3.createElement("span", {
|
|
1083
|
+
className: "text-foreground/70 text-[11px] font-medium tabular-nums"
|
|
1084
|
+
}, pct.toFixed(1), "%"))), /* @__PURE__ */ React3.createElement("span", {
|
|
1085
|
+
className: "text-muted-foreground w-12 shrink-0 text-right text-[11px] tabular-nums"
|
|
1086
|
+
}, purchased, "/", responded));
|
|
1087
|
+
})));
|
|
1088
|
+
}));
|
|
1089
|
+
})())), hasRevenueChart && /* @__PURE__ */ React3.createElement(Card, {
|
|
1090
|
+
className: "overflow-hidden"
|
|
1091
|
+
}, /* @__PURE__ */ React3.createElement(CardHeader, {
|
|
1092
|
+
className: "pb-3"
|
|
1093
|
+
}, /* @__PURE__ */ React3.createElement("div", {
|
|
1094
|
+
className: "flex items-center gap-2.5"
|
|
1095
|
+
}, /* @__PURE__ */ React3.createElement(TrendingUpIcon, {
|
|
1096
|
+
className: "text-muted-foreground h-4 w-4"
|
|
1097
|
+
}), /* @__PURE__ */ React3.createElement("div", null, /* @__PURE__ */ React3.createElement(CardTitle, {
|
|
1098
|
+
className: "text-sm font-semibold"
|
|
1099
|
+
}, "Daily Revenue"), /* @__PURE__ */ React3.createElement(CardDescription, {
|
|
1100
|
+
className: "text-[11px]"
|
|
1101
|
+
}, rangeLabel)))), /* @__PURE__ */ React3.createElement(CardContent, {
|
|
1102
|
+
className: "pb-3"
|
|
1103
|
+
}, /* @__PURE__ */ React3.createElement(RevenueChart, {
|
|
1104
|
+
data: data.daily,
|
|
1105
|
+
previousData: data.previousDaily
|
|
1106
|
+
}))), (hasByProduct || hasByCountry) && /* @__PURE__ */ React3.createElement("div", {
|
|
1107
|
+
className: "grid gap-3 md:grid-cols-2"
|
|
1108
|
+
}, hasByProduct && /* @__PURE__ */ React3.createElement(Card, null, /* @__PURE__ */ React3.createElement(CardHeader, {
|
|
1109
|
+
className: "pb-3"
|
|
1110
|
+
}, /* @__PURE__ */ React3.createElement("div", {
|
|
1111
|
+
className: "flex items-center gap-2.5"
|
|
1112
|
+
}, /* @__PURE__ */ React3.createElement(ShoppingCartIcon, {
|
|
1113
|
+
className: "text-muted-foreground h-4 w-4"
|
|
1114
|
+
}), /* @__PURE__ */ React3.createElement("div", null, /* @__PURE__ */ React3.createElement(CardTitle, {
|
|
1115
|
+
className: "text-sm font-semibold"
|
|
1116
|
+
}, "By Product"), /* @__PURE__ */ React3.createElement(CardDescription, {
|
|
1117
|
+
className: "text-[11px]"
|
|
1118
|
+
}, "Revenue breakdown")))), /* @__PURE__ */ React3.createElement(CardContent, null, /* @__PURE__ */ React3.createElement("div", {
|
|
1119
|
+
className: "flex flex-col gap-3"
|
|
1120
|
+
}, data.byProduct.map((p) => {
|
|
1121
|
+
const pct = data.summary.totalRevenue > 0 ? p.revenue / data.summary.totalRevenue * 100 : 0;
|
|
1122
|
+
return /* @__PURE__ */ React3.createElement("div", {
|
|
1123
|
+
key: p.productId,
|
|
1124
|
+
className: "space-y-1.5"
|
|
1125
|
+
}, /* @__PURE__ */ React3.createElement("div", {
|
|
1126
|
+
className: "flex items-center justify-between gap-2"
|
|
1127
|
+
}, /* @__PURE__ */ React3.createElement("span", {
|
|
1128
|
+
className: "truncate text-sm font-medium"
|
|
1129
|
+
}, p.productName), /* @__PURE__ */ React3.createElement("span", {
|
|
1130
|
+
className: "text-foreground shrink-0 text-sm font-semibold tabular-nums"
|
|
1131
|
+
}, fmt$(p.revenue))), /* @__PURE__ */ React3.createElement("div", {
|
|
1132
|
+
className: "flex items-center gap-2"
|
|
1133
|
+
}, /* @__PURE__ */ React3.createElement("div", {
|
|
1134
|
+
className: "bg-muted/50 relative h-1.5 flex-1 overflow-hidden rounded-full"
|
|
1135
|
+
}, /* @__PURE__ */ React3.createElement("div", {
|
|
1136
|
+
className: "absolute inset-y-0 left-0 rounded-full bg-emerald-500/60",
|
|
1137
|
+
style: {
|
|
1138
|
+
width: `${Math.min(pct, 100)}%`
|
|
1139
|
+
}
|
|
1140
|
+
})), /* @__PURE__ */ React3.createElement("span", {
|
|
1141
|
+
className: "text-muted-foreground text-[11px] tabular-nums"
|
|
1142
|
+
}, p.count)));
|
|
1143
|
+
})))), hasByCountry && /* @__PURE__ */ React3.createElement(Card, {
|
|
1144
|
+
className: "overflow-hidden"
|
|
1145
|
+
}, /* @__PURE__ */ React3.createElement(CardHeader, {
|
|
1146
|
+
className: "pb-3"
|
|
1147
|
+
}, /* @__PURE__ */ React3.createElement("div", {
|
|
1148
|
+
className: "flex items-center gap-2.5"
|
|
1149
|
+
}, /* @__PURE__ */ React3.createElement(GlobeIcon, {
|
|
1150
|
+
className: "text-muted-foreground h-4 w-4"
|
|
1151
|
+
}), /* @__PURE__ */ React3.createElement("div", null, /* @__PURE__ */ React3.createElement(CardTitle, {
|
|
1152
|
+
className: "text-sm font-semibold"
|
|
1153
|
+
}, "By Country"), /* @__PURE__ */ React3.createElement(CardDescription, {
|
|
1154
|
+
className: "text-[11px]"
|
|
1155
|
+
}, "Top 10 by revenue")))), /* @__PURE__ */ React3.createElement(CardContent, {
|
|
1156
|
+
className: "pb-3"
|
|
1157
|
+
}, /* @__PURE__ */ React3.createElement(CountryChart, {
|
|
1158
|
+
data: data.byCountry
|
|
1159
|
+
})))), hasMux && data.mux.topVideos.length > 5 && /* @__PURE__ */ React3.createElement(Card, null, /* @__PURE__ */ React3.createElement(CardHeader, {
|
|
1160
|
+
className: "pb-3"
|
|
1161
|
+
}, /* @__PURE__ */ React3.createElement("div", {
|
|
1162
|
+
className: "flex items-center gap-2.5"
|
|
1163
|
+
}, /* @__PURE__ */ React3.createElement(FilmIcon, {
|
|
1164
|
+
className: "text-muted-foreground h-4 w-4"
|
|
1165
|
+
}), /* @__PURE__ */ React3.createElement("div", null, /* @__PURE__ */ React3.createElement(CardTitle, {
|
|
1166
|
+
className: "text-sm font-semibold"
|
|
1167
|
+
}, "All Site Videos"), /* @__PURE__ */ React3.createElement(CardDescription, {
|
|
1168
|
+
className: "text-[11px]"
|
|
1169
|
+
}, "By watch time (Mux)")))), /* @__PURE__ */ React3.createElement(CardContent, null, /* @__PURE__ */ React3.createElement("div", {
|
|
1170
|
+
className: "overflow-x-auto"
|
|
1171
|
+
}, /* @__PURE__ */ React3.createElement("table", {
|
|
1172
|
+
className: "w-full"
|
|
1173
|
+
}, /* @__PURE__ */ React3.createElement("thead", null, /* @__PURE__ */ React3.createElement("tr", {
|
|
1174
|
+
className: "text-muted-foreground border-border/50 border-b text-left text-[11px] uppercase tracking-wider"
|
|
1175
|
+
}, /* @__PURE__ */ React3.createElement("th", {
|
|
1176
|
+
className: "pb-2.5 pr-4 font-medium"
|
|
1177
|
+
}, "#"), /* @__PURE__ */ React3.createElement("th", {
|
|
1178
|
+
className: "pb-2.5 pr-4 font-medium"
|
|
1179
|
+
}, "Title"), /* @__PURE__ */ React3.createElement("th", {
|
|
1180
|
+
className: "pb-2.5 pr-4 text-right font-medium"
|
|
1181
|
+
}, "Watch Time"), /* @__PURE__ */ React3.createElement("th", {
|
|
1182
|
+
className: "pb-2.5 text-right font-medium"
|
|
1183
|
+
}, "Views"))), /* @__PURE__ */ React3.createElement("tbody", {
|
|
1184
|
+
className: "divide-border/30 divide-y"
|
|
1185
|
+
}, data.mux.topVideos.slice(0, 10).map((v, i) => /* @__PURE__ */ React3.createElement("tr", {
|
|
1186
|
+
key: v.title,
|
|
1187
|
+
className: "group"
|
|
1188
|
+
}, /* @__PURE__ */ React3.createElement("td", {
|
|
1189
|
+
className: "text-muted-foreground py-2.5 pr-4 text-sm tabular-nums"
|
|
1190
|
+
}, i + 1), /* @__PURE__ */ React3.createElement("td", {
|
|
1191
|
+
className: "max-w-[300px] truncate py-2.5 pr-4 text-sm font-medium"
|
|
1192
|
+
}, v.title), /* @__PURE__ */ React3.createElement("td", {
|
|
1193
|
+
className: "text-foreground py-2.5 pr-4 text-right text-sm font-semibold tabular-nums"
|
|
1194
|
+
}, fmtWatchMs(v.playingTimeMs)), /* @__PURE__ */ React3.createElement("td", {
|
|
1195
|
+
className: "text-muted-foreground py-2.5 text-right text-sm tabular-nums"
|
|
1196
|
+
}, fmtK(v.views))))))))), hasShortlinks && /* @__PURE__ */ React3.createElement(ShortlinksCard, {
|
|
1197
|
+
shortlinks: data.shortlinks,
|
|
1198
|
+
totalClicks
|
|
1199
|
+
}), hasRecentPurchases && /* @__PURE__ */ React3.createElement(Card, null, /* @__PURE__ */ React3.createElement(CardHeader, {
|
|
1200
|
+
className: "pb-3"
|
|
1201
|
+
}, /* @__PURE__ */ React3.createElement("div", {
|
|
1202
|
+
className: "flex items-center justify-between"
|
|
1203
|
+
}, /* @__PURE__ */ React3.createElement("div", {
|
|
1204
|
+
className: "flex items-center gap-2.5"
|
|
1205
|
+
}, /* @__PURE__ */ React3.createElement("div", {
|
|
1206
|
+
className: "bg-muted flex h-6 w-6 items-center justify-center rounded-md"
|
|
1207
|
+
}, /* @__PURE__ */ React3.createElement(ShoppingCartIcon, {
|
|
1208
|
+
className: "text-muted-foreground h-3.5 w-3.5"
|
|
1209
|
+
})), /* @__PURE__ */ React3.createElement("div", null, /* @__PURE__ */ React3.createElement(CardTitle, {
|
|
1210
|
+
className: "text-sm font-semibold"
|
|
1211
|
+
}, "Team Purchases"), /* @__PURE__ */ React3.createElement(CardDescription, {
|
|
1212
|
+
className: "text-[11px]"
|
|
1213
|
+
}, "Multi-seat deals \xB7 sorted by amount"))), /* @__PURE__ */ React3.createElement("span", {
|
|
1214
|
+
className: "rounded-full bg-red-500/10 px-2 py-0.5 text-[10px] font-medium uppercase tracking-wider text-red-600 dark:text-red-400"
|
|
1215
|
+
}, "OH YEAH"))), /* @__PURE__ */ React3.createElement(CardContent, null, /* @__PURE__ */ React3.createElement("div", {
|
|
1216
|
+
className: "overflow-x-auto"
|
|
1217
|
+
}, /* @__PURE__ */ React3.createElement("table", {
|
|
1218
|
+
className: "w-full"
|
|
1219
|
+
}, /* @__PURE__ */ React3.createElement("thead", null, /* @__PURE__ */ React3.createElement("tr", {
|
|
1220
|
+
className: "text-muted-foreground border-border/50 border-b text-left text-[11px] uppercase tracking-wider"
|
|
1221
|
+
}, /* @__PURE__ */ React3.createElement("th", {
|
|
1222
|
+
className: "pb-2.5 pr-4 text-right font-medium"
|
|
1223
|
+
}, "Amount"), /* @__PURE__ */ React3.createElement("th", {
|
|
1224
|
+
className: "pb-2.5 pr-4 text-right font-medium"
|
|
1225
|
+
}, "Seats"), /* @__PURE__ */ React3.createElement("th", {
|
|
1226
|
+
className: "pb-2.5 pr-4 font-medium"
|
|
1227
|
+
}, "Product"), /* @__PURE__ */ React3.createElement("th", {
|
|
1228
|
+
className: "hidden pb-2.5 pr-4 font-medium sm:table-cell"
|
|
1229
|
+
}, "Buyer"), /* @__PURE__ */ React3.createElement("th", {
|
|
1230
|
+
className: "hidden pb-2.5 pr-4 font-medium sm:table-cell"
|
|
1231
|
+
}, "Country"), /* @__PURE__ */ React3.createElement("th", {
|
|
1232
|
+
className: "pb-2.5 pr-4 font-medium"
|
|
1233
|
+
}, "When"))), /* @__PURE__ */ React3.createElement("tbody", {
|
|
1234
|
+
className: "divide-border/30 divide-y"
|
|
1235
|
+
}, data.recentPurchases.map((p) => /* @__PURE__ */ React3.createElement("tr", {
|
|
1236
|
+
key: p.id
|
|
1237
|
+
}, /* @__PURE__ */ React3.createElement("td", {
|
|
1238
|
+
className: `py-2.5 pr-4 text-right text-sm font-semibold tabular-nums ${p.totalAmount > 0 ? "text-emerald-600 dark:text-emerald-400" : "text-muted-foreground"}`
|
|
1239
|
+
}, p.totalAmount > 0 ? fmt$(p.totalAmount) : "Free"), /* @__PURE__ */ React3.createElement("td", {
|
|
1240
|
+
className: "py-2.5 pr-4 text-right text-sm tabular-nums"
|
|
1241
|
+
}, p.seats ?? "\u2014"), /* @__PURE__ */ React3.createElement("td", {
|
|
1242
|
+
className: "max-w-[200px] truncate py-2.5 pr-4 text-sm font-medium"
|
|
1243
|
+
}, p.productName), /* @__PURE__ */ React3.createElement("td", {
|
|
1244
|
+
className: "text-muted-foreground hidden max-w-[150px] truncate py-2.5 pr-4 text-sm sm:table-cell"
|
|
1245
|
+
}, p.userName ?? p.userEmail ?? "\u2014"), /* @__PURE__ */ React3.createElement("td", {
|
|
1246
|
+
className: "text-muted-foreground hidden py-2.5 pr-4 text-sm sm:table-cell"
|
|
1247
|
+
}, p.country ? `${countryFlag2(p.country)} ${p.country}` : "\u2014"), /* @__PURE__ */ React3.createElement("td", {
|
|
1248
|
+
className: "text-muted-foreground whitespace-nowrap py-2.5 pr-4 text-sm"
|
|
1249
|
+
}, fmtAgo(p.createdAt)))))))))));
|
|
1250
|
+
}
|
|
1251
|
+
__name(OmnibusDashboard, "OmnibusDashboard");
|
|
1252
|
+
export {
|
|
1253
|
+
CountryChart,
|
|
1254
|
+
OmnibusDashboard,
|
|
1255
|
+
RevenueChart,
|
|
1256
|
+
useChartColors
|
|
1257
|
+
};
|
|
1258
|
+
//# sourceMappingURL=index.js.map
|