@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.
- package/dist/{AssistantMessageInterface-1Kbz8RVs.d.ts → AssistantMessageInterface-CubxuBIJ.d.ts} +1 -1
- package/dist/{AssistantMessageInterface-CoxJyuFM.d.mts → AssistantMessageInterface-sHjI2zPN.d.mts} +1 -1
- package/dist/{BlockNoteEditor-35D4R7HJ.js → BlockNoteEditor-4EIKMNY3.js} +31 -21
- package/dist/BlockNoteEditor-4EIKMNY3.js.map +1 -0
- package/dist/{BlockNoteEditor-65HR6ULV.mjs → BlockNoteEditor-AXYVDDXM.mjs} +16 -6
- package/dist/{BlockNoteEditor-65HR6ULV.mjs.map → BlockNoteEditor-AXYVDDXM.mjs.map} +1 -1
- package/dist/{BlockNoteViewer-QDXDVYEM.js → BlockNoteViewer-7BFNTN4E.js} +4 -4
- package/dist/{BlockNoteViewer-QDXDVYEM.js.map → BlockNoteViewer-7BFNTN4E.js.map} +1 -1
- package/dist/{BlockNoteViewer-KDKGUIG5.mjs → BlockNoteViewer-KRTAFTUX.mjs} +3 -3
- package/dist/{HandbookSectionInterface-OePKPbqN.d.ts → HandbookSectionInterface-Ck1wZJ81.d.ts} +1 -1
- package/dist/{HandbookSectionInterface-BYsmnPEz.d.mts → HandbookSectionInterface-DgYH--Ik.d.mts} +1 -1
- package/dist/JsonApiRequest-TE5WD3WS.js +26 -0
- package/dist/{JsonApiRequest-G625QAH5.js.map → JsonApiRequest-TE5WD3WS.js.map} +1 -1
- package/dist/{JsonApiRequest-RRMNV7YE.mjs → JsonApiRequest-UHOWNI3O.mjs} +2 -2
- package/dist/analytics.modules-CfczU12P.d.ts +25 -0
- package/dist/analytics.modules-pzGwMASI.d.mts +25 -0
- package/dist/{auth.interface-BC1jp_ni.d.ts → auth.interface-BzI4onI_.d.ts} +1 -1
- package/dist/{auth.interface-dR2-6gPM.d.mts → auth.interface-Df4vXbNz.d.mts} +1 -1
- package/dist/billing/index.js +402 -402
- package/dist/billing/index.mjs +4 -4
- package/dist/{chunk-YXRVPEEJ.js → chunk-26MQIJNB.js} +1 -1
- package/dist/{chunk-YXRVPEEJ.js.map → chunk-26MQIJNB.js.map} +1 -1
- package/dist/{chunk-T76WLDE5.mjs → chunk-37DMFVLN.mjs} +1 -1
- package/dist/{chunk-3AR4KA5D.js → chunk-4Z45NYPR.js} +13 -13
- package/dist/{chunk-3AR4KA5D.js.map → chunk-4Z45NYPR.js.map} +1 -1
- package/dist/chunk-7B4E42R5.mjs +280 -0
- package/dist/chunk-7B4E42R5.mjs.map +1 -0
- package/dist/{chunk-2XCUAHQT.js → chunk-ABVTYQIJ.js} +1563 -1336
- package/dist/chunk-ABVTYQIJ.js.map +1 -0
- package/dist/{chunk-U7QVISQS.js → chunk-CAKOFJZD.js} +450 -40
- package/dist/chunk-CAKOFJZD.js.map +1 -0
- package/dist/{chunk-IBVKPLHT.mjs → chunk-E3I4MT53.mjs} +422 -12
- package/dist/chunk-E3I4MT53.mjs.map +1 -0
- package/dist/chunk-GHFMKH3M.js +280 -0
- package/dist/chunk-GHFMKH3M.js.map +1 -0
- package/dist/chunk-GKDHYB2R.js +110 -0
- package/dist/chunk-GKDHYB2R.js.map +1 -0
- package/dist/{chunk-ZJI5ZZW5.mjs → chunk-Q6J2ICKO.mjs} +3 -3
- package/dist/{chunk-4O7R2Z2O.mjs → chunk-QWZAGQWJ.mjs} +2175 -1948
- package/dist/chunk-QWZAGQWJ.mjs.map +1 -0
- package/dist/chunk-U65JPKO3.mjs +110 -0
- package/dist/chunk-U65JPKO3.mjs.map +1 -0
- package/dist/client/index.d.mts +6 -5
- package/dist/client/index.d.ts +6 -5
- package/dist/client/index.js +5 -5
- package/dist/client/index.js.map +1 -1
- package/dist/client/index.mjs +4 -4
- package/dist/components/index.d.mts +33 -10
- package/dist/components/index.d.ts +33 -10
- package/dist/components/index.js +7 -5
- package/dist/components/index.js.map +1 -1
- package/dist/components/index.mjs +6 -4
- package/dist/{config-B6UB7j69.d.ts → config-3N1LSrcC.d.ts} +1 -1
- package/dist/{config-Cd5xTEfZ.d.ts → config-AVtuvfM1.d.ts} +1 -1
- package/dist/{config-n-ZpPmOL.d.mts → config-BSKTYDoh.d.mts} +1 -1
- package/dist/{config-YyhFHxkU.d.mts → config-BmufyEj6.d.mts} +1 -1
- package/dist/contexts/index.d.mts +5 -4
- package/dist/contexts/index.d.ts +5 -4
- package/dist/contexts/index.js +5 -5
- package/dist/contexts/index.mjs +4 -4
- package/dist/core/index.d.mts +31 -14
- package/dist/core/index.d.ts +31 -14
- package/dist/core/index.js +9 -5
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs +8 -4
- package/dist/features/analytics/index.css +30 -0
- package/dist/features/analytics/index.css.map +1 -0
- package/dist/features/analytics/index.d.mts +508 -0
- package/dist/features/analytics/index.d.ts +508 -0
- package/dist/features/analytics/index.js +1166 -0
- package/dist/features/analytics/index.js.map +1 -0
- package/dist/features/analytics/index.mjs +1166 -0
- package/dist/features/analytics/index.mjs.map +1 -0
- package/dist/features/analytics/server.d.mts +28 -0
- package/dist/features/analytics/server.d.ts +28 -0
- package/dist/features/analytics/server.js +41 -0
- package/dist/features/analytics/server.js.map +1 -0
- package/dist/features/analytics/server.mjs +41 -0
- package/dist/features/analytics/server.mjs.map +1 -0
- package/dist/features/help/index.d.mts +2 -2
- package/dist/features/help/index.d.ts +2 -2
- package/dist/features/help/index.js +43 -43
- package/dist/features/help/index.js.map +1 -1
- package/dist/features/help/index.mjs +9 -9
- package/dist/features/help/index.mjs.map +1 -1
- package/dist/features/tokenusage/index.d.mts +2 -2
- package/dist/features/tokenusage/index.d.ts +2 -2
- package/dist/features/tokenusage/index.js +141 -223
- package/dist/features/tokenusage/index.js.map +1 -1
- package/dist/features/tokenusage/index.mjs +28 -110
- package/dist/features/tokenusage/index.mjs.map +1 -1
- package/dist/index.d.mts +11 -9
- package/dist/index.d.ts +11 -9
- package/dist/index.js +10 -6
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +9 -5
- package/dist/notification.interface-BNCvnVCW.d.ts +34 -0
- package/dist/notification.interface-P_pNfzZQ.d.mts +34 -0
- package/dist/{s3.service-Bqr-fQmi.d.ts → s3.service-CbUa6a-8.d.ts} +3 -2
- package/dist/{s3.service-Buiy1ncj.d.mts → s3.service-DAp8FbuG.d.mts} +3 -2
- package/dist/server/index.d.mts +5 -4
- package/dist/server/index.d.ts +5 -4
- package/dist/server/index.js +39 -279
- package/dist/server/index.js.map +1 -1
- package/dist/server/index.mjs +20 -260
- package/dist/server/index.mjs.map +1 -1
- package/dist/{types-bsm8Fp55.d.ts → types-Cn153-mH.d.ts} +1 -1
- package/dist/{types-Bq_UA8Lg.d.mts → types-D0uLYV2P.d.mts} +1 -1
- package/dist/{usePageUrlGenerator-J_ECv-oQ.d.ts → usePageUrlGenerator-B-3FRKBD.d.ts} +1 -1
- package/dist/{usePageUrlGenerator-uKBjAKqC.d.mts → usePageUrlGenerator-CCJ_cdQy.d.mts} +1 -1
- package/dist/{useSocket-BcH7l7B8.d.mts → useSocket-B9gTbuCj.d.mts} +1 -1
- package/dist/{useSocket-DCZd_eJk.d.ts → useSocket-BHVIyigI.d.ts} +1 -1
- package/dist/{notification.interface-MnRt1Tjm.d.mts → user.interface-CSaGWFbg.d.mts} +1 -31
- package/dist/{notification.interface-CjzV_KII.d.ts → user.interface-CrmwJLYW.d.ts} +1 -31
- package/package.json +11 -1
- package/src/client/index.ts +2 -0
- package/src/components/containers/PageContainer.tsx +1 -1
- package/src/components/containers/RoundPageContainer.tsx +60 -16
- package/src/components/containers/RoundPageContainerTitle.tsx +75 -35
- package/src/components/editors/BlockNoteEditor.tsx +13 -1
- package/src/components/forms/EntityMultiSelector.tsx +4 -4
- package/src/components/forms/EntitySelector.tsx +4 -4
- package/src/components/forms/FormBlockNote.tsx +21 -1
- package/src/components/forms/FormCheckbox.tsx +6 -2
- package/src/components/forms/__tests__/useEditorDialog.test.ts +47 -0
- package/src/components/forms/useEditorDialog.ts +22 -0
- package/src/components/navigations/Breadcrumb.tsx +45 -16
- package/src/components/navigations/Header.tsx +16 -3
- package/src/components/navigations/MobileNavigationBar.tsx +12 -0
- package/src/components/navigations/__tests__/MobileNavigationBar.spec.tsx +17 -0
- package/src/components/tables/ContentListTable.tsx +52 -32
- package/src/core/index.ts +4 -0
- package/src/core/registry/ModuleRegistry.ts +7 -0
- package/src/features/__tests__/i18n-keys.contract.spec.ts +1 -0
- package/src/features/analytics/__tests__/server-entry.spec.tsx +64 -0
- package/src/features/analytics/analytics.modules.ts +30 -0
- package/src/features/analytics/components/AnalyticsAdminContainer.tsx +97 -0
- package/src/features/analytics/components/AnalyticsAdminFilterBar.tsx +120 -0
- package/src/features/analytics/components/AnalyticsAdminPageContainer.tsx +25 -0
- package/src/features/analytics/components/AnalyticsAdminTiles.tsx +191 -0
- package/src/features/analytics/components/AnalyticsBreakdownTable.tsx +105 -0
- package/src/features/analytics/components/AnalyticsJourneyContainer.tsx +246 -0
- package/src/features/analytics/components/AnalyticsJourneyPageContainer.tsx +14 -0
- package/src/features/analytics/components/AnalyticsSessionsTable.tsx +107 -0
- package/src/features/analytics/components/AnalyticsTimelineChart.tsx +175 -0
- package/src/features/analytics/components/AnalyticsTracker.tsx +112 -0
- package/src/features/analytics/components/__tests__/AnalyticsAdminTiles.spec.tsx +39 -0
- package/src/features/analytics/components/__tests__/AnalyticsBreakdownTable.spec.tsx +39 -0
- package/src/features/analytics/components/__tests__/AnalyticsJourneyContainer.spec.tsx +124 -0
- package/src/features/analytics/components/__tests__/AnalyticsPageContainers.spec.tsx +117 -0
- package/src/features/analytics/components/__tests__/AnalyticsSessionsTable.spec.tsx +121 -0
- package/src/features/analytics/components/__tests__/AnalyticsTimelineChart.spec.tsx +35 -0
- package/src/features/analytics/components/__tests__/AnalyticsTracker.spec.tsx +248 -0
- package/src/features/analytics/components/server/AnalyticsAdminPage.tsx +28 -0
- package/src/features/analytics/components/server/AnalyticsJourneyPage.tsx +25 -0
- package/src/features/analytics/contexts/AnalyticsAdminContext.tsx +273 -0
- package/src/features/analytics/contexts/__tests__/AnalyticsAdminContext.spec.tsx +140 -0
- package/src/features/analytics/data/AnalyticsAdminService.ts +91 -0
- package/src/features/analytics/data/AnalyticsService.ts +23 -0
- package/src/features/analytics/data/__tests__/AnalyticsAdminService.spec.ts +117 -0
- package/src/features/analytics/data/__tests__/AnalyticsService.spec.ts +56 -0
- package/src/features/analytics/data/__tests__/models.spec.ts +300 -0
- package/src/features/analytics/data/analytics-breakdown.interface.ts +9 -0
- package/src/features/analytics/data/analytics-breakdown.ts +65 -0
- package/src/features/analytics/data/analytics-event.interface.ts +7 -0
- package/src/features/analytics/data/analytics-event.ts +63 -0
- package/src/features/analytics/data/analytics-page-view.interface.ts +9 -0
- package/src/features/analytics/data/analytics-page-view.ts +57 -0
- package/src/features/analytics/data/analytics-session.interface.ts +23 -0
- package/src/features/analytics/data/analytics-session.ts +144 -0
- package/src/features/analytics/data/analytics-summary.interface.ts +11 -0
- package/src/features/analytics/data/analytics-summary.ts +79 -0
- package/src/features/analytics/data/analytics-timeline.interface.ts +10 -0
- package/src/features/analytics/data/analytics-timeline.ts +67 -0
- package/src/features/analytics/data/analytics.types.ts +97 -0
- package/src/features/analytics/data/index.ts +15 -0
- package/src/features/analytics/i18n-keys.ts +76 -0
- package/src/features/analytics/index.ts +31 -0
- package/src/features/analytics/lib/__tests__/utm.spec.ts +20 -0
- package/src/features/analytics/lib/__tests__/visitorCookie.spec.ts +63 -0
- package/src/features/analytics/lib/utm.ts +29 -0
- package/src/features/analytics/lib/visitorCookie.ts +32 -0
- package/src/features/analytics/server-entry.ts +8 -0
- package/src/features/assistant/components/parts/AssistantBlockNoteComposer.tsx +1 -1
- package/src/features/assistant/components/parts/AssistantComposer.tsx +1 -1
- package/src/features/assistant-message/components/MessageItem.tsx +2 -2
- package/src/features/assistant-message/components/parts/tabs/CitationsTab.tsx +23 -5
- package/src/features/auth/components/containers/AuthContainer.tsx +8 -1
- package/src/features/company/components/details/CompanyContent.tsx +3 -1
- package/src/features/company/components/details/CompanyDetails.tsx +9 -1
- package/src/features/company/components/details/TokenStatusIndicator.tsx +42 -13
- package/src/features/help/components/HelpHeader.tsx +7 -5
- package/src/features/tokenusage/components/TokenUsageAdminFilterBar.tsx +9 -6
- package/src/features/tokenusage/lib/__tests__/formatters.spec.ts +21 -0
- package/src/features/tokenusage/lib/formatters.ts +5 -1
- package/src/features/waitlist/components/lists/WaitlistList.tsx +68 -121
- package/src/features/waitlist/data/WaitlistService.ts +2 -2
- package/src/features/waitlist/data/index.ts +1 -0
- package/src/features/waitlist/data/waitlist.fields.ts +8 -0
- package/src/features/waitlist/hooks/useWaitlistTableStructure.tsx +69 -30
- package/src/shadcnui/ui/button.tsx +1 -1
- package/src/shadcnui/ui/checkbox.tsx +1 -1
- package/src/shadcnui/ui/command.tsx +4 -4
- package/src/shadcnui/ui/dialog.tsx +2 -0
- package/src/shadcnui/ui/input.tsx +1 -1
- package/src/shadcnui/ui/popover.tsx +9 -0
- package/src/shadcnui/ui/select.tsx +1 -1
- package/src/shadcnui/ui/sheet.tsx +8 -1
- package/src/shadcnui/ui/table.tsx +2 -2
- package/src/shadcnui/ui/textarea.tsx +1 -1
- package/dist/BlockNoteEditor-35D4R7HJ.js.map +0 -1
- package/dist/JsonApiRequest-G625QAH5.js +0 -26
- package/dist/chunk-2XCUAHQT.js.map +0 -1
- package/dist/chunk-4O7R2Z2O.mjs.map +0 -1
- package/dist/chunk-IBVKPLHT.mjs.map +0 -1
- package/dist/chunk-U7QVISQS.js.map +0 -1
- /package/dist/{BlockNoteViewer-KDKGUIG5.mjs.map → BlockNoteViewer-KRTAFTUX.mjs.map} +0 -0
- /package/dist/{JsonApiRequest-RRMNV7YE.mjs.map → JsonApiRequest-UHOWNI3O.mjs.map} +0 -0
- /package/dist/{chunk-T76WLDE5.mjs.map → chunk-37DMFVLN.mjs.map} +0 -0
- /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
|
+
}
|