@byline/admin 4.14.1 → 4.16.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/abilities.js +2 -0
- package/dist/forms/document-actions.d.ts +16 -1
- package/dist/forms/document-actions.js +43 -1
- package/dist/forms/form-renderer.d.ts +6 -1
- package/dist/forms/form-renderer.js +39 -36
- package/dist/forms/form-renderer.module.js +2 -0
- package/dist/forms/form-renderer_module.css +7 -1
- package/dist/forms/form-status-display.d.ts +10 -1
- package/dist/forms/form-status-display.js +4 -2
- package/dist/forms/scheduled-publication-control.d.ts +56 -0
- package/dist/forms/scheduled-publication-control.js +376 -0
- package/dist/forms/scheduled-publication-control.module.js +25 -0
- package/dist/forms/scheduled-publication-control_module.css +95 -0
- package/dist/forms/scheduled-publication-state.d.ts +83 -0
- package/dist/forms/scheduled-publication-state.js +41 -0
- package/dist/forms/scheduled-publication-state.test.node.d.ts +8 -0
- package/dist/forms/scheduled-publication-time.d.ts +57 -0
- package/dist/forms/scheduled-publication-time.js +113 -0
- package/dist/forms/scheduled-publication-time.test.node.d.ts +8 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/modules/analytics/abilities.d.ts +14 -0
- package/dist/modules/analytics/abilities.js +21 -0
- package/dist/modules/analytics/components/dashboard.d.ts +20 -0
- package/dist/modules/analytics/components/dashboard.js +308 -0
- package/dist/modules/analytics/components/dashboard.module.js +29 -0
- package/dist/modules/analytics/components/dashboard.test.node.d.ts +8 -0
- package/dist/modules/analytics/components/dashboard_module.css +268 -0
- package/dist/modules/analytics/components/timeseries.d.ts +59 -0
- package/dist/modules/analytics/components/timeseries.js +253 -0
- package/dist/modules/analytics/components/timeseries.module.js +17 -0
- package/dist/modules/analytics/components/timeseries_module.css +126 -0
- package/dist/modules/analytics/index.d.ts +9 -0
- package/dist/modules/analytics/index.js +1 -0
- package/dist/modules/analytics/types.d.ts +20 -0
- package/dist/modules/analytics/types.js +1 -0
- package/dist/react.d.ts +1 -0
- package/dist/react.js +1 -0
- package/package.json +23 -7
- package/src/abilities.ts +2 -0
- package/src/forms/document-actions.tsx +73 -0
- package/src/forms/form-renderer.module.css +15 -0
- package/src/forms/form-renderer.tsx +59 -64
- package/src/forms/form-status-display.tsx +12 -0
- package/src/forms/path-widget.test.tsx +20 -8
- package/src/forms/scheduled-publication-control.module.css +149 -0
- package/src/forms/scheduled-publication-control.tsx +590 -0
- package/src/forms/scheduled-publication-datepicker.test.tsx +89 -0
- package/src/forms/scheduled-publication-state.test.node.ts +190 -0
- package/src/forms/scheduled-publication-state.ts +146 -0
- package/src/forms/scheduled-publication-time.test.node.ts +86 -0
- package/src/forms/scheduled-publication-time.ts +218 -0
- package/src/index.ts +1 -0
- package/src/modules/analytics/abilities.ts +33 -0
- package/src/modules/analytics/components/dashboard.module.css +330 -0
- package/src/modules/analytics/components/dashboard.test.node.ts +193 -0
- package/src/modules/analytics/components/dashboard.tsx +371 -0
- package/src/modules/analytics/components/timeseries.module.css +151 -0
- package/src/modules/analytics/components/timeseries.tsx +332 -0
- package/src/modules/analytics/index.ts +14 -0
- package/src/modules/analytics/types.ts +31 -0
- package/src/react.ts +3 -0
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
:is(.header-zAoAGd, .byline-analytics-header) {
|
|
2
|
+
border-bottom: var(--border-width-thin) var(--border-style-solid) var(--gray-200);
|
|
3
|
+
flex-wrap: wrap;
|
|
4
|
+
justify-content: space-between;
|
|
5
|
+
align-items: flex-end;
|
|
6
|
+
gap: 1rem 1.5rem;
|
|
7
|
+
margin-bottom: 1.25rem;
|
|
8
|
+
padding-bottom: 1rem;
|
|
9
|
+
display: flex;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
:is(.title-ZhxB2D, .byline-analytics-title) {
|
|
13
|
+
letter-spacing: -.02em;
|
|
14
|
+
margin: 0;
|
|
15
|
+
font-size: 1.875rem;
|
|
16
|
+
font-weight: 650;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
:is(.help-HT9JOI, .byline-analytics-help) {
|
|
20
|
+
max-width: 48ch;
|
|
21
|
+
margin: .35rem 0 0;
|
|
22
|
+
font-size: .8125rem;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
:is(.chartCard-NaXVMm .byline-card-title, .list-sR__nc .byline-card-title) {
|
|
26
|
+
letter-spacing: normal;
|
|
27
|
+
font-size: 1.25rem;
|
|
28
|
+
font-weight: 600;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
:is(.chartCard-NaXVMm .byline-card-description, .list-sR__nc .byline-card-description) {
|
|
32
|
+
font-size: .75rem;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
:is(.controls-_Alpbi, .byline-analytics-controls) {
|
|
36
|
+
flex-wrap: wrap;
|
|
37
|
+
align-items: center;
|
|
38
|
+
gap: .75rem;
|
|
39
|
+
display: flex;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
:is(.stats-u3kcTH, .byline-analytics-stats) {
|
|
43
|
+
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
44
|
+
gap: .75rem;
|
|
45
|
+
margin-bottom: .75rem;
|
|
46
|
+
display: grid;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
:is(.stat-KhSBeS, .byline-analytics-stat) {
|
|
50
|
+
border-radius: .375rem;
|
|
51
|
+
flex-direction: column;
|
|
52
|
+
gap: .5rem;
|
|
53
|
+
min-width: 0;
|
|
54
|
+
padding: 1rem 1.125rem 1.125rem;
|
|
55
|
+
display: flex;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
:is(.statLabel-X0_tdu, .byline-analytics-stat-label) {
|
|
59
|
+
text-transform: uppercase;
|
|
60
|
+
letter-spacing: .1em;
|
|
61
|
+
font-size: .625rem;
|
|
62
|
+
font-weight: 600;
|
|
63
|
+
line-height: 1;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
:is(.statValue-D32ine, .byline-analytics-stat-value) {
|
|
67
|
+
font-variant-numeric: tabular-nums;
|
|
68
|
+
letter-spacing: -.02em;
|
|
69
|
+
font-size: clamp(1.75rem, 4.5vw, 2.5rem);
|
|
70
|
+
font-weight: 700;
|
|
71
|
+
line-height: 1;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
:is(.statFoot-yvtZiS, .byline-analytics-stat-foot) {
|
|
75
|
+
font-variant-numeric: tabular-nums;
|
|
76
|
+
color: var(--gray-600);
|
|
77
|
+
font-size: .6875rem;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.toneViews-QGWabX {
|
|
81
|
+
background-color: #f0f9ff;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.toneViews-QGWabX .statLabel-X0_tdu, .toneViews-QGWabX .statValue-D32ine {
|
|
85
|
+
color: #0369a1;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.toneVisitors-GTLZDo {
|
|
89
|
+
background-color: #f5f3ff;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.toneVisitors-GTLZDo .statLabel-X0_tdu, .toneVisitors-GTLZDo .statValue-D32ine {
|
|
93
|
+
color: #6d28d9;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
.toneDownloads-WuDN7n {
|
|
97
|
+
background-color: #ecfdf5;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.toneDownloads-WuDN7n .statLabel-X0_tdu, .toneDownloads-WuDN7n .statValue-D32ine {
|
|
101
|
+
color: #047857;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
:is(:is([data-theme="dark"], .dark) .statFoot-yvtZiS, :is([data-theme="dark"], .dark) .byline-analytics-stat-foot) {
|
|
105
|
+
color: var(--gray-400);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
:is([data-theme="dark"], .dark) .toneViews-QGWabX {
|
|
109
|
+
background-color: #0c4a6e47;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
:is([data-theme="dark"], .dark) .toneViews-QGWabX .statLabel-X0_tdu, :is([data-theme="dark"], .dark) .toneViews-QGWabX .statValue-D32ine {
|
|
113
|
+
color: #7dd3fc;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
:is([data-theme="dark"], .dark) .toneVisitors-GTLZDo {
|
|
117
|
+
background-color: #4c1d954d;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
:is([data-theme="dark"], .dark) .toneVisitors-GTLZDo .statLabel-X0_tdu, :is([data-theme="dark"], .dark) .toneVisitors-GTLZDo .statValue-D32ine {
|
|
121
|
+
color: #c4b5fd;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
:is([data-theme="dark"], .dark) .toneDownloads-WuDN7n {
|
|
125
|
+
background-color: #064e3b47;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
:is([data-theme="dark"], .dark) .toneDownloads-WuDN7n .statLabel-X0_tdu, :is([data-theme="dark"], .dark) .toneDownloads-WuDN7n .statValue-D32ine {
|
|
129
|
+
color: #6ee7b7;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
:is(.chartCard-NaXVMm, .byline-analytics-chart-card) {
|
|
133
|
+
margin-bottom: .75rem;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
:is(.lists-BLqFfy, .byline-analytics-lists) {
|
|
137
|
+
grid-template-columns: 1.55fr 1fr;
|
|
138
|
+
align-items: start;
|
|
139
|
+
gap: .75rem;
|
|
140
|
+
margin-bottom: .75rem;
|
|
141
|
+
display: grid;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
.stack-Yzch8f {
|
|
145
|
+
flex-direction: column;
|
|
146
|
+
gap: .75rem;
|
|
147
|
+
display: flex;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
:is(.ranking-yfNSnw, .byline-analytics-ranking) {
|
|
151
|
+
flex-direction: column;
|
|
152
|
+
gap: 1px;
|
|
153
|
+
margin: 0;
|
|
154
|
+
padding: 0;
|
|
155
|
+
list-style: none;
|
|
156
|
+
display: flex;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
:is(.rankingRow-CH1cHn, .byline-analytics-ranking-row) {
|
|
160
|
+
isolation: isolate;
|
|
161
|
+
border-radius: .25rem;
|
|
162
|
+
grid-template-columns: minmax(0, 1fr) auto auto;
|
|
163
|
+
align-items: center;
|
|
164
|
+
gap: .75rem;
|
|
165
|
+
padding: .45rem .625rem;
|
|
166
|
+
display: grid;
|
|
167
|
+
position: relative;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
.rankingRow-CH1cHn:before {
|
|
171
|
+
content: "";
|
|
172
|
+
z-index: -1;
|
|
173
|
+
width: var(--byline-analytics-share, 0%);
|
|
174
|
+
background-color: var(--byline-analytics-bar, transparent);
|
|
175
|
+
opacity: .16;
|
|
176
|
+
border-radius: .25rem;
|
|
177
|
+
position: absolute;
|
|
178
|
+
inset: 0 auto 0 0;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
.barViews-E6VuaR {
|
|
182
|
+
--byline-analytics-bar: #38bdf8;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
.barVisitors-eIANXw {
|
|
186
|
+
--byline-analytics-bar: #7c3aed;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
.barDownloads-Z50NxT {
|
|
190
|
+
--byline-analytics-bar: #10b981;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
.rankingOverflow-hUQMKU {
|
|
194
|
+
border: var(--border-width-thin) dashed var(--gray-300);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
.rankingOverflow-hUQMKU:before {
|
|
198
|
+
opacity: .07;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
.rankingOverflow-hUQMKU .rankingLabel-wsuniC {
|
|
202
|
+
color: var(--gray-600);
|
|
203
|
+
font-style: italic;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
.rankingLabel-wsuniC {
|
|
207
|
+
text-overflow: ellipsis;
|
|
208
|
+
white-space: nowrap;
|
|
209
|
+
min-width: 0;
|
|
210
|
+
font-size: .8125rem;
|
|
211
|
+
overflow: hidden;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
.rankingValue-i_vmgf {
|
|
215
|
+
font-variant-numeric: tabular-nums;
|
|
216
|
+
font-size: .8125rem;
|
|
217
|
+
font-weight: 600;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
.rankingVisitors-NZl_jj {
|
|
221
|
+
font-variant-numeric: tabular-nums;
|
|
222
|
+
text-align: end;
|
|
223
|
+
min-width: 3ch;
|
|
224
|
+
color: var(--gray-500);
|
|
225
|
+
font-size: .6875rem;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
:is(:is([data-theme="dark"], .dark) .header-zAoAGd, :is([data-theme="dark"], .dark) .byline-analytics-header) {
|
|
229
|
+
border-bottom-color: var(--gray-700);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
:is([data-theme="dark"], .dark) .rankingVisitors-NZl_jj {
|
|
233
|
+
color: var(--gray-400);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
:is([data-theme="dark"], .dark) .rankingOverflow-hUQMKU {
|
|
237
|
+
border-color: var(--gray-600);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
:is([data-theme="dark"], .dark) .rankingOverflow-hUQMKU .rankingLabel-wsuniC {
|
|
241
|
+
color: var(--gray-400);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
:is([data-theme="dark"], .dark) .barVisitors-eIANXw {
|
|
245
|
+
--byline-analytics-bar: #a78bfa;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
:is([data-theme="dark"], .dark) .barDownloads-Z50NxT {
|
|
249
|
+
--byline-analytics-bar: #34d399;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
@media (max-width: 900px) {
|
|
253
|
+
:is(.lists-BLqFfy, .byline-analytics-lists) {
|
|
254
|
+
grid-template-columns: 1fr;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
@media (max-width: 720px) {
|
|
259
|
+
:is(.header-zAoAGd, .byline-analytics-header) {
|
|
260
|
+
flex-direction: column;
|
|
261
|
+
align-items: stretch;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
:is(.stats-u3kcTH, .byline-analytics-stats) {
|
|
265
|
+
grid-template-columns: 1fr;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
@@ -0,0 +1,59 @@
|
|
|
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
|
+
* The analytics timeseries — one column per explicit UTC reporting bucket.
|
|
10
|
+
*
|
|
11
|
+
* A column rather than a continuous line is a claim about the data, not a
|
|
12
|
+
* style: the rows behind this chart are sums of daily aggregates and visitor
|
|
13
|
+
* hashes rotate at UTC midnight, so nothing is measured between two points.
|
|
14
|
+
* The dashboard selects the granularity and passes it into this renderer;
|
|
15
|
+
* the chart never guesses from the number of rows.
|
|
16
|
+
*/
|
|
17
|
+
import type React from 'react';
|
|
18
|
+
import type { AnalyticsSummaryDay } from '@byline/analytics';
|
|
19
|
+
import type { AnalyticsDashboardPeriod } from '@byline/analytics/config';
|
|
20
|
+
export type AnalyticsChartGranularity = 'day' | 'seven-day' | 'month';
|
|
21
|
+
export interface AnalyticsChartBucket {
|
|
22
|
+
from: string;
|
|
23
|
+
to: string;
|
|
24
|
+
granularity: AnalyticsChartGranularity;
|
|
25
|
+
/** Actual number of daily rows, including partial first or last buckets. */
|
|
26
|
+
dayCount: number;
|
|
27
|
+
views: number;
|
|
28
|
+
/** Sum of the bucket's daily-unique visitor values. */
|
|
29
|
+
visitors: number;
|
|
30
|
+
downloads: number;
|
|
31
|
+
}
|
|
32
|
+
export interface AnalyticsColumn {
|
|
33
|
+
index: number;
|
|
34
|
+
/** Full-height mark: that bucket's page views. */
|
|
35
|
+
x: number;
|
|
36
|
+
width: number;
|
|
37
|
+
y: number;
|
|
38
|
+
height: number;
|
|
39
|
+
/** Inset mark: summed daily uniques, always no greater than views. */
|
|
40
|
+
insetX: number;
|
|
41
|
+
insetWidth: number;
|
|
42
|
+
insetY: number;
|
|
43
|
+
insetHeight: number;
|
|
44
|
+
/** Full-height transparent target, so narrow columns stay easy to hit. */
|
|
45
|
+
hitX: number;
|
|
46
|
+
hitWidth: number;
|
|
47
|
+
}
|
|
48
|
+
/** Resolve chart density outside the renderer so the chosen width is explicit. */
|
|
49
|
+
export declare function resolveAnalyticsChartGranularity(period: AnalyticsDashboardPeriod, dayCount: number): AnalyticsChartGranularity;
|
|
50
|
+
/** Combine complete daily query rows using the caller-selected granularity. */
|
|
51
|
+
export declare function bucketAnalyticsTimeseries(days: readonly AnalyticsSummaryDay[], granularity: AnalyticsChartGranularity): readonly AnalyticsChartBucket[];
|
|
52
|
+
/** Project reporting buckets into column geometry. */
|
|
53
|
+
export declare function buildAnalyticsColumns(buckets: readonly Pick<AnalyticsChartBucket, 'views' | 'visitors'>[]): readonly AnalyticsColumn[];
|
|
54
|
+
export interface AnalyticsTimeseriesProps {
|
|
55
|
+
days: readonly AnalyticsSummaryDay[];
|
|
56
|
+
granularity: AnalyticsChartGranularity;
|
|
57
|
+
locale: string;
|
|
58
|
+
}
|
|
59
|
+
export declare function AnalyticsTimeseries({ days, granularity, locale, }: AnalyticsTimeseriesProps): React.JSX.Element;
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useMemo, useRef, useState } from "react";
|
|
4
|
+
import { useTranslation } from "@byline/i18n/react";
|
|
5
|
+
import clsx from "clsx";
|
|
6
|
+
import timeseries_module from "./timeseries.module.js";
|
|
7
|
+
const VIEWBOX_WIDTH = 900;
|
|
8
|
+
const VIEWBOX_HEIGHT = 180;
|
|
9
|
+
const COLUMN_GAP_RATIO = 0.38;
|
|
10
|
+
const MAX_COLUMN_WIDTH = 26;
|
|
11
|
+
function resolveAnalyticsChartGranularity(period, dayCount) {
|
|
12
|
+
if ('number' == typeof period || 'all' === period && dayCount <= 90) return 'day';
|
|
13
|
+
if ('ytd' === period || dayCount <= 732) return 'seven-day';
|
|
14
|
+
return 'month';
|
|
15
|
+
}
|
|
16
|
+
function bucketAnalyticsTimeseries(days, granularity) {
|
|
17
|
+
if ('day' === granularity) return days.map((day)=>({
|
|
18
|
+
from: day.day,
|
|
19
|
+
to: day.day,
|
|
20
|
+
granularity,
|
|
21
|
+
dayCount: 1,
|
|
22
|
+
views: day.views,
|
|
23
|
+
visitors: day.visitors,
|
|
24
|
+
downloads: day.downloads
|
|
25
|
+
}));
|
|
26
|
+
if ('seven-day' === granularity) return bucketBySevenDays(days);
|
|
27
|
+
return bucketByUtcMonth(days);
|
|
28
|
+
}
|
|
29
|
+
function buildAnalyticsColumns(buckets) {
|
|
30
|
+
if (0 === buckets.length) return [];
|
|
31
|
+
const ceiling = Math.max(1, ...buckets.map((bucket)=>bucket.views));
|
|
32
|
+
const step = VIEWBOX_WIDTH / buckets.length;
|
|
33
|
+
const width = Math.max(2, Math.min(step * (1 - COLUMN_GAP_RATIO), MAX_COLUMN_WIDTH));
|
|
34
|
+
return buckets.map((bucket, index)=>{
|
|
35
|
+
const centre = index * step + step / 2;
|
|
36
|
+
const height = bucket.views / ceiling * VIEWBOX_HEIGHT;
|
|
37
|
+
const insetHeight = bucket.visitors / ceiling * VIEWBOX_HEIGHT;
|
|
38
|
+
const insetWidth = Math.max(1, width / 2);
|
|
39
|
+
return {
|
|
40
|
+
index,
|
|
41
|
+
x: centre - width / 2,
|
|
42
|
+
width,
|
|
43
|
+
y: VIEWBOX_HEIGHT - height,
|
|
44
|
+
height,
|
|
45
|
+
insetX: centre - insetWidth / 2,
|
|
46
|
+
insetWidth,
|
|
47
|
+
insetY: VIEWBOX_HEIGHT - insetHeight,
|
|
48
|
+
insetHeight,
|
|
49
|
+
hitX: index * step,
|
|
50
|
+
hitWidth: step
|
|
51
|
+
};
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
function AnalyticsTimeseries({ days, granularity, locale }) {
|
|
55
|
+
const { t } = useTranslation('byline-admin');
|
|
56
|
+
const [hovered, setHovered] = useState(null);
|
|
57
|
+
const plot = useRef(null);
|
|
58
|
+
const buckets = useMemo(()=>bucketAnalyticsTimeseries(days, granularity), [
|
|
59
|
+
days,
|
|
60
|
+
granularity
|
|
61
|
+
]);
|
|
62
|
+
const columns = useMemo(()=>buildAnalyticsColumns(buckets), [
|
|
63
|
+
buckets
|
|
64
|
+
]);
|
|
65
|
+
const numbers = useMemo(()=>new Intl.NumberFormat(locale), [
|
|
66
|
+
locale
|
|
67
|
+
]);
|
|
68
|
+
const dayLabel = useMemo(()=>new Intl.DateTimeFormat(locale, {
|
|
69
|
+
month: 'short',
|
|
70
|
+
day: 'numeric',
|
|
71
|
+
timeZone: 'UTC'
|
|
72
|
+
}), [
|
|
73
|
+
locale
|
|
74
|
+
]);
|
|
75
|
+
const formatDay = (day)=>dayLabel.format(new Date(`${day}T00:00:00.000Z`));
|
|
76
|
+
const formatBucket = (bucket)=>1 === bucket.dayCount ? formatDay(bucket.from) : `${formatDay(bucket.from)} – ${formatDay(bucket.to)}`;
|
|
77
|
+
const visitorLabel = (bucket)=>'day' === bucket.granularity ? t('analytics.stats.dailyUniques') : t('analytics.stats.summedDailyUniques');
|
|
78
|
+
const active = null == hovered ? void 0 : buckets[hovered];
|
|
79
|
+
const readBucket = (bucket)=>[
|
|
80
|
+
formatBucket(bucket),
|
|
81
|
+
`${t('analytics.stats.views')} ${numbers.format(bucket.views)}`,
|
|
82
|
+
`${visitorLabel(bucket)} ${numbers.format(bucket.visitors)}`,
|
|
83
|
+
`${t('analytics.stats.downloads')} ${numbers.format(bucket.downloads)}`
|
|
84
|
+
].join(', ');
|
|
85
|
+
const trackPointer = (event)=>{
|
|
86
|
+
if (0 === buckets.length || null == plot.current) return;
|
|
87
|
+
const bounds = plot.current.getBoundingClientRect();
|
|
88
|
+
if (0 === bounds.width) return;
|
|
89
|
+
const ratio = (event.clientX - bounds.left) / bounds.width;
|
|
90
|
+
setHovered(Math.min(buckets.length - 1, Math.max(0, Math.floor(ratio * buckets.length))));
|
|
91
|
+
};
|
|
92
|
+
const trackKey = (event)=>{
|
|
93
|
+
if (0 === buckets.length) return;
|
|
94
|
+
const step = 'ArrowRight' === event.key ? 1 : 'ArrowLeft' === event.key ? -1 : 0;
|
|
95
|
+
if (0 === step && 'Home' !== event.key && 'End' !== event.key && 'Escape' !== event.key) return;
|
|
96
|
+
event.preventDefault();
|
|
97
|
+
if ('Escape' === event.key) return setHovered(null);
|
|
98
|
+
if ('Home' === event.key) return setHovered(0);
|
|
99
|
+
if ('End' === event.key) return setHovered(buckets.length - 1);
|
|
100
|
+
setHovered((current)=>{
|
|
101
|
+
const next = (current ?? (step > 0 ? -1 : buckets.length)) + step;
|
|
102
|
+
return Math.min(buckets.length - 1, Math.max(0, next));
|
|
103
|
+
});
|
|
104
|
+
};
|
|
105
|
+
return /*#__PURE__*/ jsxs("div", {
|
|
106
|
+
className: clsx('byline-analytics-timeseries', timeseries_module.root),
|
|
107
|
+
role: "slider",
|
|
108
|
+
tabIndex: 0,
|
|
109
|
+
"aria-label": t('analytics.chart.views'),
|
|
110
|
+
"aria-valuemin": 0,
|
|
111
|
+
"aria-valuemax": Math.max(0, buckets.length - 1),
|
|
112
|
+
"aria-valuenow": hovered ?? 0,
|
|
113
|
+
"aria-valuetext": null == active ? t('analytics.chart.hint') : readBucket(active),
|
|
114
|
+
onPointerMove: trackPointer,
|
|
115
|
+
onPointerLeave: ()=>setHovered(null),
|
|
116
|
+
onKeyDown: trackKey,
|
|
117
|
+
onBlur: ()=>setHovered(null),
|
|
118
|
+
children: [
|
|
119
|
+
/*#__PURE__*/ jsxs("svg", {
|
|
120
|
+
ref: plot,
|
|
121
|
+
className: clsx('byline-analytics-chart', timeseries_module.chart),
|
|
122
|
+
viewBox: `0 0 ${VIEWBOX_WIDTH} ${VIEWBOX_HEIGHT}`,
|
|
123
|
+
preserveAspectRatio: "none",
|
|
124
|
+
"aria-hidden": "true",
|
|
125
|
+
children: [
|
|
126
|
+
/*#__PURE__*/ jsx("title", {
|
|
127
|
+
children: t('analytics.chart.views')
|
|
128
|
+
}),
|
|
129
|
+
[
|
|
130
|
+
0.5,
|
|
131
|
+
1
|
|
132
|
+
].map((fraction)=>/*#__PURE__*/ jsx("line", {
|
|
133
|
+
className: timeseries_module.gridline,
|
|
134
|
+
x1: 0,
|
|
135
|
+
x2: VIEWBOX_WIDTH,
|
|
136
|
+
y1: VIEWBOX_HEIGHT - VIEWBOX_HEIGHT * fraction,
|
|
137
|
+
y2: VIEWBOX_HEIGHT - VIEWBOX_HEIGHT * fraction,
|
|
138
|
+
vectorEffect: "non-scaling-stroke"
|
|
139
|
+
}, fraction)),
|
|
140
|
+
columns.map((column)=>/*#__PURE__*/ jsxs("g", {
|
|
141
|
+
className: clsx(timeseries_module.column, hovered === column.index && timeseries_module.columnActive),
|
|
142
|
+
children: [
|
|
143
|
+
/*#__PURE__*/ jsx("rect", {
|
|
144
|
+
className: timeseries_module.views,
|
|
145
|
+
x: column.x,
|
|
146
|
+
width: column.width,
|
|
147
|
+
y: column.y,
|
|
148
|
+
height: column.height
|
|
149
|
+
}),
|
|
150
|
+
/*#__PURE__*/ jsx("rect", {
|
|
151
|
+
className: timeseries_module.visitors,
|
|
152
|
+
x: column.insetX,
|
|
153
|
+
width: column.insetWidth,
|
|
154
|
+
y: column.insetY,
|
|
155
|
+
height: column.insetHeight
|
|
156
|
+
})
|
|
157
|
+
]
|
|
158
|
+
}, buckets[column.index]?.from))
|
|
159
|
+
]
|
|
160
|
+
}),
|
|
161
|
+
/*#__PURE__*/ jsxs("div", {
|
|
162
|
+
className: clsx('byline-analytics-axis', timeseries_module.axis),
|
|
163
|
+
"aria-hidden": "true",
|
|
164
|
+
children: [
|
|
165
|
+
/*#__PURE__*/ jsx("span", {
|
|
166
|
+
children: buckets.length > 0 && null != buckets[0] ? formatDay(buckets[0].from) : ''
|
|
167
|
+
}),
|
|
168
|
+
/*#__PURE__*/ jsx("span", {
|
|
169
|
+
children: buckets.length > 0 && null != buckets[buckets.length - 1] ? formatDay(buckets[buckets.length - 1]?.to ?? '') : ''
|
|
170
|
+
})
|
|
171
|
+
]
|
|
172
|
+
}),
|
|
173
|
+
/*#__PURE__*/ jsx("p", {
|
|
174
|
+
className: clsx('byline-analytics-readout', timeseries_module.readout),
|
|
175
|
+
"aria-live": "polite",
|
|
176
|
+
children: null == active ? t('analytics.chart.hint') : /*#__PURE__*/ jsxs(Fragment, {
|
|
177
|
+
children: [
|
|
178
|
+
/*#__PURE__*/ jsx("strong", {
|
|
179
|
+
children: formatBucket(active)
|
|
180
|
+
}),
|
|
181
|
+
/*#__PURE__*/ jsx("span", {
|
|
182
|
+
className: timeseries_module.swatchViews
|
|
183
|
+
}),
|
|
184
|
+
t('analytics.stats.views'),
|
|
185
|
+
" ",
|
|
186
|
+
numbers.format(active.views),
|
|
187
|
+
/*#__PURE__*/ jsx("span", {
|
|
188
|
+
className: timeseries_module.swatchVisitors
|
|
189
|
+
}),
|
|
190
|
+
visitorLabel(active),
|
|
191
|
+
" ",
|
|
192
|
+
numbers.format(active.visitors),
|
|
193
|
+
/*#__PURE__*/ jsx("span", {
|
|
194
|
+
className: timeseries_module.swatchDownloads
|
|
195
|
+
}),
|
|
196
|
+
t('analytics.stats.downloads'),
|
|
197
|
+
" ",
|
|
198
|
+
numbers.format(active.downloads)
|
|
199
|
+
]
|
|
200
|
+
})
|
|
201
|
+
})
|
|
202
|
+
]
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
function bucketBySevenDays(days) {
|
|
206
|
+
const buckets = [];
|
|
207
|
+
for(let index = 0; index < days.length; index += 7){
|
|
208
|
+
const rows = days.slice(index, index + 7);
|
|
209
|
+
const first = rows[0];
|
|
210
|
+
const last = rows[rows.length - 1];
|
|
211
|
+
if (null != first && null != last) buckets.push(combineRows(first.day, last.day, 'seven-day', rows));
|
|
212
|
+
}
|
|
213
|
+
return buckets;
|
|
214
|
+
}
|
|
215
|
+
function bucketByUtcMonth(days) {
|
|
216
|
+
const buckets = [];
|
|
217
|
+
let rows = [];
|
|
218
|
+
let month;
|
|
219
|
+
const flush = ()=>{
|
|
220
|
+
const first = rows[0];
|
|
221
|
+
const last = rows[rows.length - 1];
|
|
222
|
+
if (null != first && null != last) buckets.push(combineRows(first.day, last.day, 'month', rows));
|
|
223
|
+
rows = [];
|
|
224
|
+
};
|
|
225
|
+
for (const day of days){
|
|
226
|
+
const nextMonth = day.day.slice(0, 7);
|
|
227
|
+
if (null != month && nextMonth !== month) flush();
|
|
228
|
+
month = nextMonth;
|
|
229
|
+
rows.push(day);
|
|
230
|
+
}
|
|
231
|
+
flush();
|
|
232
|
+
return buckets;
|
|
233
|
+
}
|
|
234
|
+
function combineRows(from, to, granularity, rows) {
|
|
235
|
+
let views = 0;
|
|
236
|
+
let visitors = 0;
|
|
237
|
+
let downloads = 0;
|
|
238
|
+
for (const row of rows){
|
|
239
|
+
views += row.views;
|
|
240
|
+
visitors += row.visitors;
|
|
241
|
+
downloads += row.downloads;
|
|
242
|
+
}
|
|
243
|
+
return {
|
|
244
|
+
from,
|
|
245
|
+
to,
|
|
246
|
+
granularity,
|
|
247
|
+
dayCount: rows.length,
|
|
248
|
+
views,
|
|
249
|
+
visitors,
|
|
250
|
+
downloads
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
export { AnalyticsTimeseries, bucketAnalyticsTimeseries, buildAnalyticsColumns, resolveAnalyticsChartGranularity };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import "./timeseries_module.css";
|
|
2
|
+
const timeseries_module = {
|
|
3
|
+
root: "root-L2qL4D",
|
|
4
|
+
chart: "chart-giYTrx",
|
|
5
|
+
gridline: "gridline-R_wAwP",
|
|
6
|
+
column: "column-_Cw2Ua",
|
|
7
|
+
views: "views-WHbm44",
|
|
8
|
+
columnActive: "columnActive-CwHwbo",
|
|
9
|
+
visitors: "visitors-n6IL4B",
|
|
10
|
+
hit: "hit-ZxDHvk",
|
|
11
|
+
axis: "axis-fBshTw",
|
|
12
|
+
readout: "readout-L9jEpz",
|
|
13
|
+
swatchViews: "swatchViews-OCeKNe",
|
|
14
|
+
swatchVisitors: "swatchVisitors-O3BWz6",
|
|
15
|
+
swatchDownloads: "swatchDownloads-SVo9mN"
|
|
16
|
+
};
|
|
17
|
+
export default timeseries_module;
|