@broberg/app-shell 0.4.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/README.md +49 -0
- package/css/app-shell.css +30 -0
- package/dist/index.cjs +84 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +85 -1
- package/dist/index.d.ts +85 -1
- package/dist/index.js +79 -3
- package/dist/index.js.map +1 -1
- package/dist/preact.cjs +174 -7
- package/dist/preact.cjs.map +1 -1
- package/dist/preact.d.cts +109 -2
- package/dist/preact.d.ts +109 -2
- package/dist/preact.js +168 -8
- package/dist/preact.js.map +1 -1
- package/package.json +1 -1
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;
|
|
@@ -444,6 +528,13 @@ interface AccountPageProps {
|
|
|
444
528
|
* reauth: that is a normal state here, not an error.
|
|
445
529
|
*/
|
|
446
530
|
reauthHref?: string;
|
|
531
|
+
/**
|
|
532
|
+
* F095.5 — called with the profile AS THE SERVER NOW HOLDS IT after every
|
|
533
|
+
* successful change (name saved, picture uploaded, picture removed), so the
|
|
534
|
+
* host can update the shell's user menu at once. Not called on load or on a
|
|
535
|
+
* failed save.
|
|
536
|
+
*/
|
|
537
|
+
onProfileChange?: (profile: AccountProfile) => void;
|
|
447
538
|
}
|
|
448
539
|
/** `/auth/login?returnTo=<this page>` — back here after signing in. */
|
|
449
540
|
declare function defaultReauthHref(): string;
|
|
@@ -455,6 +546,22 @@ declare function defaultReauthHref(): string;
|
|
|
455
546
|
* changes the name, the field shows that. On a failed save the field keeps the
|
|
456
547
|
* typed text, so nothing is lost and the error says why.
|
|
457
548
|
*/
|
|
458
|
-
declare function AccountPage({ lang, adapter, reauthHref }: AccountPageProps): JSX.Element;
|
|
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;
|
|
459
566
|
|
|
460
|
-
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;
|
|
@@ -444,6 +528,13 @@ interface AccountPageProps {
|
|
|
444
528
|
* reauth: that is a normal state here, not an error.
|
|
445
529
|
*/
|
|
446
530
|
reauthHref?: string;
|
|
531
|
+
/**
|
|
532
|
+
* F095.5 — called with the profile AS THE SERVER NOW HOLDS IT after every
|
|
533
|
+
* successful change (name saved, picture uploaded, picture removed), so the
|
|
534
|
+
* host can update the shell's user menu at once. Not called on load or on a
|
|
535
|
+
* failed save.
|
|
536
|
+
*/
|
|
537
|
+
onProfileChange?: (profile: AccountProfile) => void;
|
|
447
538
|
}
|
|
448
539
|
/** `/auth/login?returnTo=<this page>` — back here after signing in. */
|
|
449
540
|
declare function defaultReauthHref(): string;
|
|
@@ -455,6 +546,22 @@ declare function defaultReauthHref(): string;
|
|
|
455
546
|
* changes the name, the field shows that. On a failed save the field keeps the
|
|
456
547
|
* typed text, so nothing is lost and the error says why.
|
|
457
548
|
*/
|
|
458
|
-
declare function AccountPage({ lang, adapter, reauthHref }: AccountPageProps): JSX.Element;
|
|
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;
|
|
459
566
|
|
|
460
|
-
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)";
|
|
@@ -1002,7 +1078,7 @@ function defaultReauthHref() {
|
|
|
1002
1078
|
const here = typeof location === "undefined" ? "/" : location.pathname + location.search;
|
|
1003
1079
|
return `/auth/login?returnTo=${encodeURIComponent(here)}`;
|
|
1004
1080
|
}
|
|
1005
|
-
function AccountPage({ lang, adapter, reauthHref }) {
|
|
1081
|
+
function AccountPage({ lang, adapter, reauthHref, onProfileChange }) {
|
|
1006
1082
|
const t = TEXT[lang];
|
|
1007
1083
|
const api = useMemo(() => adapter ?? createFetchAccountAdapter(), [adapter]);
|
|
1008
1084
|
const [profile, setProfile] = useState(null);
|
|
@@ -1041,7 +1117,9 @@ function AccountPage({ lang, adapter, reauthHref }) {
|
|
|
1041
1117
|
setBusy(what);
|
|
1042
1118
|
setNotice(null);
|
|
1043
1119
|
try {
|
|
1044
|
-
|
|
1120
|
+
const p = await op();
|
|
1121
|
+
apply(p);
|
|
1122
|
+
onProfileChange?.(p);
|
|
1045
1123
|
setNotice({ kind: "ok", text: okText });
|
|
1046
1124
|
} catch (e) {
|
|
1047
1125
|
fail(e, t.couldNotSave);
|
|
@@ -1051,7 +1129,7 @@ function AccountPage({ lang, adapter, reauthHref }) {
|
|
|
1051
1129
|
};
|
|
1052
1130
|
const onSaveName = (e) => {
|
|
1053
1131
|
e.preventDefault();
|
|
1054
|
-
if (busy || reauth) return;
|
|
1132
|
+
if (busy || reauth || unchanged) return;
|
|
1055
1133
|
void run("name", () => api.saveName(draft), t.saved);
|
|
1056
1134
|
};
|
|
1057
1135
|
const onFile = (e) => {
|
|
@@ -1071,6 +1149,7 @@ function AccountPage({ lang, adapter, reauthHref }) {
|
|
|
1071
1149
|
void run("avatar", () => api.removeAvatar(), t.pictureRemoved);
|
|
1072
1150
|
};
|
|
1073
1151
|
const locked = reauth || busy !== null || profile === null;
|
|
1152
|
+
const unchanged = draft.trim() === (profile?.name ?? "").trim();
|
|
1074
1153
|
const user = { name: profile?.name ?? void 0, email: profile?.email ?? void 0, picture: profile?.picture ?? void 0 };
|
|
1075
1154
|
return /* @__PURE__ */ jsxs("section", { class: "bas-account", "data-testid": "account-page", "aria-busy": busy !== null, children: [
|
|
1076
1155
|
reauth ? /* @__PURE__ */ jsxs("div", { class: "bas-account__note is-reauth", role: "alert", "data-testid": "account-reauth", children: [
|
|
@@ -1125,7 +1204,7 @@ function AccountPage({ lang, adapter, reauthHref }) {
|
|
|
1125
1204
|
/* @__PURE__ */ jsx("button", { type: "button", class: "bas-account__btn is-quiet", onClick: () => setConfirming(false), "data-testid": "account-avatar-remove-cancel", children: t.cancel })
|
|
1126
1205
|
] }) : null
|
|
1127
1206
|
] }),
|
|
1128
|
-
/* @__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: [
|
|
1129
1208
|
/* @__PURE__ */ jsx("label", { class: "bas-account__label", for: "bas-account-name", children: t.name }),
|
|
1130
1209
|
/* @__PURE__ */ jsxs("div", { class: "bas-account__row", children: [
|
|
1131
1210
|
/* @__PURE__ */ jsx(
|
|
@@ -1145,14 +1224,95 @@ function AccountPage({ lang, adapter, reauthHref }) {
|
|
|
1145
1224
|
"data-testid": "account-name-input"
|
|
1146
1225
|
}
|
|
1147
1226
|
),
|
|
1148
|
-
/* @__PURE__ */ jsx("button", { type: "submit", class: "bas-account__btn is-primary", disabled: locked, "data-testid": "account-name-save", children: busy === "name" ? t.saving : t.save })
|
|
1227
|
+
/* @__PURE__ */ jsx("button", { type: "submit", class: "bas-account__btn is-primary", disabled: locked || unchanged, "data-testid": "account-name-save", children: busy === "name" ? t.saving : t.save })
|
|
1149
1228
|
] })
|
|
1150
1229
|
] }),
|
|
1151
1230
|
profile.account_url ? /* @__PURE__ */ jsx(NavLink, { href: profile.account_url, class: "bas-account__link", "data-testid": "account-bid-link", children: t.bidSecurity }) : null
|
|
1152
1231
|
] }) : null
|
|
1153
1232
|
] });
|
|
1154
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
|
+
}
|
|
1155
1315
|
|
|
1156
|
-
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 };
|
|
1157
1317
|
//# sourceMappingURL=preact.js.map
|
|
1158
1318
|
//# sourceMappingURL=preact.js.map
|