@nodewrite-labs/analytics-sdk 0.1.0 → 0.3.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.
@@ -1,289 +1,722 @@
1
1
  import {
2
+ ANALYTICS_EVENTS_SLUG,
3
+ ANALYTICS_FILTERS_PATH,
2
4
  PayloadAnalyticsEventStore,
3
- adminSections,
4
- analyticsModulesFromConfig
5
- } from "../chunk-GVOESWHZ.js";
6
- import "../chunk-Q4JRXZ6M.js";
7
- import "../chunk-F7BY2JQ3.js";
5
+ analyticsAdminSettingsFromConfig,
6
+ analyticsEventLabel,
7
+ analyticsLabel
8
+ } from "../chunk-QPHMFHRE.js";
9
+ import {
10
+ ANALYTICS_PERIODS,
11
+ AnalyticsBucketUnit,
12
+ DEFAULT_ANALYTICS_PERIOD,
13
+ analyticsPeriodRange,
14
+ isAnalyticsPeriod
15
+ } from "../chunk-MOCOVYQK.js";
16
+ import "../chunk-YAC3MRH3.js";
17
+ import {
18
+ AnalyticsEventKind
19
+ } from "../chunk-F7BY2JQ3.js";
8
20
 
9
21
  // src/payload/admin/view.tsx
10
22
  import { DefaultTemplate } from "@payloadcms/next/templates";
11
- import { jsx, jsxs } from "react/jsx-runtime";
12
- var WEEK_MS = 7 * 24 * 60 * 60 * 1e3;
13
- var reportStyle = `
14
- .analytics-report {
15
- box-sizing: border-box;
23
+ import { Gutter } from "@payloadcms/ui";
24
+ import { RenderServerComponent } from "@payloadcms/ui/elements/RenderServerComponent";
25
+ import { notFound } from "next/navigation.js";
26
+
27
+ // src/payload/admin/format.ts
28
+ var FALLBACK_LOCALE = "en";
29
+ var SECONDS_PER_MINUTE = 60;
30
+ function resolveFormatLocale(language, dateLocale, formatLocales) {
31
+ for (const candidate of [formatLocales[language], dateLocale, language]) {
32
+ if (candidate && isKnownLocale(candidate)) return candidate;
33
+ }
34
+ return FALLBACK_LOCALE;
35
+ }
36
+ function isKnownLocale(tag) {
37
+ try {
38
+ return Intl.DateTimeFormat.supportedLocalesOf([tag]).length > 0;
39
+ } catch {
40
+ return false;
41
+ }
42
+ }
43
+ function reportFormat(locale, timeZone, unit) {
44
+ const numbers = new Intl.NumberFormat(locale);
45
+ const seconds = new Intl.NumberFormat(locale, { style: "unit", unit: "second", unitDisplay: "short" });
46
+ const minutes = new Intl.NumberFormat(locale, { style: "unit", unit: "minute", unitDisplay: "short" });
47
+ const buckets = new Intl.DateTimeFormat(locale, { timeZone, ...BUCKET_FORMAT[unit] });
48
+ const instants = new Intl.DateTimeFormat(locale, { timeZone, dateStyle: "short", timeStyle: "short" });
49
+ const regions = new Intl.DisplayNames([locale], { type: "region", fallback: "code" });
50
+ return {
51
+ number: (value) => numbers.format(value),
52
+ duration: (value) => {
53
+ const total = Number.isFinite(value) ? Math.round(value) : 0;
54
+ if (total < SECONDS_PER_MINUTE) return seconds.format(total);
55
+ const rest = total % SECONDS_PER_MINUTE;
56
+ const whole = minutes.format(Math.floor(total / SECONDS_PER_MINUTE));
57
+ return rest === 0 ? whole : `${whole} ${seconds.format(rest)}`;
58
+ },
59
+ bucket: (start) => buckets.format(new Date(start)),
60
+ dateTime: (instant) => instants.format(new Date(instant)),
61
+ country: (code) => {
62
+ try {
63
+ return regions.of(code) ?? code;
64
+ } catch {
65
+ return code;
66
+ }
67
+ }
68
+ };
69
+ }
70
+ var BUCKET_FORMAT = {
71
+ [AnalyticsBucketUnit.HOUR]: { hour: "2-digit", minute: "2-digit" },
72
+ [AnalyticsBucketUnit.DAY]: { day: "numeric", month: "short" },
73
+ [AnalyticsBucketUnit.MONTH]: { month: "short", year: "numeric" }
74
+ };
75
+
76
+ // src/payload/admin/report.ts
77
+ var TOP_PAGES_COUNT = 5;
78
+ function buildAnalyticsReport({
79
+ summary,
80
+ periodSummary,
81
+ modules,
82
+ query
83
+ }) {
84
+ const pages = withShares(byViews(summary.byPath, (row) => row.path));
85
+ const clicks = modules.clicks ? [...summary.byEventName ?? []].sort(
86
+ (left, right) => right.count - left.count || compare(left.eventName, right.eventName)
87
+ ) : null;
88
+ return {
89
+ totals: {
90
+ views: summary.totalViews,
91
+ averageDurationSeconds: summary.averageDurationSeconds,
92
+ clicks: clicks ? clicks.reduce((total, row) => total + row.count, 0) : null
93
+ },
94
+ viewsOverTime: summary.viewsOverTime ?? [],
95
+ pages: query.showAllPages ? pages : pages.slice(0, TOP_PAGES_COUNT),
96
+ hasMorePages: pages.length > TOP_PAGES_COUNT,
97
+ countries: modules.country ? withShares(byViews(summary.byCountry ?? [], (row) => row.country)) : null,
98
+ countryOptions: modules.country ? byViews(periodSummary.byCountry ?? [], (row) => row.country).map((row) => row.country) : [],
99
+ cities: modules.city ? withShares(byViews(summary.byCity ?? [], (row) => row.city)) : null,
100
+ clicks,
101
+ sessions: modules.sessions && query.session === null ? summary.sessions ?? [] : null,
102
+ trail: modules.sessions && query.session !== null ? summary.events ?? [] : null
103
+ };
104
+ }
105
+ function byViews(rows, name) {
106
+ return [...rows].sort((left, right) => right.views - left.views || compare(name(left), name(right)));
107
+ }
108
+ function withShares(rows) {
109
+ const most = rows[0]?.views ?? 0;
110
+ return rows.map((row) => ({ ...row, share: most === 0 ? 0 : row.views / most }));
111
+ }
112
+ function compare(left, right) {
113
+ if (left < right) return -1;
114
+ if (left > right) return 1;
115
+ return 0;
116
+ }
117
+
118
+ // src/payload/admin/report-query.ts
119
+ var ReportParam = {
120
+ PERIOD: "period",
121
+ COUNTRY: "country",
122
+ PATH: "path",
123
+ SESSION: "session",
124
+ PAGES: "pages"
125
+ };
126
+ var ALL_PAGES = "all";
127
+ var COUNTRY_CODE = /^[A-Z]{2}$/;
128
+ var PATH_MAX = 2048;
129
+ function parseReportQuery(searchParams, modules) {
130
+ const period = firstParam(searchParams, ReportParam.PERIOD);
131
+ const country = firstParam(searchParams, ReportParam.COUNTRY)?.toUpperCase() ?? null;
132
+ const path = firstParam(searchParams, ReportParam.PATH);
133
+ return {
134
+ period: isAnalyticsPeriod(period) ? period : DEFAULT_ANALYTICS_PERIOD,
135
+ country: modules.country && country !== null && COUNTRY_CODE.test(country) ? country : null,
136
+ path: path !== null && path.startsWith("/") && path.length <= PATH_MAX ? path : null,
137
+ session: modules.sessions ? firstParam(searchParams, ReportParam.SESSION) : null,
138
+ showAllPages: firstParam(searchParams, ReportParam.PAGES) === ALL_PAGES
139
+ };
140
+ }
141
+ function reportHref(viewHref, query, changes = {}) {
142
+ const next = { ...query, ...changes };
143
+ const params = new URLSearchParams();
144
+ if (next.period !== DEFAULT_ANALYTICS_PERIOD) params.set(ReportParam.PERIOD, next.period);
145
+ if (next.country) params.set(ReportParam.COUNTRY, next.country);
146
+ if (next.path) params.set(ReportParam.PATH, next.path);
147
+ if (next.session) params.set(ReportParam.SESSION, next.session);
148
+ if (next.showAllPages) params.set(ReportParam.PAGES, ALL_PAGES);
149
+ const search = params.toString();
150
+ return search.length === 0 ? viewHref : `${viewHref}?${search}`;
151
+ }
152
+ function firstParam(searchParams, name) {
153
+ const value = searchParams?.[name];
154
+ const first = Array.isArray(value) ? value[0] : value;
155
+ if (typeof first !== "string") return null;
156
+ const trimmed = first.trim();
157
+ return trimmed.length === 0 ? null : trimmed;
158
+ }
159
+
160
+ // src/payload/admin/country-flag.tsx
161
+ import { hasFlag } from "country-flag-icons";
162
+ import * as flags from "country-flag-icons/string/3x2";
163
+ import { jsx } from "react/jsx-runtime";
164
+ var FLAG_SVGS = flags;
165
+ function CountryFlag({ code }) {
166
+ const svg = hasFlag(code) ? FLAG_SVGS[code] : void 0;
167
+ if (!svg) return /* @__PURE__ */ jsx("span", { className: "nwa-flag nwa-flag--none", "aria-hidden": "true" });
168
+ return /* @__PURE__ */ jsx(
169
+ "img",
170
+ {
171
+ className: "nwa-flag",
172
+ src: `data:image/svg+xml;utf8,${encodeURIComponent(svg)}`,
173
+ alt: "",
174
+ width: 21,
175
+ height: 14
176
+ }
177
+ );
178
+ }
179
+
180
+ // src/payload/admin/report-tables.tsx
181
+ import { Fragment, jsx as jsx2, jsxs } from "react/jsx-runtime";
182
+ function RankedTable({ title, columns, rows, empty, footer }) {
183
+ const [nameColumn, ...valueColumns] = columns;
184
+ return /* @__PURE__ */ jsxs("section", { className: "nwa-card", children: [
185
+ /* @__PURE__ */ jsx2("h2", { children: title }),
186
+ rows.length === 0 ? /* @__PURE__ */ jsx2("p", { className: "nwa-empty", children: empty }) : /* @__PURE__ */ jsxs("table", { className: "nwa-table", children: [
187
+ /* @__PURE__ */ jsx2("thead", { children: /* @__PURE__ */ jsxs("tr", { children: [
188
+ /* @__PURE__ */ jsx2("th", { scope: "col", children: nameColumn }),
189
+ valueColumns.map((column) => /* @__PURE__ */ jsx2("th", { className: "nwa-number", scope: "col", children: column }, column))
190
+ ] }) }),
191
+ /* @__PURE__ */ jsx2("tbody", { children: rows.map((row) => /* @__PURE__ */ jsxs("tr", { children: [
192
+ /* @__PURE__ */ jsx2(
193
+ "td",
194
+ {
195
+ className: row.share === void 0 ? void 0 : "nwa-bar",
196
+ style: row.share === void 0 ? void 0 : { "--nwa-share": row.share },
197
+ children: row.href ? /* @__PURE__ */ jsx2("a", { className: "nwa-name", href: row.href, children: row.name }) : /* @__PURE__ */ jsx2("span", { className: "nwa-name", children: row.name })
198
+ }
199
+ ),
200
+ row.values.map((value, index) => /* @__PURE__ */ jsx2("td", { className: "nwa-number", children: value }, `${row.key}-${valueColumns[index] ?? index}`))
201
+ ] }, row.key)) })
202
+ ] }),
203
+ footer
204
+ ] });
205
+ }
206
+ function PagesTable({
207
+ report,
208
+ label,
209
+ format,
210
+ pageHref,
211
+ toggleHref,
212
+ showAll
213
+ }) {
214
+ return /* @__PURE__ */ jsx2(
215
+ RankedTable,
216
+ {
217
+ title: label(showAll ? "allPages" : "topPages"),
218
+ columns: [label("page"), label("views"), label("averageTime")],
219
+ empty: label("empty"),
220
+ rows: report.pages.map((page) => ({
221
+ key: page.path,
222
+ name: page.path,
223
+ href: pageHref(page.path),
224
+ share: page.share,
225
+ values: [format.number(page.views), format.duration(page.averageDurationSeconds)]
226
+ })),
227
+ footer: report.hasMorePages ? /* @__PURE__ */ jsx2("a", { className: "nwa-more", href: toggleHref, children: label(showAll ? "showTopPages" : "showAllPages") }) : null
228
+ }
229
+ );
230
+ }
231
+ function CountriesTable({
232
+ report,
233
+ label,
234
+ format,
235
+ countryHref
236
+ }) {
237
+ return /* @__PURE__ */ jsx2(
238
+ RankedTable,
239
+ {
240
+ title: label("countries"),
241
+ columns: [label("country"), label("views")],
242
+ empty: label("empty"),
243
+ rows: (report.countries ?? []).map((row) => ({
244
+ key: row.country,
245
+ name: /* @__PURE__ */ jsxs(Fragment, { children: [
246
+ /* @__PURE__ */ jsx2(CountryFlag, { code: row.country }),
247
+ format.country(row.country)
248
+ ] }),
249
+ href: countryHref(row.country),
250
+ share: row.share,
251
+ values: [format.number(row.views)]
252
+ }))
253
+ }
254
+ );
255
+ }
256
+ function ClicksTable({
257
+ report,
258
+ label,
259
+ format,
260
+ eventLabel
261
+ }) {
262
+ return /* @__PURE__ */ jsx2(
263
+ RankedTable,
264
+ {
265
+ title: label("clicks"),
266
+ columns: [label("event"), label("count")],
267
+ empty: label("empty"),
268
+ rows: (report.clicks ?? []).map((row) => ({
269
+ key: row.eventName,
270
+ name: eventLabel(row.eventName),
271
+ values: [format.number(row.count)]
272
+ }))
273
+ }
274
+ );
275
+ }
276
+ function CitiesTable({ report, label, format }) {
277
+ return /* @__PURE__ */ jsx2(
278
+ RankedTable,
279
+ {
280
+ title: label("cities"),
281
+ columns: [label("city"), label("views")],
282
+ empty: label("empty"),
283
+ rows: (report.cities ?? []).map((row) => ({
284
+ key: row.city,
285
+ name: row.city,
286
+ share: row.share,
287
+ values: [format.number(row.views)]
288
+ }))
289
+ }
290
+ );
291
+ }
292
+
293
+ // src/payload/admin/report-sessions.tsx
294
+ import { jsx as jsx3 } from "react/jsx-runtime";
295
+ function SessionsSection({ report, label, format, sessionHref, allSessionsHref, eventLabel }) {
296
+ if (report.trail) {
297
+ return /* @__PURE__ */ jsx3(
298
+ RankedTable,
299
+ {
300
+ title: label("sessionEvents"),
301
+ columns: [label("time"), label("kind"), label("page"), label("event")],
302
+ empty: label("empty"),
303
+ rows: report.trail.map((event) => ({
304
+ key: event.id,
305
+ name: format.dateTime(event.occurredAt),
306
+ values: [
307
+ event.kind === AnalyticsEventKind.CLICK ? label("kindClick") : label("kindView"),
308
+ event.path,
309
+ event.eventName ? eventLabel(event.eventName) : ""
310
+ ]
311
+ })),
312
+ footer: /* @__PURE__ */ jsx3("a", { className: "nwa-more", href: allSessionsHref, children: label("allSessions") })
313
+ }
314
+ );
315
+ }
316
+ return /* @__PURE__ */ jsx3(
317
+ RankedTable,
318
+ {
319
+ title: label("sessions"),
320
+ columns: [label("session"), label("started"), label("pages")],
321
+ empty: label("empty"),
322
+ rows: (report.sessions ?? []).map((session) => ({
323
+ key: session.id,
324
+ name: session.id,
325
+ href: sessionHref(session.id),
326
+ values: [format.dateTime(session.startedAt), format.number(session.pageCount)]
327
+ }))
328
+ }
329
+ );
330
+ }
331
+
332
+ // src/payload/admin/report-style.ts
333
+ var REPORT_STYLE = `
334
+ .nwa-report {
16
335
  display: grid;
17
- gap: 1.5rem;
18
- max-width: 72rem;
19
- padding: 1.5rem 1rem 3rem;
20
- color: inherit;
336
+ gap: calc(var(--base) * 1.2);
337
+ padding-block: calc(var(--base) * 1.5) calc(var(--base) * 3);
21
338
  }
22
- .analytics-report h1,
23
- .analytics-report h2 {
339
+ .nwa-header {
340
+ display: flex;
341
+ flex-wrap: wrap;
342
+ gap: var(--base);
343
+ align-items: flex-end;
344
+ justify-content: space-between;
345
+ }
346
+ .nwa-header h1 {
24
347
  margin: 0;
25
348
  }
26
- .analytics-report a {
27
- color: inherit;
349
+ .nwa-filters {
350
+ display: flex;
351
+ flex-wrap: wrap;
352
+ gap: calc(var(--base) * 0.6);
28
353
  }
29
- .analytics-filters {
354
+ .nwa-filter {
30
355
  display: grid;
31
- grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr));
32
- gap: 0.75rem;
33
- align-items: end;
356
+ gap: calc(var(--base) * 0.25);
357
+ min-width: 12rem;
34
358
  }
35
- .analytics-filters label {
36
- display: grid;
37
- gap: 0.25rem;
359
+ .nwa-filter__label {
360
+ color: var(--theme-elevation-500);
361
+ font-size: 0.8125rem;
362
+ }
363
+ .nwa-chips {
364
+ display: flex;
365
+ flex-wrap: wrap;
366
+ gap: calc(var(--base) * 0.4);
367
+ }
368
+ .nwa-chip {
369
+ display: inline-flex;
370
+ gap: calc(var(--base) * 0.4);
371
+ align-items: center;
372
+ padding: calc(var(--base) * 0.2) calc(var(--base) * 0.6);
373
+ border-radius: var(--style-radius-l);
374
+ background: var(--theme-elevation-100);
38
375
  }
39
- .analytics-filters input,
40
- .analytics-filters button {
41
- font: inherit;
42
- min-height: 2.5rem;
43
- padding: 0.4rem 0.6rem;
376
+ .nwa-chip a {
377
+ color: var(--theme-elevation-800);
44
378
  }
45
- .analytics-totals {
379
+ .nwa-cards {
46
380
  display: grid;
47
381
  grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr));
48
- gap: 0.75rem;
49
- margin: 0.75rem 0 0;
382
+ gap: calc(var(--base) * 0.8);
383
+ }
384
+ .nwa-card {
385
+ min-width: 0;
386
+ padding: var(--base);
387
+ border: 1px solid var(--theme-elevation-100);
388
+ border-radius: var(--style-radius-m);
389
+ background: var(--theme-elevation-0);
390
+ }
391
+ .nwa-card h2 {
392
+ margin: 0 0 calc(var(--base) * 0.6);
393
+ font-size: 1rem;
50
394
  }
51
- .analytics-scroll {
52
- overflow-x: auto;
53
- margin-top: 0.75rem;
395
+ .nwa-stat dt {
396
+ color: var(--theme-elevation-500);
397
+ font-size: 0.8125rem;
54
398
  }
55
- .analytics-report table {
399
+ .nwa-stat dd {
400
+ margin: calc(var(--base) * 0.25) 0 0;
401
+ font-size: 1.75rem;
402
+ font-weight: 600;
403
+ font-variant-numeric: tabular-nums;
404
+ }
405
+ .nwa-columns {
406
+ display: grid;
407
+ grid-template-columns: repeat(auto-fit, minmax(20rem, 1fr));
408
+ gap: calc(var(--base) * 0.8);
409
+ }
410
+ .nwa-chart {
411
+ display: block;
412
+ width: 100%;
413
+ height: auto;
414
+ }
415
+ .nwa-chart__grid {
416
+ stroke: var(--theme-elevation-100);
417
+ }
418
+ .nwa-chart__axis {
419
+ fill: var(--theme-elevation-500);
420
+ font-size: 11px;
421
+ }
422
+ .nwa-chart__line {
423
+ fill: none;
424
+ stroke: var(--theme-success-500);
425
+ stroke-width: 2.5;
426
+ stroke-linejoin: round;
427
+ stroke-linecap: round;
428
+ }
429
+ .nwa-chart__area {
430
+ fill: var(--theme-success-500);
431
+ opacity: 0.12;
432
+ }
433
+ .nwa-chart__point {
434
+ fill: var(--theme-elevation-0);
435
+ stroke: var(--theme-success-500);
436
+ stroke-width: 2;
437
+ }
438
+ .nwa-table {
56
439
  width: 100%;
57
440
  border-collapse: collapse;
441
+ font-variant-numeric: tabular-nums;
58
442
  }
59
- .analytics-report th,
60
- .analytics-report td {
443
+ .nwa-table th {
444
+ padding: calc(var(--base) * 0.3) calc(var(--base) * 0.4);
445
+ color: var(--theme-elevation-500);
446
+ font-size: 0.8125rem;
447
+ font-weight: 400;
61
448
  text-align: start;
62
- padding: 0.5rem 0.75rem;
63
- border-bottom: 1px solid currentColor;
64
- vertical-align: top;
65
449
  }
66
- .analytics-back {
67
- justify-self: start;
450
+ .nwa-table td {
451
+ padding: calc(var(--base) * 0.4);
452
+ border-top: 1px solid var(--theme-elevation-100);
68
453
  }
69
- @media (max-width: 40rem) {
70
- .analytics-report {
71
- padding: 1rem 0.75rem 2rem;
72
- }
454
+ .nwa-table .nwa-number {
455
+ text-align: end;
456
+ white-space: nowrap;
73
457
  }
74
- `;
75
- async function AnalyticsAdminView(props) {
76
- if (!props.initPageResult.req.user) return null;
77
- const modules = analyticsModulesFromConfig(props.payload.config);
78
- const range = resolveRange(props.searchParams);
79
- const country = modules.country ? firstParam(props.searchParams?.country) : null;
80
- const path = firstParam(props.searchParams?.path);
81
- const session = modules.sessions ? firstParam(props.searchParams?.session) : null;
82
- const store = new PayloadAnalyticsEventStore(props.payload, modules);
83
- const summary = await store.summarizeAnalytics(
84
- {
85
- from: range.from,
86
- to: range.to,
87
- country,
88
- path,
89
- sessionId: session
90
- },
91
- modules
92
- );
93
- const sections = adminSections(summary, modules, { session });
94
- const visibleEntities = props.visibleEntities ?? props.initPageResult.visibleEntities;
95
- return /* @__PURE__ */ jsx(
96
- DefaultTemplate,
97
- {
98
- i18n: props.i18n,
99
- payload: props.payload,
100
- req: props.initPageResult.req,
101
- viewType: props.viewType,
102
- visibleEntities,
103
- children: /* @__PURE__ */ jsxs("div", { className: "analytics-report", children: [
104
- /* @__PURE__ */ jsx("style", { children: reportStyle }),
105
- /* @__PURE__ */ jsx("h1", { children: "Analytics" }),
106
- /* @__PURE__ */ jsx(
107
- FilterForm,
108
- {
109
- from: range.from,
110
- to: range.to,
111
- country,
112
- path,
113
- session,
114
- modules
115
- }
116
- ),
117
- session ? /* @__PURE__ */ jsx("a", { className: "analytics-back", href: reportHref({ from: range.from, to: range.to, country, path }), children: "All sessions" }) : null,
118
- sections.map((section) => /* @__PURE__ */ jsx(
119
- SectionBlock,
120
- {
121
- section,
122
- hrefForSession: (id) => reportHref({ from: range.from, to: range.to, country, path, session: id })
123
- },
124
- section.kind
125
- ))
126
- ] })
127
- }
128
- );
458
+ .nwa-bar {
459
+ position: relative;
129
460
  }
130
- function FilterForm({
131
- from,
132
- to,
133
- country,
134
- path,
135
- session,
136
- modules
137
- }) {
138
- return /* @__PURE__ */ jsxs("form", { className: "analytics-filters", method: "get", action: "/admin/analytics", children: [
139
- /* @__PURE__ */ jsxs("label", { children: [
140
- "From",
141
- /* @__PURE__ */ jsx("input", { name: "from", type: "text", defaultValue: from, autoComplete: "off" })
142
- ] }),
143
- /* @__PURE__ */ jsxs("label", { children: [
144
- "To",
145
- /* @__PURE__ */ jsx("input", { name: "to", type: "text", defaultValue: to, autoComplete: "off" })
146
- ] }),
147
- modules.country ? /* @__PURE__ */ jsxs("label", { children: [
148
- "Country",
149
- /* @__PURE__ */ jsx("input", { name: "country", type: "text", defaultValue: country ?? "", maxLength: 2, autoComplete: "off" })
150
- ] }) : null,
151
- /* @__PURE__ */ jsxs("label", { children: [
152
- "Path",
153
- /* @__PURE__ */ jsx("input", { name: "path", type: "text", defaultValue: path ?? "", maxLength: 2048, autoComplete: "off" })
154
- ] }),
155
- session ? /* @__PURE__ */ jsx("input", { type: "hidden", name: "session", value: session }) : null,
156
- /* @__PURE__ */ jsx("button", { type: "submit", children: "Apply" })
461
+ .nwa-bar::before {
462
+ content: "";
463
+ position: absolute;
464
+ inset: 3px auto 3px 0;
465
+ width: calc(var(--nwa-share) * 100%);
466
+ border-radius: var(--style-radius-s);
467
+ background: var(--theme-success-500);
468
+ opacity: 0.14;
469
+ }
470
+ .nwa-bar > * {
471
+ position: relative;
472
+ }
473
+ .nwa-name {
474
+ display: inline-flex;
475
+ gap: calc(var(--base) * 0.5);
476
+ align-items: center;
477
+ max-width: 100%;
478
+ overflow-wrap: anywhere;
479
+ color: var(--theme-elevation-800);
480
+ text-decoration: none;
481
+ }
482
+ a.nwa-name:hover {
483
+ text-decoration: underline;
484
+ }
485
+ .nwa-flag {
486
+ flex: none;
487
+ width: 21px;
488
+ height: 14px;
489
+ border-radius: 2px;
490
+ box-shadow: 0 0 0 1px var(--theme-elevation-100);
491
+ }
492
+ .nwa-flag--none {
493
+ display: inline-block;
494
+ background: var(--theme-elevation-100);
495
+ }
496
+ .nwa-more {
497
+ display: inline-block;
498
+ margin-top: calc(var(--base) * 0.6);
499
+ color: var(--theme-elevation-800);
500
+ }
501
+ .nwa-empty {
502
+ margin: 0;
503
+ color: var(--theme-elevation-500);
504
+ }
505
+ `;
506
+
507
+ // src/payload/admin/views-chart.tsx
508
+ import { jsx as jsx4, jsxs as jsxs2 } from "react/jsx-runtime";
509
+ var WIDTH = 720;
510
+ var HEIGHT = 220;
511
+ var PAD_LEFT = 40;
512
+ var PAD_RIGHT = 12;
513
+ var PAD_TOP = 12;
514
+ var PAD_BOTTOM = 28;
515
+ var GRID_LINES = 4;
516
+ var MAX_AXIS_LABELS = 8;
517
+ function ViewsChart({ points, formatNumber, description }) {
518
+ const top = niceCeiling(Math.max(0, ...points.map((point) => point.views)));
519
+ const plotWidth = WIDTH - PAD_LEFT - PAD_RIGHT;
520
+ const plotHeight = HEIGHT - PAD_TOP - PAD_BOTTOM;
521
+ const step = points.length > 1 ? plotWidth / (points.length - 1) : 0;
522
+ const x = (index) => PAD_LEFT + (points.length > 1 ? index * step : plotWidth / 2);
523
+ const y = (views) => PAD_TOP + plotHeight - views / top * plotHeight;
524
+ const line = points.map((point, index) => `${index === 0 ? "M" : "L"}${x(index)},${y(point.views)}`).join(" ");
525
+ const area = `${line} L${x(points.length - 1)},${PAD_TOP + plotHeight} L${x(0)},${PAD_TOP + plotHeight} Z`;
526
+ const labelEvery = Math.max(1, Math.ceil(points.length / MAX_AXIS_LABELS));
527
+ return /* @__PURE__ */ jsxs2("svg", { className: "nwa-chart", viewBox: `0 0 ${WIDTH} ${HEIGHT}`, role: "img", "aria-label": description, children: [
528
+ gridValues(top).map((value) => /* @__PURE__ */ jsxs2("g", { children: [
529
+ /* @__PURE__ */ jsx4("line", { className: "nwa-chart__grid", x1: PAD_LEFT, x2: WIDTH - PAD_RIGHT, y1: y(value), y2: y(value) }),
530
+ /* @__PURE__ */ jsx4("text", { className: "nwa-chart__axis", x: PAD_LEFT - 8, y: y(value), dy: "0.32em", textAnchor: "end", children: formatNumber(value) })
531
+ ] }, value)),
532
+ points.length > 0 ? /* @__PURE__ */ jsx4("path", { className: "nwa-chart__area", d: area }) : null,
533
+ points.length > 0 ? /* @__PURE__ */ jsx4("path", { className: "nwa-chart__line", d: line }) : null,
534
+ points.map((point, index) => /* @__PURE__ */ jsxs2("g", { children: [
535
+ /* @__PURE__ */ jsx4("circle", { className: "nwa-chart__point", cx: x(index), cy: y(point.views), r: points.length > 31 ? 2 : 3.5, children: /* @__PURE__ */ jsx4("title", { children: point.title }) }),
536
+ index % labelEvery === 0 ? /* @__PURE__ */ jsx4("text", { className: "nwa-chart__axis", x: x(index), y: HEIGHT - 8, textAnchor: "middle", children: point.label }) : null
537
+ ] }, `${point.label}-${index}`))
157
538
  ] });
158
539
  }
159
- function SectionBlock({
160
- section,
161
- hrefForSession
162
- }) {
163
- switch (section.kind) {
164
- case "totals":
165
- return /* @__PURE__ */ jsxs("section", { children: [
166
- /* @__PURE__ */ jsx("h2", { children: "Totals" }),
167
- /* @__PURE__ */ jsxs("dl", { className: "analytics-totals", children: [
168
- /* @__PURE__ */ jsxs("div", { children: [
169
- /* @__PURE__ */ jsx("dt", { children: "Total views" }),
170
- /* @__PURE__ */ jsx("dd", { children: section.totalViews })
171
- ] }),
172
- /* @__PURE__ */ jsxs("div", { children: [
173
- /* @__PURE__ */ jsx("dt", { children: "Average duration" }),
174
- /* @__PURE__ */ jsx("dd", { children: formatSeconds(section.averageDurationSeconds) })
175
- ] })
176
- ] })
177
- ] });
178
- case "paths":
179
- return /* @__PURE__ */ jsx(
180
- DataTable,
181
- {
182
- title: "Paths",
183
- columns: ["Path", "Views", "Average duration"],
184
- rows: section.rows.map((row) => [row.path, String(row.views), formatSeconds(row.averageDurationSeconds)])
185
- }
186
- );
187
- case "countries":
188
- return /* @__PURE__ */ jsx(
189
- DataTable,
190
- {
191
- title: "Countries",
192
- columns: ["Country", "Views"],
193
- rows: section.rows.map((row) => [row.country, String(row.views)])
194
- }
195
- );
196
- case "cities":
197
- return /* @__PURE__ */ jsx(
198
- DataTable,
199
- {
200
- title: "Cities",
201
- columns: ["City", "Views"],
202
- rows: section.rows.map((row) => [row.city, String(row.views)])
203
- }
204
- );
205
- case "namedClicks":
206
- return /* @__PURE__ */ jsx(
207
- DataTable,
540
+ function niceCeiling(value) {
541
+ if (value <= GRID_LINES) return GRID_LINES;
542
+ const magnitude = 10 ** Math.floor(Math.log10(value / GRID_LINES));
543
+ for (const factor of [1, 2, 5, 10]) {
544
+ const ceiling = factor * magnitude * GRID_LINES;
545
+ if (ceiling >= value) return ceiling;
546
+ }
547
+ return value;
548
+ }
549
+ function gridValues(top) {
550
+ return Array.from({ length: GRID_LINES + 1 }, (_, index) => top / GRID_LINES * index);
551
+ }
552
+
553
+ // src/payload/admin/report-view.tsx
554
+ import { jsx as jsx5, jsxs as jsxs3 } from "react/jsx-runtime";
555
+ function AnalyticsReportView(props) {
556
+ const { report, query, modules, viewHref, label, format, eventLabel, filters } = props;
557
+ const href = (changes) => reportHref(viewHref, query, changes);
558
+ const tableProps = { report, label, format };
559
+ return /* @__PURE__ */ jsxs3("div", { className: "nwa-report", children: [
560
+ /* @__PURE__ */ jsx5("style", { children: REPORT_STYLE }),
561
+ /* @__PURE__ */ jsxs3("header", { className: "nwa-header", children: [
562
+ /* @__PURE__ */ jsx5("h1", { children: label("title") }),
563
+ filters
564
+ ] }),
565
+ query.path ? /* @__PURE__ */ jsx5("div", { className: "nwa-chips", children: /* @__PURE__ */ jsxs3("span", { className: "nwa-chip", children: [
566
+ label("pageFilter", { path: query.path }),
567
+ /* @__PURE__ */ jsx5("a", { href: href({ path: null }), children: label("clearFilter") })
568
+ ] }) }) : null,
569
+ /* @__PURE__ */ jsx5(Totals, { report, label, format }),
570
+ /* @__PURE__ */ jsxs3("section", { className: "nwa-card", children: [
571
+ /* @__PURE__ */ jsx5("h2", { children: label("viewsOverTime") }),
572
+ /* @__PURE__ */ jsx5(
573
+ ViewsChart,
208
574
  {
209
- title: "Named clicks",
210
- columns: ["Event", "Count"],
211
- rows: section.rows.map((row) => [row.eventName, String(row.count)])
575
+ description: label("viewsOverTime"),
576
+ formatNumber: format.number,
577
+ points: report.viewsOverTime.map((point) => ({
578
+ label: format.bucket(point.start),
579
+ views: point.views,
580
+ title: `${format.bucket(point.start)}: ${format.number(point.views)} ${label("views")}`
581
+ }))
212
582
  }
213
- );
214
- case "sessions":
215
- return /* @__PURE__ */ jsxs("section", { children: [
216
- /* @__PURE__ */ jsx("h2", { children: "Sessions" }),
217
- /* @__PURE__ */ jsx("div", { className: "analytics-scroll", children: /* @__PURE__ */ jsxs("table", { children: [
218
- /* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { children: [
219
- /* @__PURE__ */ jsx("th", { scope: "col", children: "Session" }),
220
- /* @__PURE__ */ jsx("th", { scope: "col", children: "Started" }),
221
- /* @__PURE__ */ jsx("th", { scope: "col", children: "Pages" })
222
- ] }) }),
223
- /* @__PURE__ */ jsx("tbody", { children: section.rows.length === 0 ? /* @__PURE__ */ jsx("tr", { children: /* @__PURE__ */ jsx("td", { colSpan: 3, children: "None in this range." }) }) : section.rows.map((row) => /* @__PURE__ */ jsxs("tr", { children: [
224
- /* @__PURE__ */ jsx("td", { children: /* @__PURE__ */ jsx("a", { href: hrefForSession(row.id), children: row.id }) }),
225
- /* @__PURE__ */ jsx("td", { children: row.startedAt }),
226
- /* @__PURE__ */ jsx("td", { children: row.pageCount })
227
- ] }, row.id)) })
228
- ] }) })
229
- ] });
230
- case "trail":
231
- return /* @__PURE__ */ jsx(
232
- DataTable,
583
+ )
584
+ ] }),
585
+ /* @__PURE__ */ jsxs3("div", { className: "nwa-columns", children: [
586
+ /* @__PURE__ */ jsx5(
587
+ PagesTable,
233
588
  {
234
- title: "Session events",
235
- columns: ["Time", "Kind", "Path", "Event"],
236
- rows: section.events.map((event) => [
237
- event.occurredAt,
238
- event.kind === "click" ? "Click" : "View",
239
- event.path,
240
- event.eventName ?? ""
241
- ])
589
+ ...tableProps,
590
+ showAll: query.showAllPages,
591
+ pageHref: (path) => href({ path, showAllPages: false }),
592
+ toggleHref: href({ showAllPages: !query.showAllPages })
242
593
  }
243
- );
594
+ ),
595
+ modules.country ? /* @__PURE__ */ jsx5(CountriesTable, { ...tableProps, countryHref: (country) => href({ country }) }) : null,
596
+ modules.clicks ? /* @__PURE__ */ jsx5(ClicksTable, { ...tableProps, eventLabel }) : null,
597
+ modules.city ? /* @__PURE__ */ jsx5(CitiesTable, { ...tableProps }) : null
598
+ ] }),
599
+ modules.sessions ? /* @__PURE__ */ jsx5(
600
+ SessionsSection,
601
+ {
602
+ ...tableProps,
603
+ eventLabel,
604
+ sessionHref: (session) => href({ session }),
605
+ allSessionsHref: href({ session: null })
606
+ }
607
+ ) : null
608
+ ] });
609
+ }
610
+ function Totals({ report, label, format }) {
611
+ const stats = [
612
+ { key: "views", name: label("views"), value: format.number(report.totals.views) },
613
+ { key: "time", name: label("averageTime"), value: format.duration(report.totals.averageDurationSeconds) }
614
+ ];
615
+ if (report.totals.clicks !== null) {
616
+ stats.push({ key: "clicks", name: label("clicks"), value: format.number(report.totals.clicks) });
244
617
  }
618
+ return /* @__PURE__ */ jsx5("dl", { className: "nwa-cards", children: stats.map((stat) => /* @__PURE__ */ jsxs3("div", { className: "nwa-card nwa-stat", children: [
619
+ /* @__PURE__ */ jsx5("dt", { children: stat.name }),
620
+ /* @__PURE__ */ jsx5("dd", { children: stat.value })
621
+ ] }, stat.key)) });
245
622
  }
246
- function DataTable({ title, columns, rows }) {
247
- return /* @__PURE__ */ jsxs("section", { children: [
248
- /* @__PURE__ */ jsx("h2", { children: title }),
249
- /* @__PURE__ */ jsx("div", { className: "analytics-scroll", children: /* @__PURE__ */ jsxs("table", { children: [
250
- /* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsx("tr", { children: columns.map((column) => /* @__PURE__ */ jsx("th", { scope: "col", children: column }, column)) }) }),
251
- /* @__PURE__ */ jsx("tbody", { children: rows.length === 0 ? /* @__PURE__ */ jsx("tr", { children: /* @__PURE__ */ jsx("td", { colSpan: columns.length, children: "None in this range." }) }) : rows.map((row, index) => /* @__PURE__ */ jsx("tr", { children: row.map((cell, cellIndex) => /* @__PURE__ */ jsx("td", { children: cell }, `${columns[cellIndex] ?? cellIndex}`)) }, `${title}-${index}`)) })
252
- ] }) })
253
- ] });
623
+
624
+ // src/payload/admin/view.tsx
625
+ import { jsx as jsx6 } from "react/jsx-runtime";
626
+ var ANALYTICS_VIEW_PATH = "/analytics";
627
+ var PERIOD_FILTER_ID = "nwa-period";
628
+ var COUNTRY_FILTER_ID = "nwa-country";
629
+ var ALL_COUNTRIES = "all";
630
+ async function AnalyticsAdminView(props) {
631
+ const { initPageResult, i18n, params, searchParams } = props;
632
+ const { req, permissions, locale, visibleEntities } = initPageResult;
633
+ if (!req.user || !permissions.collections?.[ANALYTICS_EVENTS_SLUG]?.read) notFound();
634
+ const payload = req.payload;
635
+ const settings = analyticsAdminSettingsFromConfig(payload.config);
636
+ const query = parseReportQuery(searchParams, settings.modules);
637
+ const range = analyticsPeriodRange(query.period, /* @__PURE__ */ new Date(), settings.timeZone);
638
+ const report = await loadReport(payload, settings, query, range);
639
+ const formatLocale = resolveFormatLocale(i18n.language, i18n.dateFNSKey, settings.formatLocales);
640
+ const format = reportFormat(formatLocale, settings.timeZone, range.unit);
641
+ const t = i18n.t;
642
+ const label = (key, vars) => analyticsLabel(t, key, vars);
643
+ const viewHref = `${payload.config.routes.admin}${ANALYTICS_VIEW_PATH}`;
644
+ const filters = {
645
+ filters: reportFilters({ query, report, viewHref, label, format, hasCountries: settings.modules.country })
646
+ };
647
+ return /* @__PURE__ */ jsx6(
648
+ DefaultTemplate,
649
+ {
650
+ i18n,
651
+ locale,
652
+ params,
653
+ payload,
654
+ permissions,
655
+ req,
656
+ searchParams,
657
+ user: req.user,
658
+ viewType: props.viewType,
659
+ visibleEntities,
660
+ children: /* @__PURE__ */ jsx6(Gutter, { children: /* @__PURE__ */ jsx6(
661
+ AnalyticsReportView,
662
+ {
663
+ report,
664
+ query,
665
+ modules: settings.modules,
666
+ viewHref,
667
+ label,
668
+ format,
669
+ eventLabel: (eventName) => analyticsEventLabel(i18n.translations, eventName),
670
+ filters: RenderServerComponent({
671
+ Component: ANALYTICS_FILTERS_PATH,
672
+ importMap: payload.importMap,
673
+ clientProps: filters
674
+ })
675
+ }
676
+ ) })
677
+ }
678
+ );
254
679
  }
255
- function reportHref(query) {
256
- const params = new URLSearchParams();
257
- params.set("from", query.from);
258
- params.set("to", query.to);
259
- if (query.country) params.set("country", query.country);
260
- if (query.path) params.set("path", query.path);
261
- if (query.session) params.set("session", query.session);
262
- return `/admin/analytics?${params.toString()}`;
263
- }
264
- function resolveRange(searchParams) {
265
- const now = /* @__PURE__ */ new Date();
266
- const to = parseTime(firstParam(searchParams?.to), now);
267
- const from = parseTime(firstParam(searchParams?.from), new Date(to.getTime() - WEEK_MS));
268
- return { from: from.toISOString(), to: to.toISOString() };
269
- }
270
- function parseTime(value, fallback) {
271
- if (value === null) return fallback;
272
- const parsed = new Date(value);
273
- if (Number.isNaN(parsed.getTime())) return fallback;
274
- return parsed;
275
- }
276
- function firstParam(value) {
277
- const first = Array.isArray(value) ? value[0] : value;
278
- if (typeof first !== "string") return null;
279
- const trimmed = first.trim();
280
- return trimmed.length === 0 ? null : trimmed;
680
+ async function loadReport(payload, settings, query, range) {
681
+ const store = new PayloadAnalyticsEventStore(payload, settings.modules);
682
+ const base = { from: range.from, to: range.to, path: query.path, sessionId: query.session };
683
+ const summary = await store.summarizeAnalytics(
684
+ { ...base, country: query.country, buckets: range.buckets },
685
+ settings.modules
686
+ );
687
+ const periodSummary = query.country === null ? summary : await store.summarizeAnalytics({ ...base, country: null }, settings.modules);
688
+ return buildAnalyticsReport({ summary, periodSummary, modules: settings.modules, query });
281
689
  }
282
- function formatSeconds(value) {
283
- if (!Number.isFinite(value)) return "0 seconds";
284
- const rounded = Math.round(value * 10) / 10;
285
- const text = Number.isInteger(rounded) ? String(rounded) : rounded.toFixed(1);
286
- return `${text} ${rounded === 1 ? "second" : "seconds"}`;
690
+ function reportFilters({ query, report, viewHref, label, format, hasCountries }) {
691
+ const filters = [
692
+ {
693
+ id: PERIOD_FILTER_ID,
694
+ label: label("period"),
695
+ value: query.period,
696
+ options: ANALYTICS_PERIODS.map((period) => ({ value: period, label: label(`period_${period}`) })),
697
+ hrefs: Object.fromEntries(
698
+ ANALYTICS_PERIODS.map((period) => [period, reportHref(viewHref, query, { period })])
699
+ )
700
+ }
701
+ ];
702
+ if (!hasCountries) return filters;
703
+ const countries = query.country && !report.countryOptions.includes(query.country) ? [query.country, ...report.countryOptions] : report.countryOptions;
704
+ filters.push({
705
+ id: COUNTRY_FILTER_ID,
706
+ label: label("country"),
707
+ value: query.country ?? ALL_COUNTRIES,
708
+ options: [
709
+ { value: ALL_COUNTRIES, label: label("allCountries") },
710
+ ...countries.map((country) => ({ value: country, label: format.country(country) }))
711
+ ],
712
+ hrefs: {
713
+ [ALL_COUNTRIES]: reportHref(viewHref, query, { country: null }),
714
+ ...Object.fromEntries(
715
+ countries.map((country) => [country, reportHref(viewHref, query, { country })])
716
+ )
717
+ }
718
+ });
719
+ return filters;
287
720
  }
288
721
  export {
289
722
  AnalyticsAdminView