@broberg/app-shell 0.5.0 → 0.6.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/preact.d.cts CHANGED
@@ -153,6 +153,62 @@ declare function createFetchAccountAdapter(opts?: {
153
153
  fetch?: typeof fetch;
154
154
  credentials?: RequestCredentials;
155
155
  }): AccountAdapter;
156
+ interface AnalyticsDay {
157
+ /** YYYY-MM-DD on the Europe/Copenhagen calendar (Upmetrics' own day key). */
158
+ day: string;
159
+ visitors: number;
160
+ pageviews: number;
161
+ }
162
+ interface AnalyticsData {
163
+ totals: {
164
+ visitors: number;
165
+ pageviews: number;
166
+ };
167
+ /** The same-length period just before this one. */
168
+ prev: {
169
+ visitors: number;
170
+ pageviews: number;
171
+ };
172
+ /** Only days that had visits — use fillDays() to draw the whole period. */
173
+ series: AnalyticsDay[];
174
+ topPages: {
175
+ value: string;
176
+ count: number;
177
+ }[];
178
+ }
179
+ interface AnalyticsAdapter {
180
+ load(days: number): Promise<AnalyticsData>;
181
+ }
182
+ /** A failed analytics call: HTTP status (0 = no answer) and the server's `error` code. */
183
+ declare class AnalyticsError extends Error {
184
+ readonly status: number;
185
+ readonly code?: string | undefined;
186
+ constructor(status: number, code?: string | undefined);
187
+ }
188
+ /** "unconfigured" only for the backend's explicit 503 analytics_unconfigured; anything else is "unavailable". */
189
+ declare function analyticsErrorKind(err: unknown): "unconfigured" | "unavailable";
190
+ /**
191
+ * The default adapter: `GET {url}?days=N` on the app's own backend (default
192
+ * /api/analytics, where `@upmetrics/sdk/hono`'s analyticsRoutes() answers).
193
+ * A non-2xx, an unreadable body or no answer at all THROWS — never zeros that
194
+ * look like a real, quiet month.
195
+ */
196
+ declare function createFetchAnalyticsAdapter(opts?: {
197
+ url?: string;
198
+ fetch?: typeof fetch;
199
+ credentials?: RequestCredentials;
200
+ }): AnalyticsAdapter;
201
+ /** Today's YYYY-MM-DD on the Copenhagen calendar — the calendar Upmetrics buckets on. */
202
+ declare function copenhagenDay(at?: Date): string;
203
+ /**
204
+ * Exactly `days` entries ending today (Copenhagen calendar), with Upmetrics'
205
+ * values where it sent a day and zeros where it did not — Upmetrics only sends
206
+ * days that had visits. Calendar arithmetic, so a DST change never skips or
207
+ * repeats a day.
208
+ */
209
+ declare function fillDays(series: AnalyticsDay[], days: number, now?: Date): AnalyticsDay[];
210
+ /** Whole-percent change against the previous period, or null when there is nothing to compare with. */
211
+ declare function changePct(now: number, before: number): number | null;
156
212
  declare const TEXT: {
157
213
  readonly da: {
158
214
  readonly openMenu: "Åbn menu";
@@ -201,6 +257,20 @@ declare const TEXT: {
201
257
  readonly pictureTooLarge: "Billedet er for stort — højst 2 MB.";
202
258
  readonly pictureWrongType: "Vælg et PNG-, JPEG- eller WebP-billede.";
203
259
  readonly bidSecurity: "Sikkerhed i Broberg ID";
260
+ readonly analyticsPeriod: "Periode";
261
+ readonly analyticsDays: "{n} dage";
262
+ readonly analyticsVisitors: "Besøgende";
263
+ readonly analyticsPageviews: "Sidevisninger";
264
+ readonly analyticsVsPrev: "{pct} % mod forrige periode";
265
+ readonly analyticsNoPrev: "Ingen sammenligning";
266
+ readonly analyticsOverTime: "Sidevisninger pr. dag";
267
+ readonly analyticsTopPages: "Mest besøgte sider";
268
+ readonly analyticsDay: "Dag";
269
+ readonly analyticsPage: "Side";
270
+ readonly analyticsEmpty: "Ingen besøg i perioden.";
271
+ readonly analyticsLoading: "Henter tal…";
272
+ readonly analyticsUnconfigured: "Analytics er ikke sat op for denne app.";
273
+ readonly analyticsUnavailable: "Upmetrics svarer ikke lige nu. Prøv igen om lidt.";
204
274
  };
205
275
  readonly en: {
206
276
  readonly openMenu: "Open menu";
@@ -249,6 +319,20 @@ declare const TEXT: {
249
319
  readonly pictureTooLarge: "The picture is too large — 2 MB at most.";
250
320
  readonly pictureWrongType: "Choose a PNG, JPEG or WebP image.";
251
321
  readonly bidSecurity: "Security in Broberg ID";
322
+ readonly analyticsPeriod: "Period";
323
+ readonly analyticsDays: "{n} days";
324
+ readonly analyticsVisitors: "Visitors";
325
+ readonly analyticsPageviews: "Page views";
326
+ readonly analyticsVsPrev: "{pct}% vs previous period";
327
+ readonly analyticsNoPrev: "No comparison";
328
+ readonly analyticsOverTime: "Page views per day";
329
+ readonly analyticsTopPages: "Most visited pages";
330
+ readonly analyticsDay: "Day";
331
+ readonly analyticsPage: "Page";
332
+ readonly analyticsEmpty: "No visits in this period.";
333
+ readonly analyticsLoading: "Loading figures…";
334
+ readonly analyticsUnconfigured: "Analytics is not set up for this app.";
335
+ readonly analyticsUnavailable: "Upmetrics is not answering right now. Try again shortly.";
252
336
  };
253
337
  };
254
338
  type Lang = keyof typeof TEXT;
@@ -463,5 +547,21 @@ declare function defaultReauthHref(): string;
463
547
  * typed text, so nothing is lost and the error says why.
464
548
  */
465
549
  declare function AccountPage({ lang, adapter, reauthHref, onProfileChange }: AccountPageProps): JSX.Element;
550
+ interface AnalyticsPageProps {
551
+ lang: Lang;
552
+ /** Default: `createFetchAnalyticsAdapter()` → GET /api/analytics?days=N (@upmetrics/sdk/hono analyticsRoutes). */
553
+ adapter?: AnalyticsAdapter;
554
+ /** The period buttons, in days. Default 7 / 30 / 90. */
555
+ periods?: number[];
556
+ /** The period shown first. Default 30 (or the first of `periods` when 30 is not one of them). */
557
+ initialDays?: number;
558
+ }
559
+ /**
560
+ * The app's own visitors, page views, page views per day and most visited
561
+ * pages, from Upmetrics through the app's backend (F097.1). A backend that is
562
+ * not set up or does not answer says so — it never shows zeros that would read
563
+ * as a quiet month.
564
+ */
565
+ declare function AnalyticsPage({ lang, adapter, periods, initialDays }: AnalyticsPageProps): JSX.Element;
466
566
 
467
- export { AVATAR_MAX_BYTES, AVATAR_TYPES, type AccountAdapter, AccountError, AccountPage, type AccountPageProps, type AccountProfile, AppShell, type AppShellProps, Avatar, type Lang, type NavGroup, type NavItem, type Navigate, NotificationBell, type NotificationBellProps, type NotificationSource, PageHeader, type PageTab, PageTabs, type PreactNavGroup, type PreactNavItem, type ShellUser, Sidebar, type SidebarProps, TEXT, TopBar, type TopBarProps, UserMenu, type UserMenuItem, type UserMenuProps, accountErrorKind, activeNavLabel, badgeLabel, checkAvatarFile, createFetchAccountAdapter, createFetchNotificationSource, createMemoryNotificationSource, defaultReauthHref, initials, isActivePath, isExternal, readFlag, readSet, relativeTime, safeNavigate, useIsMobile, writeFlag, writeSet };
567
+ export { AVATAR_MAX_BYTES, AVATAR_TYPES, type AccountAdapter, AccountError, AccountPage, type AccountPageProps, type AccountProfile, type AnalyticsAdapter, type AnalyticsData, type AnalyticsDay, AnalyticsError, AnalyticsPage, type AnalyticsPageProps, AppShell, type AppShellProps, Avatar, type Lang, type NavGroup, type NavItem, type Navigate, NotificationBell, type NotificationBellProps, type NotificationSource, PageHeader, type PageTab, PageTabs, type PreactNavGroup, type PreactNavItem, type ShellUser, Sidebar, type SidebarProps, TEXT, TopBar, type TopBarProps, UserMenu, type UserMenuItem, type UserMenuProps, accountErrorKind, activeNavLabel, analyticsErrorKind, badgeLabel, changePct, checkAvatarFile, copenhagenDay, createFetchAccountAdapter, createFetchAnalyticsAdapter, createFetchNotificationSource, createMemoryNotificationSource, defaultReauthHref, fillDays, initials, isActivePath, isExternal, readFlag, readSet, relativeTime, safeNavigate, useIsMobile, writeFlag, writeSet };
package/dist/preact.d.ts CHANGED
@@ -153,6 +153,62 @@ declare function createFetchAccountAdapter(opts?: {
153
153
  fetch?: typeof fetch;
154
154
  credentials?: RequestCredentials;
155
155
  }): AccountAdapter;
156
+ interface AnalyticsDay {
157
+ /** YYYY-MM-DD on the Europe/Copenhagen calendar (Upmetrics' own day key). */
158
+ day: string;
159
+ visitors: number;
160
+ pageviews: number;
161
+ }
162
+ interface AnalyticsData {
163
+ totals: {
164
+ visitors: number;
165
+ pageviews: number;
166
+ };
167
+ /** The same-length period just before this one. */
168
+ prev: {
169
+ visitors: number;
170
+ pageviews: number;
171
+ };
172
+ /** Only days that had visits — use fillDays() to draw the whole period. */
173
+ series: AnalyticsDay[];
174
+ topPages: {
175
+ value: string;
176
+ count: number;
177
+ }[];
178
+ }
179
+ interface AnalyticsAdapter {
180
+ load(days: number): Promise<AnalyticsData>;
181
+ }
182
+ /** A failed analytics call: HTTP status (0 = no answer) and the server's `error` code. */
183
+ declare class AnalyticsError extends Error {
184
+ readonly status: number;
185
+ readonly code?: string | undefined;
186
+ constructor(status: number, code?: string | undefined);
187
+ }
188
+ /** "unconfigured" only for the backend's explicit 503 analytics_unconfigured; anything else is "unavailable". */
189
+ declare function analyticsErrorKind(err: unknown): "unconfigured" | "unavailable";
190
+ /**
191
+ * The default adapter: `GET {url}?days=N` on the app's own backend (default
192
+ * /api/analytics, where `@upmetrics/sdk/hono`'s analyticsRoutes() answers).
193
+ * A non-2xx, an unreadable body or no answer at all THROWS — never zeros that
194
+ * look like a real, quiet month.
195
+ */
196
+ declare function createFetchAnalyticsAdapter(opts?: {
197
+ url?: string;
198
+ fetch?: typeof fetch;
199
+ credentials?: RequestCredentials;
200
+ }): AnalyticsAdapter;
201
+ /** Today's YYYY-MM-DD on the Copenhagen calendar — the calendar Upmetrics buckets on. */
202
+ declare function copenhagenDay(at?: Date): string;
203
+ /**
204
+ * Exactly `days` entries ending today (Copenhagen calendar), with Upmetrics'
205
+ * values where it sent a day and zeros where it did not — Upmetrics only sends
206
+ * days that had visits. Calendar arithmetic, so a DST change never skips or
207
+ * repeats a day.
208
+ */
209
+ declare function fillDays(series: AnalyticsDay[], days: number, now?: Date): AnalyticsDay[];
210
+ /** Whole-percent change against the previous period, or null when there is nothing to compare with. */
211
+ declare function changePct(now: number, before: number): number | null;
156
212
  declare const TEXT: {
157
213
  readonly da: {
158
214
  readonly openMenu: "Åbn menu";
@@ -201,6 +257,20 @@ declare const TEXT: {
201
257
  readonly pictureTooLarge: "Billedet er for stort — højst 2 MB.";
202
258
  readonly pictureWrongType: "Vælg et PNG-, JPEG- eller WebP-billede.";
203
259
  readonly bidSecurity: "Sikkerhed i Broberg ID";
260
+ readonly analyticsPeriod: "Periode";
261
+ readonly analyticsDays: "{n} dage";
262
+ readonly analyticsVisitors: "Besøgende";
263
+ readonly analyticsPageviews: "Sidevisninger";
264
+ readonly analyticsVsPrev: "{pct} % mod forrige periode";
265
+ readonly analyticsNoPrev: "Ingen sammenligning";
266
+ readonly analyticsOverTime: "Sidevisninger pr. dag";
267
+ readonly analyticsTopPages: "Mest besøgte sider";
268
+ readonly analyticsDay: "Dag";
269
+ readonly analyticsPage: "Side";
270
+ readonly analyticsEmpty: "Ingen besøg i perioden.";
271
+ readonly analyticsLoading: "Henter tal…";
272
+ readonly analyticsUnconfigured: "Analytics er ikke sat op for denne app.";
273
+ readonly analyticsUnavailable: "Upmetrics svarer ikke lige nu. Prøv igen om lidt.";
204
274
  };
205
275
  readonly en: {
206
276
  readonly openMenu: "Open menu";
@@ -249,6 +319,20 @@ declare const TEXT: {
249
319
  readonly pictureTooLarge: "The picture is too large — 2 MB at most.";
250
320
  readonly pictureWrongType: "Choose a PNG, JPEG or WebP image.";
251
321
  readonly bidSecurity: "Security in Broberg ID";
322
+ readonly analyticsPeriod: "Period";
323
+ readonly analyticsDays: "{n} days";
324
+ readonly analyticsVisitors: "Visitors";
325
+ readonly analyticsPageviews: "Page views";
326
+ readonly analyticsVsPrev: "{pct}% vs previous period";
327
+ readonly analyticsNoPrev: "No comparison";
328
+ readonly analyticsOverTime: "Page views per day";
329
+ readonly analyticsTopPages: "Most visited pages";
330
+ readonly analyticsDay: "Day";
331
+ readonly analyticsPage: "Page";
332
+ readonly analyticsEmpty: "No visits in this period.";
333
+ readonly analyticsLoading: "Loading figures…";
334
+ readonly analyticsUnconfigured: "Analytics is not set up for this app.";
335
+ readonly analyticsUnavailable: "Upmetrics is not answering right now. Try again shortly.";
252
336
  };
253
337
  };
254
338
  type Lang = keyof typeof TEXT;
@@ -463,5 +547,21 @@ declare function defaultReauthHref(): string;
463
547
  * typed text, so nothing is lost and the error says why.
464
548
  */
465
549
  declare function AccountPage({ lang, adapter, reauthHref, onProfileChange }: AccountPageProps): JSX.Element;
550
+ interface AnalyticsPageProps {
551
+ lang: Lang;
552
+ /** Default: `createFetchAnalyticsAdapter()` → GET /api/analytics?days=N (@upmetrics/sdk/hono analyticsRoutes). */
553
+ adapter?: AnalyticsAdapter;
554
+ /** The period buttons, in days. Default 7 / 30 / 90. */
555
+ periods?: number[];
556
+ /** The period shown first. Default 30 (or the first of `periods` when 30 is not one of them). */
557
+ initialDays?: number;
558
+ }
559
+ /**
560
+ * The app's own visitors, page views, page views per day and most visited
561
+ * pages, from Upmetrics through the app's backend (F097.1). A backend that is
562
+ * not set up or does not answer says so — it never shows zeros that would read
563
+ * as a quiet month.
564
+ */
565
+ declare function AnalyticsPage({ lang, adapter, periods, initialDays }: AnalyticsPageProps): JSX.Element;
466
566
 
467
- export { AVATAR_MAX_BYTES, AVATAR_TYPES, type AccountAdapter, AccountError, AccountPage, type AccountPageProps, type AccountProfile, AppShell, type AppShellProps, Avatar, type Lang, type NavGroup, type NavItem, type Navigate, NotificationBell, type NotificationBellProps, type NotificationSource, PageHeader, type PageTab, PageTabs, type PreactNavGroup, type PreactNavItem, type ShellUser, Sidebar, type SidebarProps, TEXT, TopBar, type TopBarProps, UserMenu, type UserMenuItem, type UserMenuProps, accountErrorKind, activeNavLabel, badgeLabel, checkAvatarFile, createFetchAccountAdapter, createFetchNotificationSource, createMemoryNotificationSource, defaultReauthHref, initials, isActivePath, isExternal, readFlag, readSet, relativeTime, safeNavigate, useIsMobile, writeFlag, writeSet };
567
+ export { AVATAR_MAX_BYTES, AVATAR_TYPES, type AccountAdapter, AccountError, AccountPage, type AccountPageProps, type AccountProfile, type AnalyticsAdapter, type AnalyticsData, type AnalyticsDay, AnalyticsError, AnalyticsPage, type AnalyticsPageProps, AppShell, type AppShellProps, Avatar, type Lang, type NavGroup, type NavItem, type Navigate, NotificationBell, type NotificationBellProps, type NotificationSource, PageHeader, type PageTab, PageTabs, type PreactNavGroup, type PreactNavItem, type ShellUser, Sidebar, type SidebarProps, TEXT, TopBar, type TopBarProps, UserMenu, type UserMenuItem, type UserMenuProps, accountErrorKind, activeNavLabel, analyticsErrorKind, badgeLabel, changePct, checkAvatarFile, copenhagenDay, createFetchAccountAdapter, createFetchAnalyticsAdapter, createFetchNotificationSource, createMemoryNotificationSource, defaultReauthHref, fillDays, initials, isActivePath, isExternal, readFlag, readSet, relativeTime, safeNavigate, useIsMobile, writeFlag, writeSet };
package/dist/preact.js CHANGED
@@ -224,6 +224,54 @@ function createFetchAccountAdapter(opts = {}) {
224
224
  removeAvatar: () => call("/profile/avatar/remove", { method: "POST" })
225
225
  };
226
226
  }
227
+ var AnalyticsError = class extends Error {
228
+ constructor(status, code) {
229
+ super(code ? `HTTP ${status} ${code}` : `HTTP ${status}`);
230
+ this.status = status;
231
+ this.code = code;
232
+ this.name = "AnalyticsError";
233
+ }
234
+ status;
235
+ code;
236
+ };
237
+ function analyticsErrorKind(err) {
238
+ return err instanceof AnalyticsError && err.status === 503 && err.code === "analytics_unconfigured" ? "unconfigured" : "unavailable";
239
+ }
240
+ function createFetchAnalyticsAdapter(opts = {}) {
241
+ const url = opts.url ?? "/api/analytics";
242
+ return {
243
+ async load(days) {
244
+ let res;
245
+ try {
246
+ res = await (opts.fetch ?? fetch)(`${url}${url.includes("?") ? "&" : "?"}days=${encodeURIComponent(String(days))}`, {
247
+ credentials: opts.credentials ?? "same-origin",
248
+ headers: { accept: "application/json" }
249
+ });
250
+ } catch {
251
+ throw new AnalyticsError(0);
252
+ }
253
+ const body = await res.json().catch(() => null);
254
+ if (!res.ok) throw new AnalyticsError(res.status, typeof body?.error === "string" ? body.error : void 0);
255
+ const ok = body !== null && typeof body.totals?.visitors === "number" && Array.isArray(body.series) && Array.isArray(body.topPages);
256
+ if (!ok) throw new AnalyticsError(res.status, "bad_response");
257
+ return body;
258
+ }
259
+ };
260
+ }
261
+ function copenhagenDay(at = /* @__PURE__ */ new Date()) {
262
+ return new Intl.DateTimeFormat("en-CA", { timeZone: "Europe/Copenhagen", year: "numeric", month: "2-digit", day: "2-digit" }).format(at);
263
+ }
264
+ function fillDays(series, days, now = /* @__PURE__ */ new Date()) {
265
+ const byDay = new Map(series.map((d2) => [d2.day, d2]));
266
+ const [y, m, d] = copenhagenDay(now).split("-").map(Number);
267
+ return Array.from({ length: days }, (_, i) => {
268
+ const day = new Date(Date.UTC(y, m - 1, d - (days - 1 - i))).toISOString().slice(0, 10);
269
+ return byDay.get(day) ?? { day, visitors: 0, pageviews: 0 };
270
+ });
271
+ }
272
+ function changePct(now, before) {
273
+ return before === 0 ? null : Math.round((now - before) / before * 100);
274
+ }
227
275
  var TEXT = {
228
276
  da: {
229
277
  openMenu: "\xC5bn menu",
@@ -271,7 +319,21 @@ var TEXT = {
271
319
  pictureRemoved: "Billedet er fjernet",
272
320
  pictureTooLarge: "Billedet er for stort \u2014 h\xF8jst 2 MB.",
273
321
  pictureWrongType: "V\xE6lg et PNG-, JPEG- eller WebP-billede.",
274
- bidSecurity: "Sikkerhed i Broberg ID"
322
+ bidSecurity: "Sikkerhed i Broberg ID",
323
+ analyticsPeriod: "Periode",
324
+ analyticsDays: "{n} dage",
325
+ analyticsVisitors: "Bes\xF8gende",
326
+ analyticsPageviews: "Sidevisninger",
327
+ analyticsVsPrev: "{pct} % mod forrige periode",
328
+ analyticsNoPrev: "Ingen sammenligning",
329
+ analyticsOverTime: "Sidevisninger pr. dag",
330
+ analyticsTopPages: "Mest bes\xF8gte sider",
331
+ analyticsDay: "Dag",
332
+ analyticsPage: "Side",
333
+ analyticsEmpty: "Ingen bes\xF8g i perioden.",
334
+ analyticsLoading: "Henter tal\u2026",
335
+ analyticsUnconfigured: "Analytics er ikke sat op for denne app.",
336
+ analyticsUnavailable: "Upmetrics svarer ikke lige nu. Pr\xF8v igen om lidt."
275
337
  },
276
338
  en: {
277
339
  openMenu: "Open menu",
@@ -319,7 +381,21 @@ var TEXT = {
319
381
  pictureRemoved: "Picture removed",
320
382
  pictureTooLarge: "The picture is too large \u2014 2 MB at most.",
321
383
  pictureWrongType: "Choose a PNG, JPEG or WebP image.",
322
- bidSecurity: "Security in Broberg ID"
384
+ bidSecurity: "Security in Broberg ID",
385
+ analyticsPeriod: "Period",
386
+ analyticsDays: "{n} days",
387
+ analyticsVisitors: "Visitors",
388
+ analyticsPageviews: "Page views",
389
+ analyticsVsPrev: "{pct}% vs previous period",
390
+ analyticsNoPrev: "No comparison",
391
+ analyticsOverTime: "Page views per day",
392
+ analyticsTopPages: "Most visited pages",
393
+ analyticsDay: "Day",
394
+ analyticsPage: "Page",
395
+ analyticsEmpty: "No visits in this period.",
396
+ analyticsLoading: "Loading figures\u2026",
397
+ analyticsUnconfigured: "Analytics is not set up for this app.",
398
+ analyticsUnavailable: "Upmetrics is not answering right now. Try again shortly."
323
399
  }
324
400
  };
325
401
  var MOBILE_QUERY = "(max-width: 767px)";
@@ -1128,7 +1204,7 @@ function AccountPage({ lang, adapter, reauthHref, onProfileChange }) {
1128
1204
  /* @__PURE__ */ jsx("button", { type: "button", class: "bas-account__btn is-quiet", onClick: () => setConfirming(false), "data-testid": "account-avatar-remove-cancel", children: t.cancel })
1129
1205
  ] }) : null
1130
1206
  ] }),
1131
- /* @__PURE__ */ jsxs("form", { class: "bas-account__form", onSubmit: onSaveName, children: [
1207
+ /* @__PURE__ */ jsxs("form", { class: "bas-account__form", onSubmit: onSaveName, "data-testid": "account-name-form", children: [
1132
1208
  /* @__PURE__ */ jsx("label", { class: "bas-account__label", for: "bas-account-name", children: t.name }),
1133
1209
  /* @__PURE__ */ jsxs("div", { class: "bas-account__row", children: [
1134
1210
  /* @__PURE__ */ jsx(
@@ -1155,7 +1231,88 @@ function AccountPage({ lang, adapter, reauthHref, onProfileChange }) {
1155
1231
  ] }) : null
1156
1232
  ] });
1157
1233
  }
1234
+ function AnalyticsPage({ lang, adapter, periods = [7, 30, 90], initialDays }) {
1235
+ const t = TEXT[lang];
1236
+ const api = useMemo(() => adapter ?? createFetchAnalyticsAdapter(), [adapter]);
1237
+ const [days, setDays] = useState(initialDays ?? (periods.includes(30) ? 30 : periods[0]));
1238
+ const [load, setLoad] = useState({ state: "loading" });
1239
+ const locale = lang === "da" ? "da-DK" : "en-GB";
1240
+ const num = (n) => n.toLocaleString(locale);
1241
+ useEffect(() => {
1242
+ let live = true;
1243
+ setLoad({ state: "loading" });
1244
+ api.load(days).then(
1245
+ (data) => live && setLoad({ state: "ok", days, data }),
1246
+ (e) => live && setLoad({ state: "error", reason: analyticsErrorKind(e) })
1247
+ );
1248
+ return () => {
1249
+ live = false;
1250
+ };
1251
+ }, [api, days]);
1252
+ return /* @__PURE__ */ jsxs("section", { class: "bas-analytics", "data-testid": "analytics-page", "aria-busy": load.state === "loading", children: [
1253
+ /* @__PURE__ */ jsx("div", { class: "bas-analytics__periods", role: "group", "aria-label": t.analyticsPeriod, children: periods.map((d) => /* @__PURE__ */ jsx(
1254
+ "button",
1255
+ {
1256
+ type: "button",
1257
+ class: "bas-analytics__period" + (d === days ? " is-active" : ""),
1258
+ "aria-pressed": d === days,
1259
+ onClick: () => setDays(d),
1260
+ "data-testid": `analytics-period-${d}`,
1261
+ children: t.analyticsDays.replace("{n}", String(d))
1262
+ },
1263
+ d
1264
+ )) }),
1265
+ load.state === "loading" ? /* @__PURE__ */ jsx("p", { class: "bas-analytics__muted", "data-testid": "analytics-loading", children: t.analyticsLoading }) : null,
1266
+ load.state === "error" ? /* @__PURE__ */ jsx("p", { class: "bas-analytics__note", role: "alert", "data-testid": "analytics-error", "data-reason": load.reason, children: load.reason === "unconfigured" ? t.analyticsUnconfigured : t.analyticsUnavailable }) : null,
1267
+ load.state === "ok" ? /* @__PURE__ */ jsxs(Fragment, { children: [
1268
+ /* @__PURE__ */ jsx("div", { class: "bas-analytics__tiles", children: ["visitors", "pageviews"].map((k) => {
1269
+ const c = changePct(load.data.totals[k], load.data.prev[k]);
1270
+ return /* @__PURE__ */ jsxs("div", { class: "bas-analytics__tile", "data-testid": `analytics-${k}`, children: [
1271
+ /* @__PURE__ */ jsx("div", { class: "bas-analytics__label", children: k === "visitors" ? t.analyticsVisitors : t.analyticsPageviews }),
1272
+ /* @__PURE__ */ jsx("div", { class: "bas-analytics__value", "data-testid": `analytics-${k}-value`, children: num(load.data.totals[k]) }),
1273
+ /* @__PURE__ */ jsx("div", { class: "bas-analytics__muted", "data-testid": `analytics-${k}-change`, children: c === null ? t.analyticsNoPrev : t.analyticsVsPrev.replace("{pct}", `${c > 0 ? "+" : ""}${c}`) })
1274
+ ] }, k);
1275
+ }) }),
1276
+ /* @__PURE__ */ jsx(AnalyticsSeries, { data: fillDays(load.data.series, load.days), t, locale, num }),
1277
+ /* @__PURE__ */ jsxs("div", { class: "bas-analytics__card", "data-testid": "analytics-top-pages", children: [
1278
+ /* @__PURE__ */ jsx("h2", { class: "bas-analytics__h", children: t.analyticsTopPages }),
1279
+ load.data.topPages.length === 0 ? /* @__PURE__ */ jsx("p", { class: "bas-analytics__muted", "data-testid": "analytics-top-pages-empty", children: t.analyticsEmpty }) : /* @__PURE__ */ jsx("ol", { class: "bas-analytics__pages", children: load.data.topPages.map((p) => /* @__PURE__ */ jsxs("li", { class: "bas-analytics__page", children: [
1280
+ /* @__PURE__ */ jsx("span", { class: "bas-analytics__path", title: p.value, children: p.value }),
1281
+ /* @__PURE__ */ jsx("span", { class: "bas-analytics__count", children: num(p.count) })
1282
+ ] }, p.value)) })
1283
+ ] })
1284
+ ] }) : null
1285
+ ] });
1286
+ }
1287
+ function dayLabel(day, locale) {
1288
+ return (/* @__PURE__ */ new Date(`${day}T12:00:00Z`)).toLocaleDateString(locale, { day: "numeric", month: "short", timeZone: "UTC" });
1289
+ }
1290
+ function AnalyticsSeries({ data, t, locale, num }) {
1291
+ const max = Math.max(1, ...data.map((d) => d.pageviews));
1292
+ const empty = data.every((d) => d.pageviews === 0);
1293
+ return /* @__PURE__ */ jsxs("div", { class: "bas-analytics__card", "data-testid": "analytics-series", children: [
1294
+ /* @__PURE__ */ jsx("h2", { class: "bas-analytics__h", children: t.analyticsOverTime }),
1295
+ empty ? /* @__PURE__ */ jsx("p", { class: "bas-analytics__muted", "data-testid": "analytics-series-empty", children: t.analyticsEmpty }) : /* @__PURE__ */ jsxs(Fragment, { children: [
1296
+ /* @__PURE__ */ jsx("div", { class: "bas-analytics__bars", "aria-hidden": "true", children: data.map((d) => /* @__PURE__ */ jsx("div", { class: "bas-analytics__col", title: `${dayLabel(d.day, locale)}: ${num(d.pageviews)}`, "data-day": d.day, children: /* @__PURE__ */ jsx("div", { class: "bas-analytics__bar", style: { height: d.pageviews === 0 ? "0" : `max(2px, ${d.pageviews / max * 100}%)` } }) }, d.day)) }),
1297
+ /* @__PURE__ */ jsxs("div", { class: "bas-analytics__axis", "aria-hidden": "true", children: [
1298
+ /* @__PURE__ */ jsx("span", { children: dayLabel(data[0].day, locale) }),
1299
+ /* @__PURE__ */ jsx("span", { children: dayLabel(data[data.length - 1].day, locale) })
1300
+ ] }),
1301
+ /* @__PURE__ */ jsxs("table", { class: "bas-analytics__sr", "data-testid": "analytics-series-table", children: [
1302
+ /* @__PURE__ */ jsx("caption", { children: t.analyticsOverTime }),
1303
+ /* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { children: [
1304
+ /* @__PURE__ */ jsx("th", { scope: "col", children: t.analyticsDay }),
1305
+ /* @__PURE__ */ jsx("th", { scope: "col", children: t.analyticsPageviews })
1306
+ ] }) }),
1307
+ /* @__PURE__ */ jsx("tbody", { children: data.map((d) => /* @__PURE__ */ jsxs("tr", { children: [
1308
+ /* @__PURE__ */ jsx("td", { children: dayLabel(d.day, locale) }),
1309
+ /* @__PURE__ */ jsx("td", { children: num(d.pageviews) })
1310
+ ] }, d.day)) })
1311
+ ] })
1312
+ ] })
1313
+ ] });
1314
+ }
1158
1315
 
1159
- export { AVATAR_MAX_BYTES, AVATAR_TYPES, AccountError, AccountPage, AppShell, Avatar, NotificationBell, PageHeader, PageTabs, Sidebar, TEXT, TopBar, UserMenu, accountErrorKind, activeNavLabel, badgeLabel, checkAvatarFile, createFetchAccountAdapter, createFetchNotificationSource, createMemoryNotificationSource, defaultReauthHref, initials, isActivePath, isExternal, readFlag, readSet, relativeTime, safeNavigate, useIsMobile, writeFlag, writeSet };
1316
+ export { AVATAR_MAX_BYTES, AVATAR_TYPES, AccountError, AccountPage, AnalyticsError, AnalyticsPage, AppShell, Avatar, NotificationBell, PageHeader, PageTabs, Sidebar, TEXT, TopBar, UserMenu, accountErrorKind, activeNavLabel, analyticsErrorKind, badgeLabel, changePct, checkAvatarFile, copenhagenDay, createFetchAccountAdapter, createFetchAnalyticsAdapter, createFetchNotificationSource, createMemoryNotificationSource, defaultReauthHref, fillDays, initials, isActivePath, isExternal, readFlag, readSet, relativeTime, safeNavigate, useIsMobile, writeFlag, writeSet };
1160
1317
  //# sourceMappingURL=preact.js.map
1161
1318
  //# sourceMappingURL=preact.js.map