@strapi-community/plugin-better-auth-dashboard 1.0.0-beta.1 → 1.0.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.
Files changed (46) hide show
  1. package/README.md +0 -3
  2. package/dist/admin/Avatar-CdT7eBCh.js +39 -0
  3. package/dist/admin/Avatar-zlMTAZh3.mjs +40 -0
  4. package/dist/admin/{Root-2FMy-qQg.mjs → Root-CgVBJuoM.mjs} +12 -4
  5. package/dist/admin/{Root-DDi0mtve.js → Root-CsiOnzlB.js} +12 -4
  6. package/dist/admin/cs-C_jSgeON.js +416 -0
  7. package/dist/admin/cs-wtnknvBC.mjs +416 -0
  8. package/dist/admin/de-Cg5HyoCY.js +416 -0
  9. package/dist/admin/de-DnnVdUxk.mjs +416 -0
  10. package/dist/admin/en-B3utEgMT.mjs +416 -0
  11. package/dist/admin/en-Cz1IJEvp.js +416 -0
  12. package/dist/admin/es-CGA0Tslt.js +416 -0
  13. package/dist/admin/es-RoCEriJL.mjs +416 -0
  14. package/dist/admin/fr-Ca74t21t.js +416 -0
  15. package/dist/admin/fr-CtmuSfiQ.mjs +416 -0
  16. package/dist/admin/{index-BHfYu0uA.js → index-BCBu-0nr.js} +65 -10
  17. package/dist/admin/{index-D9CiI8Yr.mjs → index-BI2WIsds.mjs} +73 -18
  18. package/dist/admin/index-CPSoAbZt.js +2321 -0
  19. package/dist/admin/index-D9ttdrT3.js +2716 -0
  20. package/dist/admin/index-DQdn5s8q.js +1184 -0
  21. package/dist/admin/index-DvfwvCj0.mjs +1182 -0
  22. package/dist/admin/index-VyHXoQ6p.mjs +2321 -0
  23. package/dist/admin/index-gFqHlMLP.mjs +2716 -0
  24. package/dist/admin/index.js +1 -1
  25. package/dist/admin/index.mjs +1 -1
  26. package/dist/admin/it-BAB9hDdp.mjs +416 -0
  27. package/dist/admin/it-dYT2_ERV.js +416 -0
  28. package/dist/admin/nl-CVqSbUU9.js +416 -0
  29. package/dist/admin/nl-CYv26PHv.mjs +416 -0
  30. package/dist/admin/pl-Bc0cS2mD.mjs +416 -0
  31. package/dist/admin/pl-HYF4BxRv.js +416 -0
  32. package/dist/admin/pt-DDUGyLtb.js +416 -0
  33. package/dist/admin/pt-DZyyK3EG.mjs +416 -0
  34. package/dist/admin/{useModelSchema-CqOTI7P1.js → useModelSchema-BogQah8f.js} +309 -287
  35. package/dist/admin/{useModelSchema-B2tmzlwQ.mjs → useModelSchema-C6bxNjW4.mjs} +320 -298
  36. package/dist/server/index.js +15 -15
  37. package/dist/server/index.mjs +15 -15
  38. package/package.json +4 -4
  39. package/dist/admin/Avatar-Cvk89aMx.js +0 -67
  40. package/dist/admin/Avatar-DNczJU0i.mjs +0 -66
  41. package/dist/admin/index-BQi_BUi6.mjs +0 -1968
  42. package/dist/admin/index-BV-4M_Fp.js +0 -1970
  43. package/dist/admin/index-CANDPUGD.mjs +0 -1215
  44. package/dist/admin/index-CqR8WpOa.js +0 -1217
  45. package/dist/admin/index-D6ktNZcj.js +0 -1685
  46. package/dist/admin/index-DszX3ibA.mjs +0 -1683
@@ -1,1217 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
- const jsxRuntime = require("react/jsx-runtime");
4
- const designSystem = require("@strapi/design-system");
5
- const admin = require("@strapi/strapi/admin");
6
- const react = require("react");
7
- const reactQuery = require("react-query");
8
- const styled = require("styled-components");
9
- const index = require("./index-BHfYu0uA.js");
10
- const Avatar = require("./Avatar-Cvk89aMx.js");
11
- const _interopDefault = (e) => e && e.__esModule ? e : { default: e };
12
- const styled__default = /* @__PURE__ */ _interopDefault(styled);
13
- const T = {
14
- bg: "#f6f6f9",
15
- bgCard: "#ffffff",
16
- border: "#eaeaef",
17
- borderHover: "rgba(73,69,255,0.35)",
18
- accent: "#4945ff",
19
- green: "#5cb176",
20
- greenDim: "rgba(92,177,118,0.12)",
21
- amber: "#d9822f",
22
- red: "#d02b20",
23
- redDim: "rgba(208,43,32,0.1)",
24
- purple: "#8460b8",
25
- textPrimary: "#32324d",
26
- textSecondary: "#666687",
27
- textMuted: "#b8b8c7",
28
- mono: `'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace`
29
- };
30
- const fadeUp = styled.keyframes`
31
- from { opacity: 0; transform: translateY(10px); }
32
- to { opacity: 1; transform: translateY(0); }
33
- `;
34
- const Wrap = styled__default.default.div`
35
- padding: 28px 32px 48px;
36
- background: ${T.bg};
37
- min-height: 100%;
38
- display: flex;
39
- flex-direction: column;
40
- gap: 22px;
41
- `;
42
- const SectionDivider = styled__default.default.div`
43
- display: flex;
44
- align-items: center;
45
- gap: 10px;
46
- margin-bottom: -6px;
47
- `;
48
- const DivLabel = styled__default.default.span`
49
- font-size: 9px;
50
- font-weight: 700;
51
- letter-spacing: 0.14em;
52
- text-transform: uppercase;
53
- color: ${T.textMuted};
54
- white-space: nowrap;
55
- `;
56
- const DivLine = styled__default.default.div`
57
- flex: 1;
58
- height: 1px;
59
- background: ${T.border};
60
- `;
61
- const Card = styled__default.default.div`
62
- background: ${T.bgCard};
63
- border: 1px solid ${T.border};
64
- border-radius: 12px;
65
- overflow: hidden;
66
- position: relative;
67
- animation: ${fadeUp} 380ms ease both;
68
- animation-delay: ${(p) => (p.$delay ?? 0) * 55}ms;
69
- transition: border-color 200ms, box-shadow 200ms;
70
- &:hover {
71
- border-color: ${T.borderHover};
72
- box-shadow: 0 0 0 1px ${T.borderHover}, 0 8px 32px rgba(124,109,250,0.07);
73
- }
74
- `;
75
- const PillGroup = styled__default.default.div`
76
- display: flex;
77
- background: rgba(0,0,0,0.03);
78
- border: 1px solid ${T.border};
79
- border-radius: 9px;
80
- padding: 3px;
81
- gap: 2px;
82
- `;
83
- const Pill = styled__default.default.button`
84
- appearance: none;
85
- border: none;
86
- cursor: pointer;
87
- padding: 5px 16px;
88
- border-radius: 6px;
89
- font-size: 11px;
90
- font-weight: 600;
91
- letter-spacing: 0.03em;
92
- transition: background 180ms, color 180ms;
93
- background: ${(p) => p.$active ? T.accent : "transparent"};
94
- color: ${(p) => p.$active ? "#fff" : T.textSecondary};
95
- &:hover {
96
- background: ${(p) => p.$active ? T.accent : "rgba(0,0,0,0.05)"};
97
- color: ${(p) => p.$active ? "#fff" : T.textPrimary};
98
- }
99
- `;
100
- const StatGrid = styled__default.default.div`
101
- display: grid;
102
- grid-template-columns: repeat(5, 1fr);
103
- gap: 10px;
104
- @media (max-width: 1200px) { grid-template-columns: repeat(3, 1fr); }
105
- `;
106
- const StatCard = styled__default.default(Card)`
107
- padding: 18px 20px 0;
108
- &::after {
109
- content: '';
110
- position: absolute;
111
- top: 0; left: 0; right: 0;
112
- height: 2px;
113
- background: ${(p) => p.$accent};
114
- opacity: 0.75;
115
- }
116
- `;
117
- const StatLabel = styled__default.default.div`
118
- font-size: 9px;
119
- font-weight: 700;
120
- letter-spacing: 0.1em;
121
- text-transform: uppercase;
122
- color: ${T.textMuted};
123
- margin-bottom: 10px;
124
- `;
125
- const StatValue = styled__default.default.div`
126
- font-size: 30px;
127
- font-weight: 800;
128
- color: ${T.textPrimary};
129
- line-height: 1;
130
- letter-spacing: -0.045em;
131
- font-variant-numeric: tabular-nums;
132
- margin-bottom: 10px;
133
- `;
134
- const TrendBadge = styled__default.default.span`
135
- display: inline-flex;
136
- align-items: center;
137
- gap: 3px;
138
- padding: 2px 8px;
139
- border-radius: 20px;
140
- font-size: 9px;
141
- font-weight: 700;
142
- letter-spacing: 0.03em;
143
- background: ${(p) => p.$pos ? T.greenDim : T.redDim};
144
- color: ${(p) => p.$pos ? T.green : T.red};
145
- margin-bottom: 12px;
146
- `;
147
- const SparkWrap = styled__default.default.div`
148
- height: 44px;
149
- margin: 0 -20px;
150
- `;
151
- const ChartCard = styled__default.default(Card)`
152
- padding: 20px 20px 12px;
153
- display: flex;
154
- flex-direction: column;
155
- gap: 10px;
156
- `;
157
- const ChartHeader = styled__default.default.div`
158
- display: flex;
159
- align-items: center;
160
- justify-content: space-between;
161
- gap: 12px;
162
- flex-shrink: 0;
163
- `;
164
- const ChartTitle = styled__default.default.div`
165
- font-size: 12px;
166
- font-weight: 700;
167
- color: ${T.textPrimary};
168
- letter-spacing: 0.01em;
169
- `;
170
- const SeriesRow = styled__default.default.div`
171
- display: flex;
172
- align-items: center;
173
- gap: 6px;
174
- `;
175
- const SeriesBtn = styled__default.default.button`
176
- appearance: none;
177
- border: 1px solid ${(p) => p.$on ? `${p.$c}55` : T.border};
178
- background: ${(p) => p.$on ? `${p.$c}18` : "transparent"};
179
- color: ${(p) => p.$on ? p.$c : T.textMuted};
180
- font-size: 10px;
181
- font-weight: 600;
182
- padding: 3px 10px 3px 8px;
183
- border-radius: 6px;
184
- cursor: pointer;
185
- display: flex;
186
- align-items: center;
187
- gap: 5px;
188
- transition: all 160ms;
189
- &:hover {
190
- border-color: ${(p) => `${p.$c}88`};
191
- color: ${(p) => p.$c};
192
- background: ${(p) => `${p.$c}22`};
193
- }
194
- `;
195
- const SDot = styled__default.default.span`
196
- width: 6px; height: 6px;
197
- border-radius: 50%;
198
- background: ${(p) => p.$c};
199
- display: inline-block;
200
- flex-shrink: 0;
201
- `;
202
- const HoverInfo = styled__default.default.div`
203
- font-size: 11px;
204
- color: ${T.textSecondary};
205
- font-variant-numeric: tabular-nums;
206
- font-family: ${T.mono};
207
- min-height: 15px;
208
- `;
209
- const TwoPanel = styled__default.default.div`
210
- display: grid;
211
- grid-template-columns: ${(p) => p.$ratio ?? "1fr 288px"};
212
- gap: 10px;
213
- align-items: stretch;
214
- @media (max-width: 900px) { grid-template-columns: 1fr; }
215
- `;
216
- const FeedCard = styled__default.default(Card)`
217
- display: flex;
218
- flex-direction: column;
219
- overflow: hidden;
220
- `;
221
- const FeedHead = styled__default.default.div`
222
- padding: 13px 16px 9px;
223
- border-bottom: 1px solid ${T.border};
224
- font-size: 11px;
225
- font-weight: 700;
226
- color: ${T.textPrimary};
227
- flex-shrink: 0;
228
- display: flex;
229
- justify-content: space-between;
230
- align-items: center;
231
- `;
232
- const FeedSelect = styled__default.default.select`
233
- appearance: none;
234
- border: 1px solid ${T.border};
235
- border-radius: 6px;
236
- background: ${T.bgCard};
237
- color: ${T.textSecondary};
238
- font-size: 10px;
239
- font-weight: 600;
240
- padding: 3px 22px 3px 8px;
241
- cursor: pointer;
242
- outline: none;
243
- background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'%3E%3Cpath d='M0 0l5 6 5-6z' fill='%23666687'/%3E%3C/svg%3E");
244
- background-repeat: no-repeat;
245
- background-position: right 7px center;
246
- transition: border-color 160ms;
247
- &:hover, &:focus { border-color: ${T.accent}; color: ${T.textPrimary}; }
248
- `;
249
- const FeedScroll = styled__default.default.div`
250
- flex: 1;
251
- overflow-y: auto;
252
- min-height: 0;
253
- &::-webkit-scrollbar { width: 4px; }
254
- &::-webkit-scrollbar-thumb { background: ${T.textMuted}; border-radius: 2px; }
255
- `;
256
- const FeedItem = styled__default.default.div`
257
- padding: 9px 14px;
258
- border-bottom: 1px solid #f0f0f5;
259
- display: flex;
260
- flex-direction: column;
261
- gap: 3px;
262
- transition: background 130ms;
263
- &:hover { background: #fafaff; }
264
- &:last-child { border-bottom: none; }
265
- `;
266
- const FeedTop = styled__default.default.div`
267
- display: flex;
268
- align-items: center;
269
- justify-content: space-between;
270
- gap: 6px;
271
- `;
272
- const FeedName = styled__default.default.span`
273
- font-size: 11px;
274
- font-weight: 600;
275
- color: ${T.textPrimary};
276
- overflow: hidden;
277
- text-overflow: ellipsis;
278
- white-space: nowrap;
279
- `;
280
- const FeedEmail = styled__default.default.span`
281
- font-size: 10px;
282
- color: ${T.textSecondary};
283
- overflow: hidden;
284
- text-overflow: ellipsis;
285
- white-space: nowrap;
286
- display: block;
287
- `;
288
- const FeedMeta = styled__default.default.span`
289
- font-size: 9px;
290
- color: ${T.textMuted};
291
- white-space: nowrap;
292
- font-variant-numeric: tabular-nums;
293
- `;
294
- const RtnCard = styled__default.default(Card)`
295
- padding: 20px;
296
- display: flex;
297
- flex-direction: column;
298
- gap: 12px;
299
- `;
300
- const RtnRow = styled__default.default.div`
301
- display: flex;
302
- align-items: center;
303
- gap: 10px;
304
- padding: 4px 6px;
305
- border-radius: 7px;
306
- transition: background 130ms;
307
- background: ${(p) => p.$hov ? "#f0f0f6" : "transparent"};
308
- cursor: default;
309
- `;
310
- const RtnLabel = styled__default.default.span`
311
- font-size: 10px;
312
- color: ${T.textSecondary};
313
- min-width: 80px;
314
- text-align: right;
315
- white-space: nowrap;
316
- `;
317
- const RtnSize = styled__default.default.span`
318
- font-size: 9px;
319
- color: ${T.textMuted};
320
- min-width: 52px;
321
- text-align: right;
322
- white-space: nowrap;
323
- font-variant-numeric: tabular-nums;
324
- `;
325
- const RtnTrack = styled__default.default.div`
326
- flex: 1;
327
- height: 10px;
328
- background: #eaeaef;
329
- border-radius: 4px;
330
- overflow: hidden;
331
- `;
332
- const RtnBar = styled__default.default.div`
333
- width: ${(p) => Math.max(p.$w, 0.5)}%;
334
- height: 100%;
335
- background: hsl(${(p) => p.$hue}, 60%, 50%);
336
- border-radius: 4px;
337
- transition: width 0.55s cubic-bezier(0.25, 0.46, 0.45, 0.94);
338
- `;
339
- const RtnPct = styled__default.default.span`
340
- font-size: 10px;
341
- font-weight: 700;
342
- color: hsl(${(p) => p.$hue}, 60%, 60%);
343
- min-width: 38px;
344
- text-align: right;
345
- font-variant-numeric: tabular-nums;
346
- font-family: ${T.mono};
347
- `;
348
- const RtnTip = styled__default.default.div`
349
- font-size: 10px;
350
- color: ${T.textSecondary};
351
- min-height: 14px;
352
- font-variant-numeric: tabular-nums;
353
- padding: 0 6px;
354
- `;
355
- const RingGrid = styled__default.default.div`
356
- display: flex;
357
- flex-direction: column;
358
- gap: 10px;
359
- `;
360
- const RingCard = styled__default.default(Card)`
361
- padding: 18px 20px;
362
- display: flex;
363
- align-items: center;
364
- gap: 16px;
365
- flex: 1;
366
- `;
367
- const RingInfo = styled__default.default.div`
368
- display: flex;
369
- flex-direction: column;
370
- gap: 4px;
371
- `;
372
- const RingVal = styled__default.default.div`
373
- font-size: 24px;
374
- font-weight: 800;
375
- color: ${T.textPrimary};
376
- letter-spacing: -0.04em;
377
- font-variant-numeric: tabular-nums;
378
- line-height: 1;
379
- `;
380
- const RingLabel = styled__default.default.div`
381
- font-size: 9px;
382
- font-weight: 700;
383
- text-transform: uppercase;
384
- letter-spacing: 0.1em;
385
- color: ${T.textMuted};
386
- `;
387
- const Empty = styled__default.default.div`
388
- display: flex;
389
- flex-direction: column;
390
- align-items: center;
391
- justify-content: center;
392
- padding: 32px;
393
- color: ${T.textMuted};
394
- font-size: 12px;
395
- gap: 8px;
396
- `;
397
- function useCountUp(target, duration = 850) {
398
- const [count, setCount] = react.useState(0);
399
- const prevRef = react.useRef(0);
400
- react.useEffect(() => {
401
- let raf;
402
- let start = 0;
403
- const from = prevRef.current;
404
- const diff = target - from;
405
- const step = (ts) => {
406
- if (!start) start = ts;
407
- const progress = Math.min((ts - start) / duration, 1);
408
- const eased = 1 - (1 - progress) ** 3;
409
- setCount(Math.round(from + eased * diff));
410
- if (progress < 1) raf = requestAnimationFrame(step);
411
- else prevRef.current = target;
412
- };
413
- raf = requestAnimationFrame(step);
414
- return () => cancelAnimationFrame(raf);
415
- }, [target, duration]);
416
- return count;
417
- }
418
- function Sparkline({
419
- data,
420
- color,
421
- id
422
- }) {
423
- const W = 300;
424
- const H = 44;
425
- if (data.length < 2) return /* @__PURE__ */ jsxRuntime.jsx("svg", { width: "100%", height: H });
426
- const min = Math.min(...data);
427
- const max = Math.max(...data, min + 1);
428
- const pts = data.map((v, i) => ({
429
- x: i / (data.length - 1) * W,
430
- y: H - 3 - (v - min) / (max - min) * (H - 8)
431
- }));
432
- const line = pts.reduce((a, p, i) => {
433
- if (i === 0) return `M ${p.x} ${p.y}`;
434
- const pr = pts[i - 1];
435
- const cx = (pr.x + p.x) / 2;
436
- return `${a} C ${cx} ${pr.y} ${cx} ${p.y} ${p.x} ${p.y}`;
437
- }, "");
438
- const area = `${line} L ${pts.at(-1).x} ${H} L 0 ${H} Z`;
439
- const gid = `spk-${id}`;
440
- return /* @__PURE__ */ jsxRuntime.jsxs(
441
- "svg",
442
- {
443
- width: "100%",
444
- height: H,
445
- viewBox: `0 0 ${W} ${H}`,
446
- preserveAspectRatio: "none",
447
- "aria-hidden": true,
448
- children: [
449
- /* @__PURE__ */ jsxRuntime.jsx("defs", { children: /* @__PURE__ */ jsxRuntime.jsxs("linearGradient", { id: gid, x1: "0", y1: "0", x2: "0", y2: "1", children: [
450
- /* @__PURE__ */ jsxRuntime.jsx("stop", { offset: "0%", stopColor: color, stopOpacity: "0.32" }),
451
- /* @__PURE__ */ jsxRuntime.jsx("stop", { offset: "100%", stopColor: color, stopOpacity: "0" })
452
- ] }) }),
453
- /* @__PURE__ */ jsxRuntime.jsx("path", { d: area, fill: `url(#${gid})` }),
454
- /* @__PURE__ */ jsxRuntime.jsx(
455
- "path",
456
- {
457
- d: line,
458
- stroke: color,
459
- strokeWidth: "1.6",
460
- fill: "none",
461
- strokeLinecap: "round"
462
- }
463
- )
464
- ]
465
- }
466
- );
467
- }
468
- const ALL_SERIES = [
469
- { key: "totalUsers", color: T.accent, label: "Total" },
470
- { key: "newUsers", color: T.green, label: "New" },
471
- { key: "activeUsers", color: T.amber, label: "Active" }
472
- ];
473
- function GrowthChart({
474
- data,
475
- hovered,
476
- onHover,
477
- activeSeries
478
- }) {
479
- const W = 600;
480
- const H = 200;
481
- const PL = 40;
482
- const PR = 12;
483
- const PT = 12;
484
- const PB = 28;
485
- const CW = W - PL - PR;
486
- const CH = H - PT - PB;
487
- if (!data.length) {
488
- return /* @__PURE__ */ jsxRuntime.jsxs(Empty, { children: [
489
- /* @__PURE__ */ jsxRuntime.jsx("div", { style: { fontSize: 24 }, children: "📊" }),
490
- /* @__PURE__ */ jsxRuntime.jsx("div", { children: "No growth data for this period" })
491
- ] });
492
- }
493
- const active = ALL_SERIES.filter((s) => activeSeries.has(s.key));
494
- const allVals = data.flatMap((d) => active.map((s) => d[s.key]));
495
- const maxV = Math.max(...allVals, 10);
496
- const yMax = Math.ceil(maxV * 1.15);
497
- const xp = (i) => PL + i / Math.max(data.length - 1, 1) * CW;
498
- const yp = (v) => PT + (1 - v / yMax) * CH;
499
- const smooth = (vals) => {
500
- const pts = vals.map((v, i) => ({ x: xp(i), y: yp(v) }));
501
- const path = pts.reduce((a, p, i) => {
502
- if (i === 0) return `M ${p.x} ${p.y}`;
503
- const pr = pts[i - 1];
504
- const cx = (pr.x + p.x) / 2;
505
- return `${a} C ${cx} ${pr.y} ${cx} ${p.y} ${p.x} ${p.y}`;
506
- }, "");
507
- const area = `${path} L ${pts.at(-1).x} ${PT + CH} L ${pts[0].x} ${PT + CH} Z`;
508
- return { pts, path, area };
509
- };
510
- const lines = active.map((s) => ({
511
- ...s,
512
- ...smooth(data.map((d) => d[s.key]))
513
- }));
514
- const yTicks = Array.from({ length: 5 }, (_, i) => {
515
- const v = yMax * (4 - i) / 4;
516
- return { v: Math.round(v), y: yp(v) };
517
- });
518
- const step = Math.max(1, Math.ceil(data.length / 8));
519
- const segW = CW / Math.max(data.length - 1, 1);
520
- return /* @__PURE__ */ jsxRuntime.jsxs(
521
- "svg",
522
- {
523
- width: "100%",
524
- viewBox: `0 0 ${W} ${H}`,
525
- preserveAspectRatio: "xMidYMid meet",
526
- style: { display: "block" },
527
- "aria-label": "User growth chart",
528
- role: "img",
529
- children: [
530
- /* @__PURE__ */ jsxRuntime.jsx("defs", { children: lines.map((s) => /* @__PURE__ */ jsxRuntime.jsxs(
531
- "linearGradient",
532
- {
533
- id: `ag-${s.key}`,
534
- x1: "0",
535
- y1: "0",
536
- x2: "0",
537
- y2: "1",
538
- children: [
539
- /* @__PURE__ */ jsxRuntime.jsx("stop", { offset: "0%", stopColor: s.color, stopOpacity: "0.18" }),
540
- /* @__PURE__ */ jsxRuntime.jsx("stop", { offset: "100%", stopColor: s.color, stopOpacity: "0" })
541
- ]
542
- },
543
- s.key
544
- )) }),
545
- yTicks.map((t) => /* @__PURE__ */ jsxRuntime.jsxs("g", { children: [
546
- /* @__PURE__ */ jsxRuntime.jsx(
547
- "line",
548
- {
549
- x1: PL,
550
- y1: t.y,
551
- x2: W - PR,
552
- y2: t.y,
553
- stroke: "#eaeaef",
554
- strokeWidth: "1"
555
- }
556
- ),
557
- /* @__PURE__ */ jsxRuntime.jsx(
558
- "text",
559
- {
560
- x: PL - 6,
561
- y: t.y + 4,
562
- textAnchor: "end",
563
- fill: T.textMuted,
564
- fontSize: "9",
565
- children: t.v >= 1e3 ? `${(t.v / 1e3).toFixed(1)}k` : t.v
566
- }
567
- )
568
- ] }, t.v)),
569
- /* @__PURE__ */ jsxRuntime.jsx(
570
- "line",
571
- {
572
- x1: PL,
573
- y1: PT + CH,
574
- x2: W - PR,
575
- y2: PT + CH,
576
- stroke: "#eaeaef",
577
- strokeWidth: "1"
578
- }
579
- ),
580
- lines.map((s) => /* @__PURE__ */ jsxRuntime.jsx("path", { d: s.area, fill: `url(#ag-${s.key})` }, `area-${s.key}`)),
581
- lines.map((s) => /* @__PURE__ */ jsxRuntime.jsx(
582
- "path",
583
- {
584
- d: s.path,
585
- stroke: s.color,
586
- strokeWidth: "2",
587
- fill: "none",
588
- strokeLinecap: "round",
589
- strokeLinejoin: "round"
590
- },
591
- `line-${s.key}`
592
- )),
593
- data.map((d, i) => {
594
- if (i % step !== 0 && i !== data.length - 1) return null;
595
- return /* @__PURE__ */ jsxRuntime.jsx(
596
- "text",
597
- {
598
- x: xp(i),
599
- y: H - 6,
600
- textAnchor: "middle",
601
- fill: T.textMuted,
602
- fontSize: "9",
603
- children: d.label
604
- },
605
- i
606
- );
607
- }),
608
- data.map((d, i) => {
609
- const isHov = hovered === i;
610
- return (
611
- // biome-ignore lint/a11y/noStaticElementInteractions: SVG chart hover
612
- /* @__PURE__ */ jsxRuntime.jsxs(
613
- "g",
614
- {
615
- onMouseEnter: () => onHover(i),
616
- onMouseLeave: () => onHover(null),
617
- style: { cursor: "crosshair" },
618
- children: [
619
- /* @__PURE__ */ jsxRuntime.jsx(
620
- "rect",
621
- {
622
- x: xp(i) - segW / 2,
623
- y: PT,
624
- width: segW,
625
- height: CH,
626
- fill: "transparent"
627
- }
628
- ),
629
- isHov && /* @__PURE__ */ jsxRuntime.jsx(
630
- "line",
631
- {
632
- x1: xp(i),
633
- y1: PT,
634
- x2: xp(i),
635
- y2: PT + CH,
636
- stroke: "rgba(50,50,77,0.2)",
637
- strokeWidth: "1",
638
- strokeDasharray: "3 3"
639
- }
640
- ),
641
- lines.map((s) => /* @__PURE__ */ jsxRuntime.jsx(
642
- "circle",
643
- {
644
- cx: xp(i),
645
- cy: yp(d[s.key]),
646
- r: isHov ? 4 : 2.5,
647
- fill: s.color,
648
- stroke: T.bg,
649
- strokeWidth: isHov ? 2 : 1,
650
- opacity: isHov ? 1 : 0.5
651
- },
652
- s.key
653
- ))
654
- ]
655
- },
656
- i
657
- )
658
- );
659
- })
660
- ]
661
- }
662
- );
663
- }
664
- function ProgressRing({
665
- value,
666
- max,
667
- color,
668
- size = 56
669
- }) {
670
- const r = (size - 9) / 2;
671
- const circ = 2 * Math.PI * r;
672
- const pct = Math.min(value / Math.max(max, 1), 1);
673
- const offset = circ * (1 - pct);
674
- return /* @__PURE__ */ jsxRuntime.jsxs(
675
- "svg",
676
- {
677
- width: size,
678
- height: size,
679
- style: { flexShrink: 0 },
680
- "aria-hidden": true,
681
- children: [
682
- /* @__PURE__ */ jsxRuntime.jsx(
683
- "circle",
684
- {
685
- cx: size / 2,
686
- cy: size / 2,
687
- r,
688
- fill: "none",
689
- stroke: "#e5e7eb",
690
- strokeWidth: "7"
691
- }
692
- ),
693
- /* @__PURE__ */ jsxRuntime.jsx(
694
- "circle",
695
- {
696
- cx: size / 2,
697
- cy: size / 2,
698
- r,
699
- fill: "none",
700
- stroke: color,
701
- strokeWidth: "7",
702
- strokeDasharray: `${circ} ${circ}`,
703
- strokeDashoffset: offset,
704
- strokeLinecap: "round",
705
- transform: `rotate(-90 ${size / 2} ${size / 2})`,
706
- style: {
707
- transition: "stroke-dashoffset 0.9s cubic-bezier(0.4,0,0.2,1)"
708
- }
709
- }
710
- )
711
- ]
712
- }
713
- );
714
- }
715
- function relTime(date) {
716
- const diff = Date.now() - new Date(date).getTime();
717
- const mins = Math.floor(diff / 6e4);
718
- if (mins < 1) return "just now";
719
- if (mins < 60) return `${mins}m`;
720
- const hrs = Math.floor(mins / 60);
721
- if (hrs < 24) return `${hrs}h`;
722
- return `${Math.floor(hrs / 24)}d`;
723
- }
724
- function rateHue(r) {
725
- return r >= 70 ? 142 : r >= 40 ? 38 : 4;
726
- }
727
- function StatItem({
728
- id,
729
- label,
730
- value,
731
- pct,
732
- sparkline,
733
- color,
734
- delay = 0
735
- }) {
736
- const animated = useCountUp(value);
737
- const isPos = pct === void 0 || pct >= 0;
738
- return /* @__PURE__ */ jsxRuntime.jsxs(StatCard, { $delay: delay, $accent: color, children: [
739
- /* @__PURE__ */ jsxRuntime.jsx(StatLabel, { children: label }),
740
- /* @__PURE__ */ jsxRuntime.jsx(StatValue, { children: animated.toLocaleString() }),
741
- pct !== void 0 && /* @__PURE__ */ jsxRuntime.jsxs(TrendBadge, { $pos: isPos, children: [
742
- isPos ? "↑" : "↓",
743
- " ",
744
- Math.abs(pct).toFixed(1),
745
- "%"
746
- ] }),
747
- sparkline && sparkline.length > 1 && /* @__PURE__ */ jsxRuntime.jsx(SparkWrap, { children: /* @__PURE__ */ jsxRuntime.jsx(Sparkline, { data: sparkline, color, id }) })
748
- ] });
749
- }
750
- function OverviewPage() {
751
- const { get } = admin.useFetchClient();
752
- const [period, setPeriod] = react.useState(
753
- "weekly"
754
- );
755
- const [feedMode, setFeedMode] = react.useState("signups");
756
- const [hovIdx, setHovIdx] = react.useState(null);
757
- const [rtnHov, setRtnHov] = react.useState(null);
758
- const [activeSeries, setActiveSeries] = react.useState(
759
- () => /* @__PURE__ */ new Set(["totalUsers", "newUsers", "activeUsers"])
760
- );
761
- const toggleSeries = (key) => {
762
- setActiveSeries((prev) => {
763
- const next = new Set(prev);
764
- if (next.has(key) && next.size > 1) next.delete(key);
765
- else next.add(key);
766
- return next;
767
- });
768
- };
769
- const statsQuery = reactQuery.useQuery({
770
- queryKey: ["dash-user-stats"],
771
- queryFn: async () => {
772
- const r = await index.client.dash.userStats();
773
- if (r.error) throw new Error(r.error.message ?? "Failed");
774
- return r.data;
775
- }
776
- });
777
- const graphQuery = reactQuery.useQuery({
778
- queryKey: ["dash-user-graph", period],
779
- queryFn: async () => {
780
- const r = await index.client.dash.userGraphData({ query: { period } });
781
- if (r.error) throw new Error(r.error.message ?? "Failed");
782
- return r.data;
783
- }
784
- });
785
- const retentionQuery = reactQuery.useQuery({
786
- queryKey: ["dash-user-retention", period],
787
- queryFn: async () => {
788
- const r = await index.client.dash.userRetentionData({ query: { period } });
789
- if (r.error) throw new Error(r.error.message ?? "Failed");
790
- return r.data;
791
- }
792
- });
793
- const usersQuery = reactQuery.useQuery({
794
- queryKey: ["dash-recent-users"],
795
- queryFn: async () => {
796
- const r = await index.client.dash.listUsers({
797
- query: { limit: 12, offset: 0, sortBy: "createdAt", sortOrder: "desc" }
798
- });
799
- if (r.error) throw new Error(r.error.message ?? "Failed");
800
- return r.data;
801
- }
802
- });
803
- const sessionsQuery = reactQuery.useQuery({
804
- queryKey: ["dash-recent-sessions"],
805
- queryFn: async () => {
806
- const { data } = await get(
807
- "/better-auth-dashboard/db?uid=plugin::better-auth.session&pagination[pageSize]=12&sort[0]=updatedAt:desc"
808
- );
809
- return data.results ?? [];
810
- },
811
- refetchInterval: 3e4
812
- });
813
- const orgsQuery = reactQuery.useQuery({
814
- queryKey: ["dash-orgs-count"],
815
- queryFn: async () => {
816
- const r = await index.client.dash.listOrganizations({
817
- query: { page: 1, limit: 1 }
818
- });
819
- if (r.error) throw new Error(r.error.message ?? "Failed");
820
- return r.data;
821
- }
822
- });
823
- const sessionsRaw = sessionsQuery.data ?? [];
824
- const activeUserIds = [];
825
- const lastActiveByUserId = /* @__PURE__ */ new Map();
826
- for (const s of sessionsRaw) {
827
- const uid = String(s.userId);
828
- if (!lastActiveByUserId.has(uid)) {
829
- lastActiveByUserId.set(uid, s.updatedAt ?? s.createdAt);
830
- activeUserIds.push(uid);
831
- }
832
- }
833
- const activeUsersQuery = reactQuery.useQuery({
834
- queryKey: ["dash-active-users", activeUserIds],
835
- queryFn: async () => {
836
- if (activeUserIds.length === 0) return [];
837
- const params = new URLSearchParams({ uid: "plugin::better-auth.user" });
838
- for (let i = 0; i < activeUserIds.length; i++) {
839
- params.set(`filters[id][$in][${i}]`, activeUserIds[i]);
840
- }
841
- params.set("pagination[pageSize]", "12");
842
- const { data } = await get(
843
- `/better-auth-dashboard/db?${params}`
844
- );
845
- return data.results ?? [];
846
- },
847
- enabled: feedMode === "active" && activeUserIds.length > 0
848
- });
849
- const chartRef = react.useRef(null);
850
- const [chartHeight, setChartHeight] = react.useState(0);
851
- react.useEffect(() => {
852
- const el = chartRef.current;
853
- if (!el) return;
854
- const ro = new ResizeObserver((entries) => {
855
- for (const entry of entries) setChartHeight(entry.contentRect.height);
856
- });
857
- ro.observe(el);
858
- return () => ro.disconnect();
859
- }, []);
860
- if (statsQuery.isLoading) {
861
- return /* @__PURE__ */ jsxRuntime.jsx(
862
- designSystem.Flex,
863
- {
864
- justifyContent: "center",
865
- alignItems: "center",
866
- padding: 12,
867
- style: { background: T.bg, minHeight: "100%" },
868
- children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Loader, { children: "Loading…" })
869
- }
870
- );
871
- }
872
- const stats = statsQuery.data;
873
- if (!stats) return null;
874
- const graphData = graphQuery.data?.data ?? [];
875
- const rtnData = retentionQuery.data?.data ?? [];
876
- const users = usersQuery.data?.users ?? [];
877
- const orgCount = orgsQuery.data?.total ?? 0;
878
- const activeUserMap = /* @__PURE__ */ new Map();
879
- for (const u of activeUsersQuery.data ?? []) {
880
- activeUserMap.set(String(u.id), u);
881
- }
882
- const sortedActiveUsers = activeUserIds.map((uid) => activeUserMap.get(uid)).filter((u) => u !== void 0);
883
- const totalSpark = graphData.map((d) => d.totalUsers);
884
- const newSpark = graphData.map((d) => d.newUsers);
885
- const activeSpark = graphData.map((d) => d.activeUsers);
886
- const hovRow = hovIdx !== null ? graphData[hovIdx] : null;
887
- const rtnHovRow = rtnHov !== null ? rtnData[rtnHov] : null;
888
- const activeMax = Math.max(
889
- stats.activeUsers.daily.active ?? 0,
890
- stats.activeUsers.weekly.active ?? 0,
891
- stats.activeUsers.monthly.active ?? 0,
892
- 1
893
- );
894
- return /* @__PURE__ */ jsxRuntime.jsxs(Wrap, { "data-testid": "overview-page", children: [
895
- /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { justifyContent: "space-between", alignItems: "flex-end", children: [
896
- /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
897
- /* @__PURE__ */ jsxRuntime.jsx(
898
- "div",
899
- {
900
- style: {
901
- fontSize: 22,
902
- fontWeight: 800,
903
- color: T.textPrimary,
904
- letterSpacing: "-0.03em"
905
- },
906
- children: "Overview"
907
- }
908
- ),
909
- /* @__PURE__ */ jsxRuntime.jsx("div", { style: { fontSize: 11, color: T.textSecondary, marginTop: 4 }, children: (/* @__PURE__ */ new Date()).toLocaleDateString(void 0, {
910
- weekday: "long",
911
- year: "numeric",
912
- month: "long",
913
- day: "numeric"
914
- }) })
915
- ] }),
916
- /* @__PURE__ */ jsxRuntime.jsx(PillGroup, { children: ["daily", "weekly", "monthly"].map((p) => /* @__PURE__ */ jsxRuntime.jsx(Pill, { $active: period === p, onClick: () => setPeriod(p), children: p[0].toUpperCase() + p.slice(1) }, p)) })
917
- ] }),
918
- /* @__PURE__ */ jsxRuntime.jsxs(SectionDivider, { children: [
919
- /* @__PURE__ */ jsxRuntime.jsx(DivLabel, { children: "Metrics" }),
920
- /* @__PURE__ */ jsxRuntime.jsx(DivLine, {})
921
- ] }),
922
- /* @__PURE__ */ jsxRuntime.jsxs(StatGrid, { children: [
923
- /* @__PURE__ */ jsxRuntime.jsx(
924
- StatItem,
925
- {
926
- id: "total",
927
- label: "Total Users",
928
- value: stats.total ?? 0,
929
- sparkline: totalSpark,
930
- color: T.accent,
931
- delay: 0
932
- }
933
- ),
934
- /* @__PURE__ */ jsxRuntime.jsx(
935
- StatItem,
936
- {
937
- id: "d-sig",
938
- label: "Daily Sign-ups",
939
- value: stats.daily.signUps ?? 0,
940
- pct: stats.daily.percentage ?? void 0,
941
- sparkline: newSpark,
942
- color: T.green,
943
- delay: 1
944
- }
945
- ),
946
- /* @__PURE__ */ jsxRuntime.jsx(
947
- StatItem,
948
- {
949
- id: "w-sig",
950
- label: "Weekly Sign-ups",
951
- value: stats.weekly.signUps ?? 0,
952
- pct: stats.weekly.percentage ?? void 0,
953
- sparkline: newSpark,
954
- color: T.green,
955
- delay: 2
956
- }
957
- ),
958
- /* @__PURE__ */ jsxRuntime.jsx(
959
- StatItem,
960
- {
961
- id: "m-sig",
962
- label: "Monthly Sign-ups",
963
- value: stats.monthly.signUps ?? 0,
964
- pct: stats.monthly.percentage ?? void 0,
965
- sparkline: newSpark,
966
- color: T.green,
967
- delay: 3
968
- }
969
- ),
970
- /* @__PURE__ */ jsxRuntime.jsx(
971
- StatItem,
972
- {
973
- id: "orgs",
974
- label: "Organizations",
975
- value: orgCount,
976
- color: T.purple,
977
- delay: 4
978
- }
979
- )
980
- ] }),
981
- /* @__PURE__ */ jsxRuntime.jsxs(SectionDivider, { children: [
982
- /* @__PURE__ */ jsxRuntime.jsx(DivLabel, { children: "Growth" }),
983
- /* @__PURE__ */ jsxRuntime.jsx(DivLine, {})
984
- ] }),
985
- /* @__PURE__ */ jsxRuntime.jsxs(TwoPanel, { children: [
986
- /* @__PURE__ */ jsxRuntime.jsxs(ChartCard, { ref: chartRef, $delay: 5, children: [
987
- /* @__PURE__ */ jsxRuntime.jsxs(ChartHeader, { children: [
988
- /* @__PURE__ */ jsxRuntime.jsx(ChartTitle, { children: "User Growth" }),
989
- /* @__PURE__ */ jsxRuntime.jsx(SeriesRow, { children: ALL_SERIES.map((s) => /* @__PURE__ */ jsxRuntime.jsxs(
990
- SeriesBtn,
991
- {
992
- $on: activeSeries.has(s.key),
993
- $c: s.color,
994
- onClick: () => toggleSeries(s.key),
995
- children: [
996
- /* @__PURE__ */ jsxRuntime.jsx(SDot, { $c: s.color }),
997
- s.label
998
- ]
999
- },
1000
- s.key
1001
- )) })
1002
- ] }),
1003
- /* @__PURE__ */ jsxRuntime.jsx(HoverInfo, { children: hovRow ? `${hovRow.label} · ${hovRow.totalUsers.toLocaleString()} total · +${hovRow.newUsers.toLocaleString()} new · ${hovRow.activeUsers.toLocaleString()} active` : "Hover the chart to inspect a data point" }),
1004
- graphQuery.isLoading ? /* @__PURE__ */ jsxRuntime.jsx(
1005
- designSystem.Flex,
1006
- {
1007
- justifyContent: "center",
1008
- alignItems: "center",
1009
- style: { flex: 1, minHeight: 170 },
1010
- children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Loader, { children: "Loading…" })
1011
- }
1012
- ) : /* @__PURE__ */ jsxRuntime.jsx(
1013
- GrowthChart,
1014
- {
1015
- data: graphData,
1016
- hovered: hovIdx,
1017
- onHover: setHovIdx,
1018
- activeSeries
1019
- }
1020
- )
1021
- ] }),
1022
- /* @__PURE__ */ jsxRuntime.jsxs(
1023
- FeedCard,
1024
- {
1025
- $delay: 6,
1026
- style: chartHeight > 0 ? { height: chartHeight } : void 0,
1027
- children: [
1028
- /* @__PURE__ */ jsxRuntime.jsxs(FeedHead, { children: [
1029
- /* @__PURE__ */ jsxRuntime.jsx("span", { children: feedMode === "signups" ? "Recent Sign-ups" : "Recently Active" }),
1030
- /* @__PURE__ */ jsxRuntime.jsxs(
1031
- FeedSelect,
1032
- {
1033
- value: feedMode,
1034
- onChange: (e) => setFeedMode(e.target.value),
1035
- children: [
1036
- /* @__PURE__ */ jsxRuntime.jsx("option", { value: "signups", children: "Recent Sign-ups" }),
1037
- /* @__PURE__ */ jsxRuntime.jsx("option", { value: "active", children: "Recently Active" })
1038
- ]
1039
- }
1040
- )
1041
- ] }),
1042
- /* @__PURE__ */ jsxRuntime.jsx(FeedScroll, { children: feedMode === "signups" ? usersQuery.isLoading ? /* @__PURE__ */ jsxRuntime.jsx(designSystem.Flex, { justifyContent: "center", padding: 4, children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Loader, { children: "Loading…" }) }) : users.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx(Empty, { children: "No users yet" }) : users.map((u) => /* @__PURE__ */ jsxRuntime.jsxs(FeedItem, { children: [
1043
- /* @__PURE__ */ jsxRuntime.jsxs(FeedTop, { children: [
1044
- /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { alignItems: "center", gap: 1, style: { minWidth: 0 }, children: [
1045
- /* @__PURE__ */ jsxRuntime.jsx(Avatar.Avatar, { name: u.name, src: u.image, size: 20 }),
1046
- /* @__PURE__ */ jsxRuntime.jsx(FeedName, { title: u.name, children: u.name })
1047
- ] }),
1048
- /* @__PURE__ */ jsxRuntime.jsx(FeedMeta, { children: relTime(u.createdAt) })
1049
- ] }),
1050
- /* @__PURE__ */ jsxRuntime.jsx(FeedEmail, { title: u.email, children: u.email })
1051
- ] }, u.id)) : activeUsersQuery.isLoading || sessionsQuery.isLoading ? /* @__PURE__ */ jsxRuntime.jsx(designSystem.Flex, { justifyContent: "center", padding: 4, children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Loader, { children: "Loading…" }) }) : sortedActiveUsers.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx(Empty, { children: "No recent activity" }) : sortedActiveUsers.map((u) => /* @__PURE__ */ jsxRuntime.jsxs(FeedItem, { children: [
1052
- /* @__PURE__ */ jsxRuntime.jsxs(FeedTop, { children: [
1053
- /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { alignItems: "center", gap: 1, style: { minWidth: 0 }, children: [
1054
- /* @__PURE__ */ jsxRuntime.jsx(
1055
- Avatar.Avatar,
1056
- {
1057
- name: u.name,
1058
- src: u.image ?? void 0,
1059
- size: 20
1060
- }
1061
- ),
1062
- /* @__PURE__ */ jsxRuntime.jsx(FeedName, { title: u.name, children: u.name })
1063
- ] }),
1064
- /* @__PURE__ */ jsxRuntime.jsx(FeedMeta, { children: relTime(
1065
- lastActiveByUserId.get(String(u.id)) ?? u.createdAt
1066
- ) })
1067
- ] }),
1068
- /* @__PURE__ */ jsxRuntime.jsx(FeedEmail, { title: u.email, children: u.email })
1069
- ] }, u.documentId)) })
1070
- ]
1071
- }
1072
- )
1073
- ] }),
1074
- /* @__PURE__ */ jsxRuntime.jsxs(SectionDivider, { children: [
1075
- /* @__PURE__ */ jsxRuntime.jsx(DivLabel, { children: "Retention & Activity" }),
1076
- /* @__PURE__ */ jsxRuntime.jsx(DivLine, {})
1077
- ] }),
1078
- /* @__PURE__ */ jsxRuntime.jsxs(TwoPanel, { children: [
1079
- /* @__PURE__ */ jsxRuntime.jsxs(RtnCard, { $delay: 7, children: [
1080
- /* @__PURE__ */ jsxRuntime.jsxs(ChartHeader, { children: [
1081
- /* @__PURE__ */ jsxRuntime.jsx(ChartTitle, { children: "Cohort Retention" }),
1082
- /* @__PURE__ */ jsxRuntime.jsx(designSystem.Flex, { alignItems: "center", gap: 2, children: [
1083
- { hue: 142, label: "≥70%" },
1084
- { hue: 38, label: "40–70%" },
1085
- { hue: 4, label: "<40%" }
1086
- ].map(({ hue, label }) => /* @__PURE__ */ jsxRuntime.jsxs(
1087
- designSystem.Flex,
1088
- {
1089
- alignItems: "center",
1090
- gap: 1,
1091
- style: { marginLeft: 8 },
1092
- children: [
1093
- /* @__PURE__ */ jsxRuntime.jsx(
1094
- "span",
1095
- {
1096
- style: {
1097
- width: 8,
1098
- height: 8,
1099
- borderRadius: "50%",
1100
- background: `hsl(${hue},60%,50%)`,
1101
- display: "inline-block",
1102
- flexShrink: 0
1103
- }
1104
- }
1105
- ),
1106
- /* @__PURE__ */ jsxRuntime.jsx("span", { style: { fontSize: 9, color: T.textMuted }, children: label })
1107
- ]
1108
- },
1109
- hue
1110
- )) })
1111
- ] }),
1112
- /* @__PURE__ */ jsxRuntime.jsx(RtnTip, { children: rtnHovRow ? `Cohort ${rtnHovRow.label} · ${rtnHovRow.cohortSize.toLocaleString()} users · active ${rtnHovRow.activeStart} – ${rtnHovRow.activeEnd}` : "Hover a row to see cohort details" }),
1113
- retentionQuery.isLoading ? /* @__PURE__ */ jsxRuntime.jsx(
1114
- designSystem.Flex,
1115
- {
1116
- justifyContent: "center",
1117
- alignItems: "center",
1118
- style: { minHeight: 80 },
1119
- children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Loader, { children: "Loading…" })
1120
- }
1121
- ) : rtnData.length === 0 ? /* @__PURE__ */ jsxRuntime.jsxs(Empty, { children: [
1122
- /* @__PURE__ */ jsxRuntime.jsx("div", { style: { fontSize: 22 }, children: "📉" }),
1123
- /* @__PURE__ */ jsxRuntime.jsx("div", { children: "No retention data for this period" })
1124
- ] }) : /* @__PURE__ */ jsxRuntime.jsx("div", { style: { display: "flex", flexDirection: "column", gap: 5 }, children: rtnData.map((row, i) => {
1125
- const hue = rateHue(row.retentionRate);
1126
- return /* @__PURE__ */ jsxRuntime.jsxs(
1127
- RtnRow,
1128
- {
1129
- $hov: rtnHov === i,
1130
- onMouseEnter: () => setRtnHov(i),
1131
- onMouseLeave: () => setRtnHov(null),
1132
- children: [
1133
- /* @__PURE__ */ jsxRuntime.jsx(RtnLabel, { children: row.label }),
1134
- /* @__PURE__ */ jsxRuntime.jsx(RtnSize, { children: row.cohortSize.toLocaleString() }),
1135
- /* @__PURE__ */ jsxRuntime.jsx(RtnTrack, { children: /* @__PURE__ */ jsxRuntime.jsx(RtnBar, { $w: row.retentionRate, $hue: hue }) }),
1136
- /* @__PURE__ */ jsxRuntime.jsxs(RtnPct, { $hue: hue, children: [
1137
- row.retentionRate.toFixed(1),
1138
- "%"
1139
- ] })
1140
- ]
1141
- },
1142
- row.n
1143
- );
1144
- }) })
1145
- ] }),
1146
- /* @__PURE__ */ jsxRuntime.jsx(RingGrid, { children: [
1147
- {
1148
- label: "Daily Active",
1149
- value: stats.activeUsers.daily.active,
1150
- pct: stats.activeUsers.daily.percentage,
1151
- color: T.amber,
1152
- sparkline: activeSpark,
1153
- delay: 8
1154
- },
1155
- {
1156
- label: "Weekly Active",
1157
- value: stats.activeUsers.weekly.active,
1158
- pct: stats.activeUsers.weekly.percentage,
1159
- color: T.green,
1160
- sparkline: activeSpark,
1161
- delay: 9
1162
- },
1163
- {
1164
- label: "Monthly Active",
1165
- value: stats.activeUsers.monthly.active,
1166
- pct: stats.activeUsers.monthly.percentage,
1167
- color: T.accent,
1168
- sparkline: activeSpark,
1169
- delay: 10
1170
- }
1171
- ].map(({ label, value, pct, color, delay }) => {
1172
- const isPos = pct == null || pct >= 0;
1173
- return /* @__PURE__ */ jsxRuntime.jsxs(RingCard, { $delay: delay, children: [
1174
- /* @__PURE__ */ jsxRuntime.jsx(
1175
- ProgressRing,
1176
- {
1177
- value: value ?? 0,
1178
- max: activeMax,
1179
- color,
1180
- size: 56
1181
- }
1182
- ),
1183
- /* @__PURE__ */ jsxRuntime.jsxs(RingInfo, { children: [
1184
- /* @__PURE__ */ jsxRuntime.jsx(RingLabel, { children: label }),
1185
- /* @__PURE__ */ jsxRuntime.jsx(RingVal, { children: (value ?? 0).toLocaleString() }),
1186
- pct != null && /* @__PURE__ */ jsxRuntime.jsxs(
1187
- TrendBadge,
1188
- {
1189
- $pos: isPos,
1190
- style: { marginTop: 4, marginBottom: 0 },
1191
- children: [
1192
- isPos ? "↑" : "↓",
1193
- " ",
1194
- Math.abs(pct).toFixed(1),
1195
- "%"
1196
- ]
1197
- }
1198
- )
1199
- ] })
1200
- ] }, label);
1201
- }) })
1202
- ] })
1203
- ] });
1204
- }
1205
- function ProtectedOverviewPage() {
1206
- return /* @__PURE__ */ jsxRuntime.jsx(
1207
- admin.Page.Protect,
1208
- {
1209
- permissions: index.PERMISSIONS.overview.filter(
1210
- (p) => p.action === `plugin::${index.PLUGIN_ID}.overview.read`
1211
- ),
1212
- children: /* @__PURE__ */ jsxRuntime.jsx(OverviewPage, {})
1213
- }
1214
- );
1215
- }
1216
- exports.OverviewPage = OverviewPage;
1217
- exports.default = ProtectedOverviewPage;