@carlonicora/nextjs-jsonapi 3.24.2 → 3.26.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 (220) hide show
  1. package/dist/{AssistantMessageInterface-1Kbz8RVs.d.ts → AssistantMessageInterface-CubxuBIJ.d.ts} +1 -1
  2. package/dist/{AssistantMessageInterface-CoxJyuFM.d.mts → AssistantMessageInterface-sHjI2zPN.d.mts} +1 -1
  3. package/dist/{BlockNoteEditor-35D4R7HJ.js → BlockNoteEditor-4EIKMNY3.js} +31 -21
  4. package/dist/BlockNoteEditor-4EIKMNY3.js.map +1 -0
  5. package/dist/{BlockNoteEditor-65HR6ULV.mjs → BlockNoteEditor-AXYVDDXM.mjs} +16 -6
  6. package/dist/{BlockNoteEditor-65HR6ULV.mjs.map → BlockNoteEditor-AXYVDDXM.mjs.map} +1 -1
  7. package/dist/{BlockNoteViewer-QDXDVYEM.js → BlockNoteViewer-7BFNTN4E.js} +4 -4
  8. package/dist/{BlockNoteViewer-QDXDVYEM.js.map → BlockNoteViewer-7BFNTN4E.js.map} +1 -1
  9. package/dist/{BlockNoteViewer-KDKGUIG5.mjs → BlockNoteViewer-KRTAFTUX.mjs} +3 -3
  10. package/dist/{HandbookSectionInterface-OePKPbqN.d.ts → HandbookSectionInterface-Ck1wZJ81.d.ts} +1 -1
  11. package/dist/{HandbookSectionInterface-BYsmnPEz.d.mts → HandbookSectionInterface-DgYH--Ik.d.mts} +1 -1
  12. package/dist/JsonApiRequest-TE5WD3WS.js +26 -0
  13. package/dist/{JsonApiRequest-G625QAH5.js.map → JsonApiRequest-TE5WD3WS.js.map} +1 -1
  14. package/dist/{JsonApiRequest-RRMNV7YE.mjs → JsonApiRequest-UHOWNI3O.mjs} +2 -2
  15. package/dist/analytics.modules-CfczU12P.d.ts +25 -0
  16. package/dist/analytics.modules-pzGwMASI.d.mts +25 -0
  17. package/dist/{auth.interface-BC1jp_ni.d.ts → auth.interface-BzI4onI_.d.ts} +1 -1
  18. package/dist/{auth.interface-dR2-6gPM.d.mts → auth.interface-Df4vXbNz.d.mts} +1 -1
  19. package/dist/billing/index.js +402 -402
  20. package/dist/billing/index.mjs +4 -4
  21. package/dist/{chunk-YXRVPEEJ.js → chunk-26MQIJNB.js} +1 -1
  22. package/dist/{chunk-YXRVPEEJ.js.map → chunk-26MQIJNB.js.map} +1 -1
  23. package/dist/{chunk-T76WLDE5.mjs → chunk-37DMFVLN.mjs} +1 -1
  24. package/dist/{chunk-3AR4KA5D.js → chunk-4Z45NYPR.js} +13 -13
  25. package/dist/{chunk-3AR4KA5D.js.map → chunk-4Z45NYPR.js.map} +1 -1
  26. package/dist/chunk-7B4E42R5.mjs +280 -0
  27. package/dist/chunk-7B4E42R5.mjs.map +1 -0
  28. package/dist/{chunk-2XCUAHQT.js → chunk-ABVTYQIJ.js} +1563 -1336
  29. package/dist/chunk-ABVTYQIJ.js.map +1 -0
  30. package/dist/{chunk-U7QVISQS.js → chunk-CAKOFJZD.js} +450 -40
  31. package/dist/chunk-CAKOFJZD.js.map +1 -0
  32. package/dist/{chunk-IBVKPLHT.mjs → chunk-E3I4MT53.mjs} +422 -12
  33. package/dist/chunk-E3I4MT53.mjs.map +1 -0
  34. package/dist/chunk-GHFMKH3M.js +280 -0
  35. package/dist/chunk-GHFMKH3M.js.map +1 -0
  36. package/dist/chunk-GKDHYB2R.js +110 -0
  37. package/dist/chunk-GKDHYB2R.js.map +1 -0
  38. package/dist/{chunk-ZJI5ZZW5.mjs → chunk-Q6J2ICKO.mjs} +3 -3
  39. package/dist/{chunk-4O7R2Z2O.mjs → chunk-QWZAGQWJ.mjs} +2175 -1948
  40. package/dist/chunk-QWZAGQWJ.mjs.map +1 -0
  41. package/dist/chunk-U65JPKO3.mjs +110 -0
  42. package/dist/chunk-U65JPKO3.mjs.map +1 -0
  43. package/dist/client/index.d.mts +6 -5
  44. package/dist/client/index.d.ts +6 -5
  45. package/dist/client/index.js +5 -5
  46. package/dist/client/index.js.map +1 -1
  47. package/dist/client/index.mjs +4 -4
  48. package/dist/components/index.d.mts +33 -10
  49. package/dist/components/index.d.ts +33 -10
  50. package/dist/components/index.js +7 -5
  51. package/dist/components/index.js.map +1 -1
  52. package/dist/components/index.mjs +6 -4
  53. package/dist/{config-B6UB7j69.d.ts → config-3N1LSrcC.d.ts} +1 -1
  54. package/dist/{config-Cd5xTEfZ.d.ts → config-AVtuvfM1.d.ts} +1 -1
  55. package/dist/{config-n-ZpPmOL.d.mts → config-BSKTYDoh.d.mts} +1 -1
  56. package/dist/{config-YyhFHxkU.d.mts → config-BmufyEj6.d.mts} +1 -1
  57. package/dist/contexts/index.d.mts +5 -4
  58. package/dist/contexts/index.d.ts +5 -4
  59. package/dist/contexts/index.js +5 -5
  60. package/dist/contexts/index.mjs +4 -4
  61. package/dist/core/index.d.mts +31 -14
  62. package/dist/core/index.d.ts +31 -14
  63. package/dist/core/index.js +9 -5
  64. package/dist/core/index.js.map +1 -1
  65. package/dist/core/index.mjs +8 -4
  66. package/dist/features/analytics/index.css +30 -0
  67. package/dist/features/analytics/index.css.map +1 -0
  68. package/dist/features/analytics/index.d.mts +508 -0
  69. package/dist/features/analytics/index.d.ts +508 -0
  70. package/dist/features/analytics/index.js +1166 -0
  71. package/dist/features/analytics/index.js.map +1 -0
  72. package/dist/features/analytics/index.mjs +1166 -0
  73. package/dist/features/analytics/index.mjs.map +1 -0
  74. package/dist/features/analytics/server.d.mts +28 -0
  75. package/dist/features/analytics/server.d.ts +28 -0
  76. package/dist/features/analytics/server.js +41 -0
  77. package/dist/features/analytics/server.js.map +1 -0
  78. package/dist/features/analytics/server.mjs +41 -0
  79. package/dist/features/analytics/server.mjs.map +1 -0
  80. package/dist/features/help/index.d.mts +2 -2
  81. package/dist/features/help/index.d.ts +2 -2
  82. package/dist/features/help/index.js +43 -43
  83. package/dist/features/help/index.js.map +1 -1
  84. package/dist/features/help/index.mjs +9 -9
  85. package/dist/features/help/index.mjs.map +1 -1
  86. package/dist/features/tokenusage/index.d.mts +2 -2
  87. package/dist/features/tokenusage/index.d.ts +2 -2
  88. package/dist/features/tokenusage/index.js +141 -223
  89. package/dist/features/tokenusage/index.js.map +1 -1
  90. package/dist/features/tokenusage/index.mjs +28 -110
  91. package/dist/features/tokenusage/index.mjs.map +1 -1
  92. package/dist/index.d.mts +11 -9
  93. package/dist/index.d.ts +11 -9
  94. package/dist/index.js +10 -6
  95. package/dist/index.js.map +1 -1
  96. package/dist/index.mjs +9 -5
  97. package/dist/notification.interface-BNCvnVCW.d.ts +34 -0
  98. package/dist/notification.interface-P_pNfzZQ.d.mts +34 -0
  99. package/dist/{s3.service-Bqr-fQmi.d.ts → s3.service-CbUa6a-8.d.ts} +3 -2
  100. package/dist/{s3.service-Buiy1ncj.d.mts → s3.service-DAp8FbuG.d.mts} +3 -2
  101. package/dist/server/index.d.mts +5 -4
  102. package/dist/server/index.d.ts +5 -4
  103. package/dist/server/index.js +39 -279
  104. package/dist/server/index.js.map +1 -1
  105. package/dist/server/index.mjs +20 -260
  106. package/dist/server/index.mjs.map +1 -1
  107. package/dist/{types-bsm8Fp55.d.ts → types-Cn153-mH.d.ts} +1 -1
  108. package/dist/{types-Bq_UA8Lg.d.mts → types-D0uLYV2P.d.mts} +1 -1
  109. package/dist/{usePageUrlGenerator-J_ECv-oQ.d.ts → usePageUrlGenerator-B-3FRKBD.d.ts} +1 -1
  110. package/dist/{usePageUrlGenerator-uKBjAKqC.d.mts → usePageUrlGenerator-CCJ_cdQy.d.mts} +1 -1
  111. package/dist/{useSocket-BcH7l7B8.d.mts → useSocket-B9gTbuCj.d.mts} +1 -1
  112. package/dist/{useSocket-DCZd_eJk.d.ts → useSocket-BHVIyigI.d.ts} +1 -1
  113. package/dist/{notification.interface-MnRt1Tjm.d.mts → user.interface-CSaGWFbg.d.mts} +1 -31
  114. package/dist/{notification.interface-CjzV_KII.d.ts → user.interface-CrmwJLYW.d.ts} +1 -31
  115. package/package.json +11 -1
  116. package/src/client/index.ts +2 -0
  117. package/src/components/containers/PageContainer.tsx +1 -1
  118. package/src/components/containers/RoundPageContainer.tsx +60 -16
  119. package/src/components/containers/RoundPageContainerTitle.tsx +75 -35
  120. package/src/components/editors/BlockNoteEditor.tsx +13 -1
  121. package/src/components/forms/EntityMultiSelector.tsx +4 -4
  122. package/src/components/forms/EntitySelector.tsx +4 -4
  123. package/src/components/forms/FormBlockNote.tsx +21 -1
  124. package/src/components/forms/FormCheckbox.tsx +6 -2
  125. package/src/components/forms/__tests__/useEditorDialog.test.ts +47 -0
  126. package/src/components/forms/useEditorDialog.ts +22 -0
  127. package/src/components/navigations/Breadcrumb.tsx +45 -16
  128. package/src/components/navigations/Header.tsx +16 -3
  129. package/src/components/navigations/MobileNavigationBar.tsx +12 -0
  130. package/src/components/navigations/__tests__/MobileNavigationBar.spec.tsx +17 -0
  131. package/src/components/tables/ContentListTable.tsx +52 -32
  132. package/src/core/index.ts +4 -0
  133. package/src/core/registry/ModuleRegistry.ts +7 -0
  134. package/src/features/__tests__/i18n-keys.contract.spec.ts +1 -0
  135. package/src/features/analytics/__tests__/server-entry.spec.tsx +64 -0
  136. package/src/features/analytics/analytics.modules.ts +30 -0
  137. package/src/features/analytics/components/AnalyticsAdminContainer.tsx +97 -0
  138. package/src/features/analytics/components/AnalyticsAdminFilterBar.tsx +120 -0
  139. package/src/features/analytics/components/AnalyticsAdminPageContainer.tsx +25 -0
  140. package/src/features/analytics/components/AnalyticsAdminTiles.tsx +191 -0
  141. package/src/features/analytics/components/AnalyticsBreakdownTable.tsx +105 -0
  142. package/src/features/analytics/components/AnalyticsJourneyContainer.tsx +246 -0
  143. package/src/features/analytics/components/AnalyticsJourneyPageContainer.tsx +14 -0
  144. package/src/features/analytics/components/AnalyticsSessionsTable.tsx +107 -0
  145. package/src/features/analytics/components/AnalyticsTimelineChart.tsx +175 -0
  146. package/src/features/analytics/components/AnalyticsTracker.tsx +112 -0
  147. package/src/features/analytics/components/__tests__/AnalyticsAdminTiles.spec.tsx +39 -0
  148. package/src/features/analytics/components/__tests__/AnalyticsBreakdownTable.spec.tsx +39 -0
  149. package/src/features/analytics/components/__tests__/AnalyticsJourneyContainer.spec.tsx +124 -0
  150. package/src/features/analytics/components/__tests__/AnalyticsPageContainers.spec.tsx +117 -0
  151. package/src/features/analytics/components/__tests__/AnalyticsSessionsTable.spec.tsx +121 -0
  152. package/src/features/analytics/components/__tests__/AnalyticsTimelineChart.spec.tsx +35 -0
  153. package/src/features/analytics/components/__tests__/AnalyticsTracker.spec.tsx +248 -0
  154. package/src/features/analytics/components/server/AnalyticsAdminPage.tsx +28 -0
  155. package/src/features/analytics/components/server/AnalyticsJourneyPage.tsx +25 -0
  156. package/src/features/analytics/contexts/AnalyticsAdminContext.tsx +273 -0
  157. package/src/features/analytics/contexts/__tests__/AnalyticsAdminContext.spec.tsx +140 -0
  158. package/src/features/analytics/data/AnalyticsAdminService.ts +91 -0
  159. package/src/features/analytics/data/AnalyticsService.ts +23 -0
  160. package/src/features/analytics/data/__tests__/AnalyticsAdminService.spec.ts +117 -0
  161. package/src/features/analytics/data/__tests__/AnalyticsService.spec.ts +56 -0
  162. package/src/features/analytics/data/__tests__/models.spec.ts +300 -0
  163. package/src/features/analytics/data/analytics-breakdown.interface.ts +9 -0
  164. package/src/features/analytics/data/analytics-breakdown.ts +65 -0
  165. package/src/features/analytics/data/analytics-event.interface.ts +7 -0
  166. package/src/features/analytics/data/analytics-event.ts +63 -0
  167. package/src/features/analytics/data/analytics-page-view.interface.ts +9 -0
  168. package/src/features/analytics/data/analytics-page-view.ts +57 -0
  169. package/src/features/analytics/data/analytics-session.interface.ts +23 -0
  170. package/src/features/analytics/data/analytics-session.ts +144 -0
  171. package/src/features/analytics/data/analytics-summary.interface.ts +11 -0
  172. package/src/features/analytics/data/analytics-summary.ts +79 -0
  173. package/src/features/analytics/data/analytics-timeline.interface.ts +10 -0
  174. package/src/features/analytics/data/analytics-timeline.ts +67 -0
  175. package/src/features/analytics/data/analytics.types.ts +97 -0
  176. package/src/features/analytics/data/index.ts +15 -0
  177. package/src/features/analytics/i18n-keys.ts +76 -0
  178. package/src/features/analytics/index.ts +31 -0
  179. package/src/features/analytics/lib/__tests__/utm.spec.ts +20 -0
  180. package/src/features/analytics/lib/__tests__/visitorCookie.spec.ts +63 -0
  181. package/src/features/analytics/lib/utm.ts +29 -0
  182. package/src/features/analytics/lib/visitorCookie.ts +32 -0
  183. package/src/features/analytics/server-entry.ts +8 -0
  184. package/src/features/assistant/components/parts/AssistantBlockNoteComposer.tsx +1 -1
  185. package/src/features/assistant/components/parts/AssistantComposer.tsx +1 -1
  186. package/src/features/assistant-message/components/MessageItem.tsx +2 -2
  187. package/src/features/assistant-message/components/parts/tabs/CitationsTab.tsx +23 -5
  188. package/src/features/auth/components/containers/AuthContainer.tsx +8 -1
  189. package/src/features/company/components/details/CompanyContent.tsx +3 -1
  190. package/src/features/company/components/details/CompanyDetails.tsx +9 -1
  191. package/src/features/company/components/details/TokenStatusIndicator.tsx +42 -13
  192. package/src/features/help/components/HelpHeader.tsx +7 -5
  193. package/src/features/tokenusage/components/TokenUsageAdminFilterBar.tsx +9 -6
  194. package/src/features/tokenusage/lib/__tests__/formatters.spec.ts +21 -0
  195. package/src/features/tokenusage/lib/formatters.ts +5 -1
  196. package/src/features/waitlist/components/lists/WaitlistList.tsx +68 -121
  197. package/src/features/waitlist/data/WaitlistService.ts +2 -2
  198. package/src/features/waitlist/data/index.ts +1 -0
  199. package/src/features/waitlist/data/waitlist.fields.ts +8 -0
  200. package/src/features/waitlist/hooks/useWaitlistTableStructure.tsx +69 -30
  201. package/src/shadcnui/ui/button.tsx +1 -1
  202. package/src/shadcnui/ui/checkbox.tsx +1 -1
  203. package/src/shadcnui/ui/command.tsx +4 -4
  204. package/src/shadcnui/ui/dialog.tsx +2 -0
  205. package/src/shadcnui/ui/input.tsx +1 -1
  206. package/src/shadcnui/ui/popover.tsx +9 -0
  207. package/src/shadcnui/ui/select.tsx +1 -1
  208. package/src/shadcnui/ui/sheet.tsx +8 -1
  209. package/src/shadcnui/ui/table.tsx +2 -2
  210. package/src/shadcnui/ui/textarea.tsx +1 -1
  211. package/dist/BlockNoteEditor-35D4R7HJ.js.map +0 -1
  212. package/dist/JsonApiRequest-G625QAH5.js +0 -26
  213. package/dist/chunk-2XCUAHQT.js.map +0 -1
  214. package/dist/chunk-4O7R2Z2O.mjs.map +0 -1
  215. package/dist/chunk-IBVKPLHT.mjs.map +0 -1
  216. package/dist/chunk-U7QVISQS.js.map +0 -1
  217. /package/dist/{BlockNoteViewer-KDKGUIG5.mjs.map → BlockNoteViewer-KRTAFTUX.mjs.map} +0 -0
  218. /package/dist/{JsonApiRequest-RRMNV7YE.mjs.map → JsonApiRequest-UHOWNI3O.mjs.map} +0 -0
  219. /package/dist/{chunk-T76WLDE5.mjs.map → chunk-37DMFVLN.mjs.map} +0 -0
  220. /package/dist/{chunk-ZJI5ZZW5.mjs.map → chunk-Q6J2ICKO.mjs.map} +0 -0
@@ -0,0 +1,97 @@
1
+ "use client";
2
+
3
+ import { useTranslations } from "next-intl";
4
+ import { RoundPageContainer } from "../../../components";
5
+ import { Card, CardContent, CardHeader, CardTitle } from "../../../shadcnui";
6
+ import { useAnalyticsAdmin } from "../contexts/AnalyticsAdminContext";
7
+ import { AnalyticsAdminTiles } from "./AnalyticsAdminTiles";
8
+ import { AnalyticsBreakdownTable } from "./AnalyticsBreakdownTable";
9
+ import { AnalyticsSessionsTable } from "./AnalyticsSessionsTable";
10
+ import { AnalyticsTimelineChart } from "./AnalyticsTimelineChart";
11
+
12
+ /**
13
+ * Page body for the administrative analytics dashboard.
14
+ *
15
+ * Stateless by design — every value it renders comes from
16
+ * `useAnalyticsAdmin()`. The filter bar is deliberately NOT here: it belongs to
17
+ * the page title bar, which `RoundPageContainer` fills from `SharedContext`, so
18
+ * the provider publishes it (see AnalyticsAdminContext).
19
+ */
20
+ export function AnalyticsAdminContainer() {
21
+ const t = useTranslations();
22
+ const {
23
+ summary,
24
+ timeline,
25
+ breakdowns,
26
+ sessions,
27
+ loadMoreSessions,
28
+ hasMoreSessions,
29
+ sessionPageUrl,
30
+ isLoading,
31
+ error,
32
+ } = useAnalyticsAdmin();
33
+
34
+ if (error) {
35
+ return (
36
+ <RoundPageContainer fullWidth forceHeader>
37
+ <div className="p-4">
38
+ <Card>
39
+ <CardContent>
40
+ <p className="text-destructive text-xs/relaxed">{error}</p>
41
+ </CardContent>
42
+ </Card>
43
+ </div>
44
+ </RoundPageContainer>
45
+ );
46
+ }
47
+
48
+ // Loading renders nothing in the body: the title bar (with the filter bar) is
49
+ // already mounted, so a spinner would only make the controls jump on arrival.
50
+ if (isLoading) return <RoundPageContainer fullWidth forceHeader />;
51
+
52
+ return (
53
+ <RoundPageContainer fullWidth forceHeader>
54
+ <div className="flex w-full flex-col gap-4 p-4">
55
+ <div className="flex flex-col gap-2">
56
+ <AnalyticsAdminTiles summary={summary} />
57
+ {/* Days past the raw-event retention are served from daily summaries.
58
+ Whether the window reaches them is the backend's knowledge, not the
59
+ client's, so the caption is always shown. */}
60
+ <p className="text-muted-foreground text-xs">{t("analytics.admin.tiles.summarised_note")}</p>
61
+ </div>
62
+
63
+ <Card>
64
+ <CardHeader>
65
+ <CardTitle>{t("analytics.admin.panels.over_time")}</CardTitle>
66
+ </CardHeader>
67
+ <CardContent>
68
+ <AnalyticsTimelineChart rows={timeline} />
69
+ </CardContent>
70
+ </Card>
71
+
72
+ <Card>
73
+ <CardHeader>
74
+ <CardTitle>{t("analytics.admin.panels.breakdown")}</CardTitle>
75
+ </CardHeader>
76
+ <CardContent>
77
+ <AnalyticsBreakdownTable breakdowns={breakdowns} />
78
+ </CardContent>
79
+ </Card>
80
+
81
+ <Card>
82
+ <CardHeader>
83
+ <CardTitle>{t("analytics.admin.panels.sessions")}</CardTitle>
84
+ </CardHeader>
85
+ <CardContent>
86
+ <AnalyticsSessionsTable
87
+ rows={sessions}
88
+ sessionPageUrl={sessionPageUrl}
89
+ onLoadMore={loadMoreSessions}
90
+ hasMore={hasMoreSessions}
91
+ />
92
+ </CardContent>
93
+ </Card>
94
+ </div>
95
+ </RoundPageContainer>
96
+ );
97
+ }
@@ -0,0 +1,120 @@
1
+ "use client";
2
+
3
+ import { useTranslations } from "next-intl";
4
+ import { DateRangeSelector } from "../../../components/forms/DateRangeSelector";
5
+ import { Button } from "../../../shadcnui";
6
+ import { cn } from "../../../utils";
7
+ import {
8
+ ANALYTICS_GRANULARITIES,
9
+ ANALYTICS_SECTIONS,
10
+ type AnalyticsGranularity,
11
+ type AnalyticsSection,
12
+ } from "../data/analytics.types";
13
+
14
+ type FilterState = {
15
+ from: string;
16
+ to: string;
17
+ section: AnalyticsSection;
18
+ granularity: AnalyticsGranularity;
19
+ };
20
+
21
+ type Props = FilterState & {
22
+ /** Receives ONLY the keys that changed. */
23
+ onChange: (next: Partial<FilterState>) => void;
24
+ };
25
+
26
+ /**
27
+ * The single control row above the KPI tiles.
28
+ *
29
+ * It is deliberately stateless: every control reports the one key it changed and
30
+ * the owning context re-fetches. Keeping the whole filter state in one place is
31
+ * what lets the page issue a single coordinated batch of requests instead of one
32
+ * per control.
33
+ */
34
+ export function AnalyticsAdminFilterBar({ section, granularity, onChange }: Props) {
35
+ const t = useTranslations();
36
+
37
+ const sectionLabels: Record<AnalyticsSection, string> = {
38
+ all: t("analytics.admin.filter.section_all"),
39
+ public: t("analytics.admin.filter.section_public"),
40
+ app: t("analytics.admin.filter.section_app"),
41
+ };
42
+
43
+ const granularityLabels: Record<AnalyticsGranularity, string> = {
44
+ day: t("analytics.admin.filter.granularity_day"),
45
+ week: t("analytics.admin.filter.granularity_week"),
46
+ month: t("analytics.admin.filter.granularity_month"),
47
+ };
48
+
49
+ return (
50
+ <div className="flex flex-wrap items-center gap-2">
51
+ {/* The date button is a fixed 300px; on a phone it takes the full row instead. */}
52
+ <div className="max-md:w-full max-md:[&_#date]:w-full">
53
+ <DateRangeSelector
54
+ onDateChange={(range) => {
55
+ if (!range?.from || !range?.to) return;
56
+ onChange({ from: range.from.toISOString(), to: range.to.toISOString() });
57
+ }}
58
+ />
59
+ </div>
60
+
61
+ <Segmented
62
+ ariaLabel={t("analytics.admin.filter.section")}
63
+ value={section}
64
+ options={ANALYTICS_SECTIONS.map((value) => ({ value, label: sectionLabels[value] }))}
65
+ onSelect={(value) => onChange({ section: value })}
66
+ />
67
+
68
+ <Segmented
69
+ ariaLabel={t("analytics.admin.filter.granularity")}
70
+ value={granularity}
71
+ options={ANALYTICS_GRANULARITIES.map((value) => ({ value, label: granularityLabels[value] }))}
72
+ onSelect={(value) => onChange({ granularity: value })}
73
+ />
74
+ </div>
75
+ );
76
+ }
77
+
78
+ /**
79
+ * A segmented control built from plain Buttons.
80
+ *
81
+ * The design system has no ToggleGroup primitive, and Base UI triggers may never
82
+ * wrap a Button, so the segmented look is composed from Buttons directly — the
83
+ * selected segment takes the solid variant, the rest stay ghosts.
84
+ */
85
+ function Segmented<T extends string>({
86
+ ariaLabel,
87
+ value,
88
+ options,
89
+ onSelect,
90
+ }: {
91
+ ariaLabel: string;
92
+ value: T;
93
+ options: { value: T; label: string }[];
94
+ onSelect: (value: T) => void;
95
+ }) {
96
+ return (
97
+ <div
98
+ role="group"
99
+ aria-label={ariaLabel}
100
+ className="border-border inline-flex items-center gap-0.5 rounded-md border p-0.5"
101
+ >
102
+ {options.map((option) => {
103
+ const selected = option.value === value;
104
+ return (
105
+ <Button
106
+ key={option.value}
107
+ type="button"
108
+ size="sm"
109
+ variant={selected ? "default" : "ghost"}
110
+ aria-pressed={selected}
111
+ className={cn(!selected && "text-muted-foreground")}
112
+ onClick={() => onSelect(option.value)}
113
+ >
114
+ {option.label}
115
+ </Button>
116
+ );
117
+ })}
118
+ </div>
119
+ );
120
+ }
@@ -0,0 +1,25 @@
1
+ "use client";
2
+
3
+ import { AnalyticsAdminProvider } from "../contexts/AnalyticsAdminContext";
4
+ import { AnalyticsAdminContainer } from "./AnalyticsAdminContainer";
5
+
6
+ type Props = {
7
+ /** Route the breadcrumb links back to. Defaults to the provider's default. */
8
+ pageUrl?: string;
9
+ /** Base route of the journey page each session row links to. */
10
+ sessionPageUrl?: string;
11
+ };
12
+
13
+ /**
14
+ * The whole administrative analytics page as one client component, so a host
15
+ * app's server page renders a single element. `AnalyticsAdminContainer` owns
16
+ * the `RoundPageContainer`; the provider sits above it because it publishes the
17
+ * filter bar into the title bar.
18
+ */
19
+ export function AnalyticsAdminPageContainer({ pageUrl, sessionPageUrl }: Props) {
20
+ return (
21
+ <AnalyticsAdminProvider pageUrl={pageUrl} sessionPageUrl={sessionPageUrl}>
22
+ <AnalyticsAdminContainer />
23
+ </AnalyticsAdminProvider>
24
+ );
25
+ }
@@ -0,0 +1,191 @@
1
+ "use client";
2
+
3
+ import { ArrowDownIcon, ArrowUpIcon } from "lucide-react";
4
+ import { useTranslations } from "next-intl";
5
+ import { MicroLabel } from "../../../components";
6
+ import { Card, CardContent } from "../../../shadcnui";
7
+ import { cn } from "../../../utils";
8
+ import { useUsageFormatters } from "../../tokenusage/lib/formatters";
9
+ import { percentageDelta } from "../../tokenusage/lib/metrics";
10
+ import type { AnalyticsSummaryInterface } from "../data/analytics-summary.interface";
11
+
12
+ type Props = {
13
+ /** The six summary rows: {public, app, total} × {current, previous}. */
14
+ summary: AnalyticsSummaryInterface[];
15
+ };
16
+
17
+ type SummaryMetrics = Pick<
18
+ AnalyticsSummaryInterface,
19
+ "visitors" | "sessions" | "pageViews" | "pagesPerSession" | "consentShare"
20
+ >;
21
+
22
+ const ZERO: SummaryMetrics = { visitors: 0, sessions: 0, pageViews: 0, pagesPerSession: 0, consentShare: 0 };
23
+
24
+ /**
25
+ * The KPI header of the administrative analytics page.
26
+ *
27
+ * Three lead tiles carry the traffic volumes — visitors, sessions, page views —
28
+ * each with its delta against the equal-length preceding window. Two supporting
29
+ * tiles below carry the ratios that give those numbers context.
30
+ *
31
+ * The tiles read the `total` row: the backend already applies the section filter
32
+ * to it, so it is the figure for whatever the filter bar selected. The backend
33
+ * always returns both windows, which is why no tile has to branch on a missing
34
+ * row: an absent one is simply zero-filled here.
35
+ */
36
+ export function AnalyticsAdminTiles({ summary }: Props) {
37
+ const t = useTranslations();
38
+ const { decimal } = useUsageFormatters();
39
+
40
+ const rowFor = (window: string): SummaryMetrics =>
41
+ summary.find((r) => r.section === "total" && r.window === window) ?? ZERO;
42
+
43
+ const current = rowFor("current");
44
+ const previous = rowFor("previous");
45
+
46
+ const previousLabel = t("analytics.admin.tiles.vs_previous");
47
+
48
+ return (
49
+ <div className="grid gap-3">
50
+ <div className="grid gap-3 sm:grid-cols-3">
51
+ <LeadTile
52
+ testId="analytics-tile-visitors"
53
+ label={t("analytics.admin.tiles.visitors")}
54
+ value={decimal(current.visitors, 0)}
55
+ delta={percentageDelta(current.visitors, previous.visitors)}
56
+ previousLabel={previousLabel}
57
+ decimal={decimal}
58
+ />
59
+ <LeadTile
60
+ testId="analytics-tile-sessions"
61
+ label={t("analytics.admin.tiles.sessions")}
62
+ value={decimal(current.sessions, 0)}
63
+ delta={percentageDelta(current.sessions, previous.sessions)}
64
+ previousLabel={previousLabel}
65
+ decimal={decimal}
66
+ />
67
+ <LeadTile
68
+ testId="analytics-tile-page-views"
69
+ label={t("analytics.admin.tiles.page_views")}
70
+ value={decimal(current.pageViews, 0)}
71
+ delta={percentageDelta(current.pageViews, previous.pageViews)}
72
+ previousLabel={previousLabel}
73
+ decimal={decimal}
74
+ />
75
+ </div>
76
+
77
+ <div className="grid gap-3 sm:grid-cols-2">
78
+ <SupportingTile
79
+ testId="analytics-tile-pages-per-session"
80
+ label={t("analytics.admin.tiles.pages_per_session")}
81
+ value={decimal(current.pagesPerSession, 1)}
82
+ delta={percentageDelta(current.pagesPerSession, previous.pagesPerSession)}
83
+ decimal={decimal}
84
+ />
85
+ <SupportingTile
86
+ testId="analytics-tile-consent-share"
87
+ label={t("analytics.admin.tiles.consent_share")}
88
+ // consentShare is a 0..1 ratio on the wire.
89
+ value={`${decimal(current.consentShare * 100, 0)}%`}
90
+ decimal={decimal}
91
+ />
92
+ </div>
93
+ </div>
94
+ );
95
+ }
96
+
97
+ function LeadTile({
98
+ testId,
99
+ label,
100
+ value,
101
+ delta,
102
+ previousLabel,
103
+ decimal,
104
+ }: {
105
+ testId: string;
106
+ label: string;
107
+ value: string;
108
+ delta: number | undefined;
109
+ previousLabel: string;
110
+ /** Passed down because `Delta` is a plain function, not a component: it cannot call the hook itself. */
111
+ decimal: (value: number, decimals: number) => string;
112
+ }) {
113
+ return (
114
+ <Card data-testid={testId}>
115
+ <CardContent className="grid gap-1">
116
+ <MicroLabel>{label}</MicroLabel>
117
+ <span className="text-primary text-xl font-semibold tabular-nums">{value}</span>
118
+ <span className="flex items-center gap-1">
119
+ <Delta testId={`${testId}-delta`} delta={delta} decimal={decimal} />
120
+ <span className="text-muted-foreground text-xs">{previousLabel}</span>
121
+ </span>
122
+ </CardContent>
123
+ </Card>
124
+ );
125
+ }
126
+
127
+ function SupportingTile({
128
+ testId,
129
+ label,
130
+ value,
131
+ delta,
132
+ decimal,
133
+ }: {
134
+ testId: string;
135
+ label: string;
136
+ value: string;
137
+ delta?: number | undefined;
138
+ /** Passed down because `Delta` is a plain function, not a component: it cannot call the hook itself. */
139
+ decimal: (value: number, decimals: number) => string;
140
+ }) {
141
+ return (
142
+ <Card data-testid={testId} size="sm">
143
+ <CardContent className="grid gap-1">
144
+ <MicroLabel>{label}</MicroLabel>
145
+ <span className="flex items-center gap-2">
146
+ <span className="text-sm font-medium tabular-nums">{value}</span>
147
+ {delta !== undefined && <Delta testId={`${testId}-delta`} delta={delta} decimal={decimal} />}
148
+ </span>
149
+ </CardContent>
150
+ </Card>
151
+ );
152
+ }
153
+
154
+ /**
155
+ * The delta slot. An undefined delta means the previous window was zero: an
156
+ * em dash says "not comparable" where a percentage would say "infinite growth".
157
+ */
158
+ function Delta({
159
+ testId,
160
+ delta,
161
+ decimal,
162
+ }: {
163
+ testId: string;
164
+ delta: number | undefined;
165
+ decimal: (value: number, decimals: number) => string;
166
+ }) {
167
+ if (delta === undefined) {
168
+ return (
169
+ <span data-testid={testId} className="text-muted-foreground text-xs">
170
+ —
171
+ </span>
172
+ );
173
+ }
174
+
175
+ const increased = delta >= 0;
176
+ const Icon = increased ? ArrowUpIcon : ArrowDownIcon;
177
+ const sign = delta > 0 ? "+" : delta < 0 ? "-" : "";
178
+
179
+ return (
180
+ <span
181
+ data-testid={testId}
182
+ className={cn(
183
+ "inline-flex items-center gap-0.5 text-xs tabular-nums",
184
+ increased ? "text-success" : "text-destructive",
185
+ )}
186
+ >
187
+ <Icon aria-hidden className="size-3" />
188
+ {`${sign}${decimal(Math.abs(delta), 0)}%`}
189
+ </span>
190
+ );
191
+ }
@@ -0,0 +1,105 @@
1
+ "use client";
2
+
3
+ import { useTranslations } from "next-intl";
4
+ import { useMemo } from "react";
5
+ import {
6
+ Table,
7
+ TableBody,
8
+ TableCell,
9
+ TableHead,
10
+ TableHeader,
11
+ TableRow,
12
+ Tabs,
13
+ TabsContent,
14
+ TabsList,
15
+ TabsTrigger,
16
+ } from "../../../shadcnui";
17
+ import { useUsageFormatters } from "../../tokenusage/lib/formatters";
18
+ import type { AnalyticsBreakdownInterface } from "../data/analytics-breakdown.interface";
19
+ import { ANALYTICS_DIMENSIONS, type AnalyticsDimension } from "../data/analytics.types";
20
+
21
+ type Props = {
22
+ breakdowns: Record<AnalyticsDimension, AnalyticsBreakdownInterface[]>;
23
+ };
24
+
25
+ /** The key the repository folds the tail past the limit into. */
26
+ const OTHER_KEY = "other";
27
+
28
+ /**
29
+ * Attribution and content breakdowns, one tab per dimension.
30
+ *
31
+ * Each tab is the same four-column table — key, visitors, sessions, page views —
32
+ * sorted by visitors. Sorting is client-side because the whole ranked set is
33
+ * already in memory, so a round trip would buy nothing. Keys are data, not
34
+ * vocabulary: `(none)` and the routes are rendered as the backend wrote them;
35
+ * only the `other` rollup is translated.
36
+ */
37
+ export function AnalyticsBreakdownTable({ breakdowns }: Props) {
38
+ const t = useTranslations();
39
+
40
+ const labels: Record<AnalyticsDimension, string> = {
41
+ source: t("analytics.admin.dimensions.source"),
42
+ medium: t("analytics.admin.dimensions.medium"),
43
+ campaign: t("analytics.admin.dimensions.campaign"),
44
+ referrer: t("analytics.admin.dimensions.referrer"),
45
+ route: t("analytics.admin.dimensions.route"),
46
+ landing: t("analytics.admin.dimensions.landing"),
47
+ };
48
+
49
+ return (
50
+ <Tabs defaultValue={ANALYTICS_DIMENSIONS[0]}>
51
+ {/* Six tabs are wider than a phone-width card: scroll them sideways there. */}
52
+ <TabsList className="max-md:w-full max-md:justify-start max-md:overflow-x-auto">
53
+ {ANALYTICS_DIMENSIONS.map((dimension) => (
54
+ <TabsTrigger key={dimension} value={dimension}>
55
+ {labels[dimension]}
56
+ </TabsTrigger>
57
+ ))}
58
+ </TabsList>
59
+ {ANALYTICS_DIMENSIONS.map((dimension) => (
60
+ <TabsContent key={dimension} value={dimension}>
61
+ <BreakdownRows rows={breakdowns[dimension] ?? []} />
62
+ </TabsContent>
63
+ ))}
64
+ </Tabs>
65
+ );
66
+ }
67
+
68
+ function BreakdownRows({ rows }: { rows: AnalyticsBreakdownInterface[] }) {
69
+ const t = useTranslations();
70
+ const { decimal } = useUsageFormatters();
71
+
72
+ const sortedRows = useMemo(() => [...rows].sort((a, b) => b.visitors - a.visitors), [rows]);
73
+
74
+ if (sortedRows.length === 0) {
75
+ return <p className="text-muted-foreground text-sm">{t("analytics.admin.no_data")}</p>;
76
+ }
77
+
78
+ return (
79
+ // The package's <Table> wraps itself in an `overflow-x-clip` container, which
80
+ // would swallow the overflow before this scroller ever saw it — hence the
81
+ // child override. Wide tables must scroll, never widen the page.
82
+ <div className="overflow-x-auto [&_[data-slot=table-container]]:overflow-x-visible">
83
+ <Table>
84
+ <TableHeader>
85
+ <TableRow>
86
+ <TableHead className="text-xs font-medium">{t("analytics.admin.columns.key")}</TableHead>
87
+ <TableHead className="text-end text-xs font-medium">{t("analytics.admin.columns.visitors")}</TableHead>
88
+ <TableHead className="text-end text-xs font-medium">{t("analytics.admin.columns.sessions")}</TableHead>
89
+ <TableHead className="text-end text-xs font-medium">{t("analytics.admin.columns.page_views")}</TableHead>
90
+ </TableRow>
91
+ </TableHeader>
92
+ <TableBody>
93
+ {sortedRows.map((row) => (
94
+ <TableRow key={row.id} data-testid={`analytics-breakdown-row-${row.id}`}>
95
+ <TableCell className="text-xs">{row.key === OTHER_KEY ? t("analytics.admin.other") : row.key}</TableCell>
96
+ <TableCell className="text-end text-xs tabular-nums">{decimal(row.visitors, 0)}</TableCell>
97
+ <TableCell className="text-end text-xs tabular-nums">{decimal(row.sessions, 0)}</TableCell>
98
+ <TableCell className="text-end text-xs tabular-nums">{decimal(row.pageViews, 0)}</TableCell>
99
+ </TableRow>
100
+ ))}
101
+ </TableBody>
102
+ </Table>
103
+ </div>
104
+ );
105
+ }