@byline/admin 5.1.3 → 5.1.4

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.
@@ -6,15 +6,25 @@
6
6
  * Copyright (c) Infonomic Company Limited
7
7
  */
8
8
  import type React from 'react';
9
+ import type { AnalyticsPathTotal, AnalyticsRankedTotals, AnalyticsReferrerTotal } from '@byline/analytics';
9
10
  import type { AnalyticsDashboardData, AnalyticsDashboardPeriod } from '../types.js';
11
+ export { shareWidth } from './ranking.js';
12
+ /** Rows each ranked card shows before offering the full list. */
13
+ export declare const ANALYTICS_PREVIEW_ROWS = 10;
14
+ /** The truncated lists a host can fetch in full on demand. */
15
+ export type AnalyticsFullListKind = 'pages' | 'downloads' | 'referrers';
10
16
  export interface AnalyticsDashboardProps {
11
17
  data: AnalyticsDashboardData;
12
18
  period: AnalyticsDashboardPeriod;
13
19
  onPeriodChange(period: AnalyticsDashboardPeriod): void;
20
+ /**
21
+ * Fetch a larger top-N for one of the truncated lists when its "View all"
22
+ * modal opens. Countries are always complete and never use this. Omit it
23
+ * and those cards still show their preview, without the action.
24
+ */
25
+ loadFullList?(kind: AnalyticsFullListKind): Promise<AnalyticsRankedTotals<AnalyticsPathTotal | AnalyticsReferrerTotal>>;
14
26
  }
15
- export declare function AnalyticsDashboard({ data, period, onPeriodChange, }: AnalyticsDashboardProps): React.JSX.Element;
16
- /** Never collapse the bar entirely: a visible sliver still encodes "smallest". */
17
- export declare function shareWidth(value: number, ceiling: number): number;
27
+ export declare function AnalyticsDashboard({ data, period, onPeriodChange, loadFullList, }: AnalyticsDashboardProps): React.JSX.Element;
18
28
  export declare function formatShare(part: number, whole: number, locale: string): string;
19
29
  /** Return the retained boundary only when it truncates the selected range. */
20
30
  export declare function partialCoverageFrom(rangeFrom: string, coverageFrom: string | null): string | undefined;
@@ -4,11 +4,14 @@ import { useEffect, useMemo, useState } from "react";
4
4
  import { ANALYTICS_DASHBOARD_PERIODS, ANALYTICS_OVERFLOW_KEY, isAnalyticsDashboardPeriod } from "@byline/analytics/config";
5
5
  import { ANALYTICS_IGNORE_STORAGE_KEY } from "@byline/analytics-agent";
6
6
  import { useTranslation } from "@byline/i18n/react";
7
- import { Button, Card, Container, Section, Select } from "@byline/ui/react";
7
+ import { Button, Card, Container, Section, Select, useModal } from "@byline/ui/react";
8
8
  import clsx from "clsx";
9
9
  import dashboard_module from "./dashboard.module.js";
10
+ import { RankedListModal } from "./ranked-list-modal.js";
11
+ import { RankingRows, regionName, shareWidth } from "./ranking.js";
10
12
  import { AnalyticsTimeseries, resolveAnalyticsChartGranularity } from "./timeseries.js";
11
- function AnalyticsDashboard({ data, period, onPeriodChange }) {
13
+ const ANALYTICS_PREVIEW_ROWS = 10;
14
+ function AnalyticsDashboard({ data, period, onPeriodChange, loadFullList }) {
12
15
  const { locale, t } = useTranslation('byline-admin');
13
16
  const [excluded, setExcluded] = useState(false);
14
17
  const numbers = useMemo(()=>new Intl.NumberFormat(locale), [
@@ -37,6 +40,30 @@ function AnalyticsDashboard({ data, period, onPeriodChange }) {
37
40
  const { views, visitors, downloads } = data.summary;
38
41
  const days = data.summary.timeseries.length;
39
42
  const chartGranularity = resolveAnalyticsChartGranularity(period, days);
43
+ const fullLists = useMemo(()=>{
44
+ if (!loadFullList) return {};
45
+ const load = (kind)=>()=>loadFullList(kind).then((result)=>({
46
+ rows: result.rows.map(toRankedRow),
47
+ total: result.total
48
+ }));
49
+ return {
50
+ pages: load('pages'),
51
+ downloads: load('downloads'),
52
+ referrers: load('referrers')
53
+ };
54
+ }, [
55
+ loadFullList
56
+ ]);
57
+ const countryRows = useMemo(()=>data.countries.map((row)=>({
58
+ key: row.country,
59
+ label: regionName(row.country, locale),
60
+ value: row.views,
61
+ visitors: row.visitors,
62
+ overflow: false
63
+ })), [
64
+ data.countries,
65
+ locale
66
+ ]);
40
67
  return /*#__PURE__*/ jsx(Section, {
41
68
  children: /*#__PURE__*/ jsxs(Container, {
42
69
  children: [
@@ -136,8 +163,9 @@ function AnalyticsDashboard({ data, period, onPeriodChange }) {
136
163
  caption: t('analytics.columns.viewsAndUniques'),
137
164
  tone: "views",
138
165
  locale: locale,
139
- rows: data.pages.rows.map(toPathRow),
166
+ rows: data.pages.rows.map(toRankedRow),
140
167
  total: data.pages.total,
168
+ fullList: fullLists.pages,
141
169
  coverageFrom: partialCoverageFrom(data.range.from, data.coverage.pathsFrom)
142
170
  }),
143
171
  /*#__PURE__*/ jsxs("div", {
@@ -147,27 +175,21 @@ function AnalyticsDashboard({ data, period, onPeriodChange }) {
147
175
  title: t('analytics.sections.referrers'),
148
176
  tone: "visitors",
149
177
  locale: locale,
178
+ rows: data.referrers.rows.map(toRankedRow),
150
179
  total: data.referrers.total,
151
- coverageFrom: partialCoverageFrom(data.range.from, data.coverage.referrersFrom),
152
- rows: data.referrers.rows.map((row)=>({
153
- key: row.referrerHost,
154
- label: row.referrerHost,
155
- value: row.views,
156
- visitors: row.visitors,
157
- overflow: row.referrerHost === ANALYTICS_OVERFLOW_KEY
158
- }))
180
+ fullList: fullLists.referrers,
181
+ coverageFrom: partialCoverageFrom(data.range.from, data.coverage.referrersFrom)
159
182
  }),
160
183
  /*#__PURE__*/ jsx(RankedList, {
161
184
  title: t('analytics.sections.countries'),
162
185
  tone: "visitors",
163
186
  locale: locale,
164
- rows: data.countries.map((row)=>({
165
- key: row.country,
166
- label: row.country,
167
- value: row.views,
168
- visitors: row.visitors,
169
- overflow: false
170
- }))
187
+ rows: countryRows,
188
+ total: countryRows.length,
189
+ fullList: {
190
+ rows: countryRows,
191
+ total: countryRows.length
192
+ }
171
193
  })
172
194
  ]
173
195
  })
@@ -178,8 +200,9 @@ function AnalyticsDashboard({ data, period, onPeriodChange }) {
178
200
  caption: t('analytics.columns.clicksAndUniques'),
179
201
  tone: "downloads",
180
202
  locale: locale,
181
- rows: data.downloads.rows.map(toPathRow),
203
+ rows: data.downloads.rows.map(toRankedRow),
182
204
  total: data.downloads.total,
205
+ fullList: fullLists.downloads,
183
206
  coverageFrom: partialCoverageFrom(data.range.from, data.coverage.pathsFrom)
184
207
  })
185
208
  ]
@@ -191,11 +214,6 @@ const TONE_TILE = {
191
214
  visitors: dashboard_module.toneVisitors,
192
215
  downloads: dashboard_module.toneDownloads
193
216
  };
194
- const TONE_BAR = {
195
- views: dashboard_module.barViews,
196
- visitors: dashboard_module.barVisitors,
197
- downloads: dashboard_module.barDownloads
198
- };
199
217
  function StatTile({ tone, label, value, foot }) {
200
218
  return /*#__PURE__*/ jsxs("div", {
201
219
  className: clsx('byline-analytics-stat', dashboard_module.stat, TONE_TILE[tone]),
@@ -215,22 +233,21 @@ function StatTile({ tone, label, value, foot }) {
215
233
  ]
216
234
  });
217
235
  }
218
- function toPathRow(row) {
236
+ function toRankedRow(row) {
237
+ const key = 'path' in row ? row.path : row.referrerHost;
219
238
  return {
220
- key: row.path,
221
- label: row.path,
239
+ key,
240
+ label: key,
222
241
  value: row.views,
223
242
  visitors: row.visitors,
224
- overflow: row.path === ANALYTICS_OVERFLOW_KEY
243
+ overflow: key === ANALYTICS_OVERFLOW_KEY
225
244
  };
226
245
  }
227
- function RankedList({ title, caption, rows, tone, locale, total, coverageFrom }) {
246
+ function RankedList({ title, caption, rows, tone, locale, total, fullList, coverageFrom }) {
228
247
  const { t } = useTranslation('byline-admin');
229
- const numbers = useMemo(()=>new Intl.NumberFormat(locale), [
230
- locale
231
- ]);
232
- const ceiling = Math.max(1, ...rows.map((row)=>row.value));
233
- const truncated = null != total && total > rows.length;
248
+ const modal = useModal();
249
+ const preview = rows.slice(0, ANALYTICS_PREVIEW_ROWS);
250
+ const truncated = total > preview.length;
234
251
  const coverageDate = useMemo(()=>new Intl.DateTimeFormat(locale, {
235
252
  dateStyle: 'medium',
236
253
  timeZone: 'UTC'
@@ -239,7 +256,7 @@ function RankedList({ title, caption, rows, tone, locale, total, coverageFrom })
239
256
  ]);
240
257
  const description = [
241
258
  truncated ? t('analytics.topOf', {
242
- shown: rows.length,
259
+ shown: preview.length,
243
260
  total
244
261
  }) : caption,
245
262
  null == coverageFrom ? void 0 : t('analytics.coverage.since', {
@@ -250,51 +267,52 @@ function RankedList({ title, caption, rows, tone, locale, total, coverageFrom })
250
267
  className: clsx('byline-analytics-list', dashboard_module.list),
251
268
  children: [
252
269
  /*#__PURE__*/ jsxs(Card.Header, {
270
+ className: clsx('byline-analytics-list-header', dashboard_module.listHeader),
253
271
  children: [
254
- /*#__PURE__*/ jsx(Card.Title, {
255
- children: title
272
+ /*#__PURE__*/ jsxs("div", {
273
+ className: dashboard_module.listHeading,
274
+ children: [
275
+ /*#__PURE__*/ jsx(Card.Title, {
276
+ children: title
277
+ }),
278
+ description.length > 0 && /*#__PURE__*/ jsx(Card.Description, {
279
+ children: description
280
+ })
281
+ ]
256
282
  }),
257
- description.length > 0 && /*#__PURE__*/ jsx(Card.Description, {
258
- children: description
283
+ null != fullList && truncated && /*#__PURE__*/ jsx(Button, {
284
+ type: "button",
285
+ size: "xs",
286
+ variant: "outlined",
287
+ className: clsx('byline-analytics-view-all', dashboard_module.viewAll),
288
+ onClick: modal.onOpen,
289
+ children: t('analytics.viewAll', {
290
+ count: total
291
+ })
259
292
  })
260
293
  ]
261
294
  }),
262
295
  /*#__PURE__*/ jsx(Card.Content, {
263
- children: 0 === rows.length ? /*#__PURE__*/ jsx("p", {
296
+ children: 0 === preview.length ? /*#__PURE__*/ jsx("p", {
264
297
  className: "muted",
265
298
  children: t('analytics.empty')
266
- }) : /*#__PURE__*/ jsx("ol", {
267
- className: clsx('byline-analytics-ranking', dashboard_module.ranking),
268
- children: rows.map((row)=>/*#__PURE__*/ jsxs("li", {
269
- className: clsx('byline-analytics-ranking-row', dashboard_module.rankingRow, TONE_BAR[tone], row.overflow && dashboard_module.rankingOverflow),
270
- style: {
271
- '--byline-analytics-share': `${shareWidth(row.value, ceiling)}%`
272
- },
273
- children: [
274
- /*#__PURE__*/ jsx("span", {
275
- className: dashboard_module.rankingLabel,
276
- title: row.key,
277
- children: row.overflow ? t('analytics.overflow') : row.label
278
- }),
279
- /*#__PURE__*/ jsx("span", {
280
- className: dashboard_module.rankingValue,
281
- children: numbers.format(row.value)
282
- }),
283
- /*#__PURE__*/ jsx("span", {
284
- className: dashboard_module.rankingVisitors,
285
- children: numbers.format(row.visitors)
286
- })
287
- ]
288
- }, row.key))
299
+ }) : /*#__PURE__*/ jsx(RankingRows, {
300
+ rows: preview,
301
+ tone: tone,
302
+ locale: locale
289
303
  })
304
+ }),
305
+ null != fullList && /*#__PURE__*/ jsx(RankedListModal, {
306
+ isOpen: modal.isOpen,
307
+ onDismiss: modal.onDismiss,
308
+ title: title,
309
+ tone: tone,
310
+ locale: locale,
311
+ source: fullList
290
312
  })
291
313
  ]
292
314
  });
293
315
  }
294
- function shareWidth(value, ceiling) {
295
- if (!Number.isFinite(value) || value <= 0 || ceiling <= 0) return 0;
296
- return Math.max(3, Math.min(100, value / ceiling * 100));
297
- }
298
316
  function formatShare(part, whole, locale) {
299
317
  const percent = new Intl.NumberFormat(locale, {
300
318
  style: 'percent',
@@ -305,4 +323,4 @@ function formatShare(part, whole, locale) {
305
323
  function partialCoverageFrom(rangeFrom, coverageFrom) {
306
324
  return null != coverageFrom && coverageFrom > rangeFrom ? coverageFrom : void 0;
307
325
  }
308
- export { AnalyticsDashboard, formatShare, partialCoverageFrom, shareWidth };
326
+ export { ANALYTICS_PREVIEW_ROWS, AnalyticsDashboard, formatShare, partialCoverageFrom, shareWidth };
@@ -24,6 +24,15 @@ const dashboard_module = {
24
24
  rankingOverflow: "rankingOverflow-hUQMKU",
25
25
  rankingLabel: "rankingLabel-wsuniC",
26
26
  rankingValue: "rankingValue-i_vmgf",
27
- rankingVisitors: "rankingVisitors-NZl_jj"
27
+ rankingVisitors: "rankingVisitors-NZl_jj",
28
+ listHeader: "listHeader-nWIpzD",
29
+ listHeading: "listHeading-ljSubR",
30
+ viewAll: "viewAll-jPtoC0",
31
+ modal: "modal-_Fpti3",
32
+ modalCaption: "modalCaption-Z7Qsck",
33
+ modalContent: "modalContent-raK5q1",
34
+ modalStatus: "modalStatus-bXbZr9",
35
+ modalActions: "modalActions-XVBrOS",
36
+ srOnly: "srOnly-szSAhX"
28
37
  };
29
38
  export default dashboard_module;
@@ -266,3 +266,60 @@
266
266
  }
267
267
  }
268
268
 
269
+ :is(.listHeader-nWIpzD, .byline-analytics-list-header) {
270
+ justify-content: space-between;
271
+ align-items: flex-start;
272
+ gap: .75rem;
273
+ display: flex;
274
+ }
275
+
276
+ .listHeading-ljSubR {
277
+ min-width: 0;
278
+ }
279
+
280
+ :is(.viewAll-jPtoC0, .byline-analytics-view-all) {
281
+ white-space: nowrap;
282
+ flex-shrink: 0;
283
+ }
284
+
285
+ :is(.modal-_Fpti3, .byline-analytics-modal) {
286
+ width: min(40rem, 100vw - 2rem);
287
+ }
288
+
289
+ :is(.modalCaption-Z7Qsck, .byline-analytics-modal-caption) {
290
+ font-variant-numeric: tabular-nums;
291
+ margin: .25rem 0 0;
292
+ font-size: .75rem;
293
+ }
294
+
295
+ :is(.modalContent-raK5q1, .byline-analytics-modal-content) {
296
+ max-height: min(60vh, 48rem);
297
+ overflow-y: auto;
298
+ }
299
+
300
+ .modalStatus-bXbZr9 {
301
+ justify-content: center;
302
+ padding: 2rem 0;
303
+ display: flex;
304
+ }
305
+
306
+ :is(.modalActions-XVBrOS, .byline-analytics-modal-actions) {
307
+ flex-wrap: wrap;
308
+ justify-content: space-between;
309
+ align-items: center;
310
+ gap: .75rem;
311
+ display: flex;
312
+ }
313
+
314
+ .srOnly-szSAhX {
315
+ clip: rect(0, 0, 0, 0);
316
+ white-space: nowrap;
317
+ border: 0;
318
+ width: 1px;
319
+ height: 1px;
320
+ margin: -1px;
321
+ padding: 0;
322
+ position: absolute;
323
+ overflow: hidden;
324
+ }
325
+
@@ -0,0 +1,32 @@
1
+ /**
2
+ * This Source Code is subject to the terms of the Mozilla Public
3
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
4
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
5
+ *
6
+ * Copyright (c) Infonomic Company Limited
7
+ */
8
+ /**
9
+ * Full ranking for one dashboard card, paged client-side.
10
+ *
11
+ * The dashboard cards show a short preview so the lists band stays even
12
+ * regardless of traffic. This modal shows the whole set behind a card's
13
+ * "View all" action. A `source` may be a ready list (countries are fully
14
+ * loaded with the dashboard) or a loader invoked once each time the modal
15
+ * opens (pages, downloads and referrers fetch a larger top-N on demand).
16
+ *
17
+ * The caption keeps the dashboard's honesty rule: when the loaded rows are
18
+ * a top-N slice of a larger set it says so alongside the page range.
19
+ */
20
+ import type React from 'react';
21
+ import { type AnalyticsTone, type RankedListSource } from './ranking.js';
22
+ export type RankedListSourceInput = RankedListSource | (() => Promise<RankedListSource>);
23
+ export interface RankedListModalProps {
24
+ isOpen: boolean;
25
+ onDismiss(): void;
26
+ title: string;
27
+ tone: AnalyticsTone;
28
+ locale: string;
29
+ source: RankedListSourceInput;
30
+ pageSize?: number;
31
+ }
32
+ export declare function RankedListModal({ isOpen, onDismiss, title, tone, locale, source, pageSize, }: RankedListModalProps): React.JSX.Element;
@@ -0,0 +1,145 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import { useEffect, useState } from "react";
4
+ import { useTranslation } from "@byline/i18n/react";
5
+ import { Alert, Button, LoaderRing, Modal, Pagination } from "@byline/ui/react";
6
+ import clsx from "clsx";
7
+ import dashboard_module from "./dashboard.module.js";
8
+ import { RankingRows, pageWindow } from "./ranking.js";
9
+ function RankedListModal({ isOpen, onDismiss, title, tone, locale, source, pageSize = 25 }) {
10
+ const { t } = useTranslation('byline-admin');
11
+ const [state, setState] = useState({
12
+ status: 'idle'
13
+ });
14
+ const [page, setPage] = useState(1);
15
+ useEffect(()=>{
16
+ if (!isOpen) {
17
+ setState({
18
+ status: 'idle'
19
+ });
20
+ setPage(1);
21
+ return;
22
+ }
23
+ if ('function' != typeof source) return void setState({
24
+ status: 'ready',
25
+ data: source
26
+ });
27
+ let current = true;
28
+ setState({
29
+ status: 'loading'
30
+ });
31
+ source().then((data)=>{
32
+ if (current) setState({
33
+ status: 'ready',
34
+ data
35
+ });
36
+ }).catch(()=>{
37
+ if (current) setState({
38
+ status: 'error'
39
+ });
40
+ });
41
+ return ()=>{
42
+ current = false;
43
+ };
44
+ }, [
45
+ isOpen,
46
+ source
47
+ ]);
48
+ const data = 'ready' === state.status ? state.data : void 0;
49
+ const loaded = data?.rows.length ?? 0;
50
+ const window = pageWindow(loaded, pageSize, page);
51
+ const visible = data?.rows.slice(window.start, window.end) ?? [];
52
+ const truncated = null != data && data.total > loaded;
53
+ const caption = data ? [
54
+ 0 === loaded ? void 0 : t('analytics.range', {
55
+ from: window.start + 1,
56
+ to: window.end,
57
+ total: loaded
58
+ }),
59
+ truncated ? t('analytics.topOf', {
60
+ shown: loaded,
61
+ total: data.total
62
+ }) : void 0
63
+ ].filter((value)=>null != value).join(' · ') : void 0;
64
+ return /*#__PURE__*/ jsx(Modal, {
65
+ isOpen: isOpen,
66
+ onDismiss: onDismiss,
67
+ closeOnOverlayClick: true,
68
+ children: /*#__PURE__*/ jsxs(Modal.Container, {
69
+ className: clsx('byline-analytics-modal', dashboard_module.modal),
70
+ children: [
71
+ /*#__PURE__*/ jsxs(Modal.Header, {
72
+ children: [
73
+ /*#__PURE__*/ jsx("h2", {
74
+ children: title
75
+ }),
76
+ null != caption && caption.length > 0 && /*#__PURE__*/ jsx("p", {
77
+ className: clsx('muted', 'byline-analytics-modal-caption', dashboard_module.modalCaption),
78
+ children: caption
79
+ })
80
+ ]
81
+ }),
82
+ /*#__PURE__*/ jsxs(Modal.Content, {
83
+ className: clsx('byline-analytics-modal-content', dashboard_module.modalContent),
84
+ children: [
85
+ 'loading' === state.status && /*#__PURE__*/ jsxs("div", {
86
+ role: "status",
87
+ "aria-live": "polite",
88
+ className: dashboard_module.modalStatus,
89
+ children: [
90
+ /*#__PURE__*/ jsx(LoaderRing, {
91
+ size: 28,
92
+ "aria-hidden": "true"
93
+ }),
94
+ /*#__PURE__*/ jsx("span", {
95
+ className: dashboard_module.srOnly,
96
+ children: t('common.loading')
97
+ })
98
+ ]
99
+ }),
100
+ 'error' === state.status && /*#__PURE__*/ jsx("div", {
101
+ role: "alert",
102
+ children: /*#__PURE__*/ jsx(Alert, {
103
+ intent: "danger",
104
+ close: false,
105
+ children: t('analytics.loadError')
106
+ })
107
+ }),
108
+ null != data && 0 === loaded && /*#__PURE__*/ jsx("p", {
109
+ className: "muted",
110
+ children: t('analytics.empty')
111
+ }),
112
+ null != data && loaded > 0 && /*#__PURE__*/ jsx(RankingRows, {
113
+ rows: visible,
114
+ tone: tone,
115
+ locale: locale
116
+ })
117
+ ]
118
+ }),
119
+ /*#__PURE__*/ jsxs(Modal.Actions, {
120
+ className: clsx('byline-analytics-modal-actions', dashboard_module.modalActions),
121
+ children: [
122
+ window.pageCount > 1 ? /*#__PURE__*/ jsx(Pagination, {
123
+ variant: "dashboard",
124
+ count: window.pageCount,
125
+ page: Math.min(page, window.pageCount),
126
+ onChange: (_event, next)=>setPage(next),
127
+ children: /*#__PURE__*/ jsx(Pagination.Root, {
128
+ ariaLabel: t('analytics.pager'),
129
+ children: /*#__PURE__*/ jsx(Pagination.Pager, {})
130
+ })
131
+ }) : /*#__PURE__*/ jsx("span", {}),
132
+ /*#__PURE__*/ jsx(Button, {
133
+ type: "button",
134
+ size: "sm",
135
+ variant: "outlined",
136
+ onClick: onDismiss,
137
+ children: t('common.actions.close')
138
+ })
139
+ ]
140
+ })
141
+ ]
142
+ })
143
+ });
144
+ }
145
+ export { RankedListModal };
@@ -0,0 +1,45 @@
1
+ /**
2
+ * This Source Code is subject to the terms of the Mozilla Public
3
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
4
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
5
+ *
6
+ * Copyright (c) Infonomic Company Limited
7
+ */
8
+ /**
9
+ * Ranked-row rendering shared by the dashboard cards and the full-list
10
+ * modal, plus the small pure helpers both rely on.
11
+ */
12
+ import type React from 'react';
13
+ export type AnalyticsTone = 'views' | 'visitors' | 'downloads';
14
+ export interface RankedRow {
15
+ key: string;
16
+ label: string;
17
+ value: number;
18
+ visitors: number;
19
+ overflow: boolean;
20
+ }
21
+ /** A complete ranking plus the size of the set it was drawn from. */
22
+ export interface RankedListSource {
23
+ rows: RankedRow[];
24
+ total: number;
25
+ }
26
+ /** The ordered list with a share bar behind each row. Renders no card chrome. */
27
+ export declare function RankingRows({ rows, tone, locale, }: {
28
+ rows: RankedRow[];
29
+ tone: AnalyticsTone;
30
+ locale: string;
31
+ }): React.JSX.Element;
32
+ /** Never collapse the bar entirely: a visible sliver still encodes "smallest". */
33
+ export declare function shareWidth(value: number, ceiling: number): number;
34
+ /** Row indices for one page of a list, with the page clamped into range. */
35
+ export declare function pageWindow(total: number, pageSize: number, page: number): {
36
+ start: number;
37
+ end: number;
38
+ pageCount: number;
39
+ };
40
+ /**
41
+ * Localised region name for an ISO 3166-1 alpha-2 code, or the code itself
42
+ * when the value is not a code or the runtime has no name for it. The
43
+ * dashboard stores bare codes, so this is purely a display concern.
44
+ */
45
+ export declare function regionName(code: string, locale: string): string;
@@ -0,0 +1,71 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import { useMemo } from "react";
4
+ import { useTranslation } from "@byline/i18n/react";
5
+ import clsx from "clsx";
6
+ import dashboard_module from "./dashboard.module.js";
7
+ const TONE_BAR = {
8
+ views: dashboard_module.barViews,
9
+ visitors: dashboard_module.barVisitors,
10
+ downloads: dashboard_module.barDownloads
11
+ };
12
+ function RankingRows({ rows, tone, locale }) {
13
+ const { t } = useTranslation('byline-admin');
14
+ const numbers = useMemo(()=>new Intl.NumberFormat(locale), [
15
+ locale
16
+ ]);
17
+ const ceiling = Math.max(1, ...rows.map((row)=>row.value));
18
+ return /*#__PURE__*/ jsx("ol", {
19
+ className: clsx('byline-analytics-ranking', dashboard_module.ranking),
20
+ children: rows.map((row)=>/*#__PURE__*/ jsxs("li", {
21
+ className: clsx('byline-analytics-ranking-row', dashboard_module.rankingRow, TONE_BAR[tone], row.overflow && dashboard_module.rankingOverflow),
22
+ style: {
23
+ '--byline-analytics-share': `${shareWidth(row.value, ceiling)}%`
24
+ },
25
+ children: [
26
+ /*#__PURE__*/ jsx("span", {
27
+ className: dashboard_module.rankingLabel,
28
+ title: row.key,
29
+ children: row.overflow ? t('analytics.overflow') : row.label
30
+ }),
31
+ /*#__PURE__*/ jsx("span", {
32
+ className: dashboard_module.rankingValue,
33
+ children: numbers.format(row.value)
34
+ }),
35
+ /*#__PURE__*/ jsx("span", {
36
+ className: dashboard_module.rankingVisitors,
37
+ children: numbers.format(row.visitors)
38
+ })
39
+ ]
40
+ }, row.key))
41
+ });
42
+ }
43
+ function shareWidth(value, ceiling) {
44
+ if (!Number.isFinite(value) || value <= 0 || ceiling <= 0) return 0;
45
+ return Math.max(3, Math.min(100, value / ceiling * 100));
46
+ }
47
+ function pageWindow(total, pageSize, page) {
48
+ const pageCount = Math.max(1, Math.ceil(total / pageSize));
49
+ const current = Math.min(Math.max(1, page), pageCount);
50
+ const start = (current - 1) * pageSize;
51
+ return {
52
+ start,
53
+ end: Math.min(total, start + pageSize),
54
+ pageCount
55
+ };
56
+ }
57
+ function regionName(code, locale) {
58
+ if (!/^[A-Za-z]{2}$/.test(code)) return code;
59
+ try {
60
+ const names = new Intl.DisplayNames([
61
+ locale
62
+ ], {
63
+ type: 'region',
64
+ fallback: 'none'
65
+ });
66
+ return names.of(code.toUpperCase()) ?? code;
67
+ } catch {
68
+ return code;
69
+ }
70
+ }
71
+ export { RankingRows, pageWindow, regionName, shareWidth };
@@ -0,0 +1,8 @@
1
+ /**
2
+ * This Source Code is subject to the terms of the Mozilla Public
3
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
4
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
5
+ *
6
+ * Copyright (c) Infonomic Company Limited
7
+ */
8
+ export {};