@broberg/app-shell 0.3.1 → 0.4.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
@@ -1,4 +1,4 @@
1
- import { ComponentChildren, JSX } from 'preact';
1
+ import { JSX, ComponentChildren } from 'preact';
2
2
  import { NotificationRow } from '@broberg/notifications/types';
3
3
  export { NotificationRow } from '@broberg/notifications/types';
4
4
 
@@ -101,6 +101,58 @@ declare function createMemoryNotificationSource(initial?: NotificationRow[]): No
101
101
  };
102
102
  /** "nu", "5 min", "3 t", "i går", "4 d" (da) or the English equivalents. */
103
103
  declare function relativeTime(ts: string | number, lang: "da" | "en", now?: number): string;
104
+ /** What every account call resolves to — the profile AS THE SERVER NOW HOLDS IT. */
105
+ interface AccountProfile {
106
+ sub: string;
107
+ name: string | null;
108
+ picture: string | null;
109
+ email: string | null;
110
+ /** Broberg ID's own account page (password, passkeys, sessions). */
111
+ account_url: string;
112
+ }
113
+ /** Where the account page gets and saves the profile. Swap it to point anywhere. */
114
+ interface AccountAdapter {
115
+ load(): Promise<AccountProfile>;
116
+ saveName(name: string): Promise<AccountProfile>;
117
+ uploadAvatar(file: Blob): Promise<AccountProfile>;
118
+ removeAvatar(): Promise<AccountProfile>;
119
+ }
120
+ /** A failed account call: the HTTP status and the server's `error` code, if any. */
121
+ declare class AccountError extends Error {
122
+ readonly status: number;
123
+ readonly code?: string | undefined;
124
+ constructor(status: number, code?: string | undefined);
125
+ }
126
+ declare const AVATAR_TYPES: readonly ["image/png", "image/jpeg", "image/webp"];
127
+ declare const AVATAR_MAX_BYTES: number;
128
+ /** Checked BEFORE uploading, so a file the server will refuse is never sent. */
129
+ declare function checkAvatarFile(file: {
130
+ size: number;
131
+ type: string;
132
+ }): "tooLarge" | "wrongType" | null;
133
+ /**
134
+ * What a failed call means for the page. `reauth` = the user's session or its
135
+ * scope does not allow the change (401, or 403 insufficient_scope): signing in
136
+ * again is the fix, so the page says that instead of «try again».
137
+ */
138
+ declare function accountErrorKind(err: unknown): "reauth" | "tooLarge" | "wrongType" | "other";
139
+ /**
140
+ * The default adapter: the app's own backend, shaped after @broberg/sso's
141
+ * `accountRoutes()`.
142
+ *
143
+ * GET {url}/profile → AccountProfile
144
+ * POST {url}/profile {name} → AccountProfile
145
+ * POST {url}/profile/avatar (raw bytes, Content-Type = the image's) → AccountProfile
146
+ * POST {url}/profile/avatar/remove → AccountProfile
147
+ *
148
+ * A non-2xx THROWS an AccountError — a failed save must never look like a
149
+ * saved one.
150
+ */
151
+ declare function createFetchAccountAdapter(opts?: {
152
+ url?: string;
153
+ fetch?: typeof fetch;
154
+ credentials?: RequestCredentials;
155
+ }): AccountAdapter;
104
156
  declare const TEXT: {
105
157
  readonly da: {
106
158
  readonly openMenu: "Åbn menu";
@@ -129,6 +181,26 @@ declare const TEXT: {
129
181
  readonly backdrop: "Baggrund";
130
182
  readonly neurons: "Neuroner";
131
183
  readonly plain: "Ren";
184
+ readonly name: "Navn";
185
+ readonly email: "E-mail";
186
+ readonly save: "Gem";
187
+ readonly saving: "Gemmer…";
188
+ readonly saved: "Gemt";
189
+ readonly loadingProfile: "Henter profil…";
190
+ readonly couldNotLoadProfile: "Kunne ikke hente din profil.";
191
+ readonly couldNotSave: "Kunne ikke gemme. Prøv igen.";
192
+ readonly reauth: "Log ind igen for at rette";
193
+ readonly signInAgain: "Log ind igen";
194
+ readonly changePicture: "Skift billede";
195
+ readonly removePicture: "Fjern billede";
196
+ readonly confirmRemovePicture: "Fjern dit billede?";
197
+ readonly remove: "Fjern";
198
+ readonly cancel: "Annuller";
199
+ readonly pictureSaved: "Billedet er gemt";
200
+ readonly pictureRemoved: "Billedet er fjernet";
201
+ readonly pictureTooLarge: "Billedet er for stort — højst 2 MB.";
202
+ readonly pictureWrongType: "Vælg et PNG-, JPEG- eller WebP-billede.";
203
+ readonly bidSecurity: "Sikkerhed i Broberg ID";
132
204
  };
133
205
  readonly en: {
134
206
  readonly openMenu: "Open menu";
@@ -157,6 +229,26 @@ declare const TEXT: {
157
229
  readonly backdrop: "Backdrop";
158
230
  readonly neurons: "Neurons";
159
231
  readonly plain: "Plain";
232
+ readonly name: "Name";
233
+ readonly email: "Email";
234
+ readonly save: "Save";
235
+ readonly saving: "Saving…";
236
+ readonly saved: "Saved";
237
+ readonly loadingProfile: "Loading profile…";
238
+ readonly couldNotLoadProfile: "Could not load your profile.";
239
+ readonly couldNotSave: "Could not save. Try again.";
240
+ readonly reauth: "Sign in again to make changes";
241
+ readonly signInAgain: "Sign in again";
242
+ readonly changePicture: "Change picture";
243
+ readonly removePicture: "Remove picture";
244
+ readonly confirmRemovePicture: "Remove your picture?";
245
+ readonly remove: "Remove";
246
+ readonly cancel: "Cancel";
247
+ readonly pictureSaved: "Picture saved";
248
+ readonly pictureRemoved: "Picture removed";
249
+ readonly pictureTooLarge: "The picture is too large — 2 MB at most.";
250
+ readonly pictureWrongType: "Choose a PNG, JPEG or WebP image.";
251
+ readonly bidSecurity: "Security in Broberg ID";
160
252
  };
161
253
  };
162
254
  type Lang = keyof typeof TEXT;
@@ -340,5 +432,29 @@ interface AppShellProps {
340
432
  children?: ComponentChildren;
341
433
  }
342
434
  declare function AppShell(p: AppShellProps): JSX.Element;
435
+ interface AccountPageProps {
436
+ lang: Lang;
437
+ /** Default: `createFetchAccountAdapter()` → /api/account/profile* (@broberg/sso accountRoutes). */
438
+ adapter?: AccountAdapter;
439
+ /**
440
+ * Where «Log ind igen» goes — a FULL page load, so the app's login flow runs.
441
+ * Default `/auth/login?returnTo=<current path>` (no prompt=login — that forces
442
+ * re-proof and is a redirect-loop trap, F084.108). Broberg ID issues
443
+ * no refresh token, so about an hour after login every change answers 401
444
+ * reauth: that is a normal state here, not an error.
445
+ */
446
+ reauthHref?: string;
447
+ }
448
+ /** `/auth/login?returnTo=<this page>` — back here after signing in. */
449
+ declare function defaultReauthHref(): string;
450
+ /**
451
+ * The user's own name and picture, edited inside the app (F095.3).
452
+ *
453
+ * EVERYTHING SHOWN AFTER A SAVE IS THE SERVER'S ANSWER, never what was typed
454
+ * (CLAUDE.md «Et gem-felt SKAL bevises at gemme»): if Broberg ID trims or
455
+ * changes the name, the field shows that. On a failed save the field keeps the
456
+ * typed text, so nothing is lost and the error says why.
457
+ */
458
+ declare function AccountPage({ lang, adapter, reauthHref }: AccountPageProps): JSX.Element;
343
459
 
344
- export { 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, activeNavLabel, badgeLabel, createFetchNotificationSource, createMemoryNotificationSource, initials, isActivePath, isExternal, readFlag, readSet, relativeTime, safeNavigate, useIsMobile, writeFlag, writeSet };
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 };
package/dist/preact.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { ComponentChildren, JSX } from 'preact';
1
+ import { JSX, ComponentChildren } from 'preact';
2
2
  import { NotificationRow } from '@broberg/notifications/types';
3
3
  export { NotificationRow } from '@broberg/notifications/types';
4
4
 
@@ -101,6 +101,58 @@ declare function createMemoryNotificationSource(initial?: NotificationRow[]): No
101
101
  };
102
102
  /** "nu", "5 min", "3 t", "i går", "4 d" (da) or the English equivalents. */
103
103
  declare function relativeTime(ts: string | number, lang: "da" | "en", now?: number): string;
104
+ /** What every account call resolves to — the profile AS THE SERVER NOW HOLDS IT. */
105
+ interface AccountProfile {
106
+ sub: string;
107
+ name: string | null;
108
+ picture: string | null;
109
+ email: string | null;
110
+ /** Broberg ID's own account page (password, passkeys, sessions). */
111
+ account_url: string;
112
+ }
113
+ /** Where the account page gets and saves the profile. Swap it to point anywhere. */
114
+ interface AccountAdapter {
115
+ load(): Promise<AccountProfile>;
116
+ saveName(name: string): Promise<AccountProfile>;
117
+ uploadAvatar(file: Blob): Promise<AccountProfile>;
118
+ removeAvatar(): Promise<AccountProfile>;
119
+ }
120
+ /** A failed account call: the HTTP status and the server's `error` code, if any. */
121
+ declare class AccountError extends Error {
122
+ readonly status: number;
123
+ readonly code?: string | undefined;
124
+ constructor(status: number, code?: string | undefined);
125
+ }
126
+ declare const AVATAR_TYPES: readonly ["image/png", "image/jpeg", "image/webp"];
127
+ declare const AVATAR_MAX_BYTES: number;
128
+ /** Checked BEFORE uploading, so a file the server will refuse is never sent. */
129
+ declare function checkAvatarFile(file: {
130
+ size: number;
131
+ type: string;
132
+ }): "tooLarge" | "wrongType" | null;
133
+ /**
134
+ * What a failed call means for the page. `reauth` = the user's session or its
135
+ * scope does not allow the change (401, or 403 insufficient_scope): signing in
136
+ * again is the fix, so the page says that instead of «try again».
137
+ */
138
+ declare function accountErrorKind(err: unknown): "reauth" | "tooLarge" | "wrongType" | "other";
139
+ /**
140
+ * The default adapter: the app's own backend, shaped after @broberg/sso's
141
+ * `accountRoutes()`.
142
+ *
143
+ * GET {url}/profile → AccountProfile
144
+ * POST {url}/profile {name} → AccountProfile
145
+ * POST {url}/profile/avatar (raw bytes, Content-Type = the image's) → AccountProfile
146
+ * POST {url}/profile/avatar/remove → AccountProfile
147
+ *
148
+ * A non-2xx THROWS an AccountError — a failed save must never look like a
149
+ * saved one.
150
+ */
151
+ declare function createFetchAccountAdapter(opts?: {
152
+ url?: string;
153
+ fetch?: typeof fetch;
154
+ credentials?: RequestCredentials;
155
+ }): AccountAdapter;
104
156
  declare const TEXT: {
105
157
  readonly da: {
106
158
  readonly openMenu: "Åbn menu";
@@ -129,6 +181,26 @@ declare const TEXT: {
129
181
  readonly backdrop: "Baggrund";
130
182
  readonly neurons: "Neuroner";
131
183
  readonly plain: "Ren";
184
+ readonly name: "Navn";
185
+ readonly email: "E-mail";
186
+ readonly save: "Gem";
187
+ readonly saving: "Gemmer…";
188
+ readonly saved: "Gemt";
189
+ readonly loadingProfile: "Henter profil…";
190
+ readonly couldNotLoadProfile: "Kunne ikke hente din profil.";
191
+ readonly couldNotSave: "Kunne ikke gemme. Prøv igen.";
192
+ readonly reauth: "Log ind igen for at rette";
193
+ readonly signInAgain: "Log ind igen";
194
+ readonly changePicture: "Skift billede";
195
+ readonly removePicture: "Fjern billede";
196
+ readonly confirmRemovePicture: "Fjern dit billede?";
197
+ readonly remove: "Fjern";
198
+ readonly cancel: "Annuller";
199
+ readonly pictureSaved: "Billedet er gemt";
200
+ readonly pictureRemoved: "Billedet er fjernet";
201
+ readonly pictureTooLarge: "Billedet er for stort — højst 2 MB.";
202
+ readonly pictureWrongType: "Vælg et PNG-, JPEG- eller WebP-billede.";
203
+ readonly bidSecurity: "Sikkerhed i Broberg ID";
132
204
  };
133
205
  readonly en: {
134
206
  readonly openMenu: "Open menu";
@@ -157,6 +229,26 @@ declare const TEXT: {
157
229
  readonly backdrop: "Backdrop";
158
230
  readonly neurons: "Neurons";
159
231
  readonly plain: "Plain";
232
+ readonly name: "Name";
233
+ readonly email: "Email";
234
+ readonly save: "Save";
235
+ readonly saving: "Saving…";
236
+ readonly saved: "Saved";
237
+ readonly loadingProfile: "Loading profile…";
238
+ readonly couldNotLoadProfile: "Could not load your profile.";
239
+ readonly couldNotSave: "Could not save. Try again.";
240
+ readonly reauth: "Sign in again to make changes";
241
+ readonly signInAgain: "Sign in again";
242
+ readonly changePicture: "Change picture";
243
+ readonly removePicture: "Remove picture";
244
+ readonly confirmRemovePicture: "Remove your picture?";
245
+ readonly remove: "Remove";
246
+ readonly cancel: "Cancel";
247
+ readonly pictureSaved: "Picture saved";
248
+ readonly pictureRemoved: "Picture removed";
249
+ readonly pictureTooLarge: "The picture is too large — 2 MB at most.";
250
+ readonly pictureWrongType: "Choose a PNG, JPEG or WebP image.";
251
+ readonly bidSecurity: "Security in Broberg ID";
160
252
  };
161
253
  };
162
254
  type Lang = keyof typeof TEXT;
@@ -340,5 +432,29 @@ interface AppShellProps {
340
432
  children?: ComponentChildren;
341
433
  }
342
434
  declare function AppShell(p: AppShellProps): JSX.Element;
435
+ interface AccountPageProps {
436
+ lang: Lang;
437
+ /** Default: `createFetchAccountAdapter()` → /api/account/profile* (@broberg/sso accountRoutes). */
438
+ adapter?: AccountAdapter;
439
+ /**
440
+ * Where «Log ind igen» goes — a FULL page load, so the app's login flow runs.
441
+ * Default `/auth/login?returnTo=<current path>` (no prompt=login — that forces
442
+ * re-proof and is a redirect-loop trap, F084.108). Broberg ID issues
443
+ * no refresh token, so about an hour after login every change answers 401
444
+ * reauth: that is a normal state here, not an error.
445
+ */
446
+ reauthHref?: string;
447
+ }
448
+ /** `/auth/login?returnTo=<this page>` — back here after signing in. */
449
+ declare function defaultReauthHref(): string;
450
+ /**
451
+ * The user's own name and picture, edited inside the app (F095.3).
452
+ *
453
+ * EVERYTHING SHOWN AFTER A SAVE IS THE SERVER'S ANSWER, never what was typed
454
+ * (CLAUDE.md «Et gem-felt SKAL bevises at gemme»): if Broberg ID trims or
455
+ * changes the name, the field shows that. On a failed save the field keeps the
456
+ * typed text, so nothing is lost and the error says why.
457
+ */
458
+ declare function AccountPage({ lang, adapter, reauthHref }: AccountPageProps): JSX.Element;
343
459
 
344
- export { 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, activeNavLabel, badgeLabel, createFetchNotificationSource, createMemoryNotificationSource, initials, isActivePath, isExternal, readFlag, readSet, relativeTime, safeNavigate, useIsMobile, writeFlag, writeSet };
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 };
package/dist/preact.js CHANGED
@@ -179,6 +179,51 @@ function relativeTime(ts, lang, now = Date.now()) {
179
179
  if (d === 1) return lang === "da" ? "i g\xE5r" : "yesterday";
180
180
  return `${d} d`;
181
181
  }
182
+ var AccountError = class extends Error {
183
+ constructor(status, code) {
184
+ super(code ? `HTTP ${status} ${code}` : `HTTP ${status}`);
185
+ this.status = status;
186
+ this.code = code;
187
+ this.name = "AccountError";
188
+ }
189
+ status;
190
+ code;
191
+ };
192
+ var AVATAR_TYPES = ["image/png", "image/jpeg", "image/webp"];
193
+ var AVATAR_MAX_BYTES = 2 * 1024 * 1024;
194
+ function checkAvatarFile(file) {
195
+ if (!AVATAR_TYPES.includes(file.type)) return "wrongType";
196
+ if (file.size > AVATAR_MAX_BYTES) return "tooLarge";
197
+ return null;
198
+ }
199
+ function accountErrorKind(err) {
200
+ if (!(err instanceof AccountError)) return "other";
201
+ if (err.status === 401) return "reauth";
202
+ if (err.status === 403 && err.code === "insufficient_scope") return "reauth";
203
+ if (err.status === 413) return "tooLarge";
204
+ if (err.status === 415) return "wrongType";
205
+ return "other";
206
+ }
207
+ function createFetchAccountAdapter(opts = {}) {
208
+ const base = (opts.url ?? "/api/account").replace(/\/+$/, "");
209
+ const call = async (path, init) => {
210
+ const res = await (opts.fetch ?? fetch)(`${base}${path}`, {
211
+ credentials: opts.credentials ?? "same-origin",
212
+ ...init,
213
+ headers: { accept: "application/json", ...init?.headers }
214
+ });
215
+ const body = await res.json().catch(() => null);
216
+ if (!res.ok) throw new AccountError(res.status, typeof body?.error === "string" ? body.error : void 0);
217
+ if (!body || typeof body.sub !== "string") throw new AccountError(res.status, "bad_response");
218
+ return body;
219
+ };
220
+ return {
221
+ load: () => call("/profile"),
222
+ saveName: (name) => call("/profile", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ name }) }),
223
+ uploadAvatar: (file) => call("/profile/avatar", { method: "POST", headers: { "content-type": file.type }, body: file }),
224
+ removeAvatar: () => call("/profile/avatar/remove", { method: "POST" })
225
+ };
226
+ }
182
227
  var TEXT = {
183
228
  da: {
184
229
  openMenu: "\xC5bn menu",
@@ -206,7 +251,27 @@ var TEXT = {
206
251
  layered: "Lag",
207
252
  backdrop: "Baggrund",
208
253
  neurons: "Neuroner",
209
- plain: "Ren"
254
+ plain: "Ren",
255
+ name: "Navn",
256
+ email: "E-mail",
257
+ save: "Gem",
258
+ saving: "Gemmer\u2026",
259
+ saved: "Gemt",
260
+ loadingProfile: "Henter profil\u2026",
261
+ couldNotLoadProfile: "Kunne ikke hente din profil.",
262
+ couldNotSave: "Kunne ikke gemme. Pr\xF8v igen.",
263
+ reauth: "Log ind igen for at rette",
264
+ signInAgain: "Log ind igen",
265
+ changePicture: "Skift billede",
266
+ removePicture: "Fjern billede",
267
+ confirmRemovePicture: "Fjern dit billede?",
268
+ remove: "Fjern",
269
+ cancel: "Annuller",
270
+ pictureSaved: "Billedet er gemt",
271
+ pictureRemoved: "Billedet er fjernet",
272
+ pictureTooLarge: "Billedet er for stort \u2014 h\xF8jst 2 MB.",
273
+ pictureWrongType: "V\xE6lg et PNG-, JPEG- eller WebP-billede.",
274
+ bidSecurity: "Sikkerhed i Broberg ID"
210
275
  },
211
276
  en: {
212
277
  openMenu: "Open menu",
@@ -234,7 +299,27 @@ var TEXT = {
234
299
  layered: "Layered",
235
300
  backdrop: "Backdrop",
236
301
  neurons: "Neurons",
237
- plain: "Plain"
302
+ plain: "Plain",
303
+ name: "Name",
304
+ email: "Email",
305
+ save: "Save",
306
+ saving: "Saving\u2026",
307
+ saved: "Saved",
308
+ loadingProfile: "Loading profile\u2026",
309
+ couldNotLoadProfile: "Could not load your profile.",
310
+ couldNotSave: "Could not save. Try again.",
311
+ reauth: "Sign in again to make changes",
312
+ signInAgain: "Sign in again",
313
+ changePicture: "Change picture",
314
+ removePicture: "Remove picture",
315
+ confirmRemovePicture: "Remove your picture?",
316
+ remove: "Remove",
317
+ cancel: "Cancel",
318
+ pictureSaved: "Picture saved",
319
+ pictureRemoved: "Picture removed",
320
+ pictureTooLarge: "The picture is too large \u2014 2 MB at most.",
321
+ pictureWrongType: "Choose a PNG, JPEG or WebP image.",
322
+ bidSecurity: "Security in Broberg ID"
238
323
  }
239
324
  };
240
325
  var MOBILE_QUERY = "(max-width: 767px)";
@@ -913,7 +998,161 @@ function AppShell(p) {
913
998
  ] })
914
999
  ] });
915
1000
  }
1001
+ function defaultReauthHref() {
1002
+ const here = typeof location === "undefined" ? "/" : location.pathname + location.search;
1003
+ return `/auth/login?returnTo=${encodeURIComponent(here)}`;
1004
+ }
1005
+ function AccountPage({ lang, adapter, reauthHref }) {
1006
+ const t = TEXT[lang];
1007
+ const api = useMemo(() => adapter ?? createFetchAccountAdapter(), [adapter]);
1008
+ const [profile, setProfile] = useState(null);
1009
+ const [draft, setDraft] = useState("");
1010
+ const [busy, setBusy] = useState("load");
1011
+ const [notice, setNotice] = useState(null);
1012
+ const [reauth, setReauth] = useState(false);
1013
+ const [confirming, setConfirming] = useState(false);
1014
+ const fileRef = useRef(null);
1015
+ const apply = (p) => {
1016
+ setProfile(p);
1017
+ setDraft(p.name ?? "");
1018
+ };
1019
+ const fail = (err, fallback) => {
1020
+ const kind = accountErrorKind(err);
1021
+ if (kind === "reauth") {
1022
+ setReauth(true);
1023
+ setNotice(null);
1024
+ return;
1025
+ }
1026
+ const text = kind === "tooLarge" ? t.pictureTooLarge : kind === "wrongType" ? t.pictureWrongType : fallback;
1027
+ setNotice({ kind: "error", text });
1028
+ };
1029
+ useEffect(() => {
1030
+ let live = true;
1031
+ setBusy("load");
1032
+ api.load().then(
1033
+ (p) => live && (apply(p), setBusy(null)),
1034
+ (e) => live && (fail(e, t.couldNotLoadProfile), setBusy(null))
1035
+ );
1036
+ return () => {
1037
+ live = false;
1038
+ };
1039
+ }, [api]);
1040
+ const run = async (what, op, okText) => {
1041
+ setBusy(what);
1042
+ setNotice(null);
1043
+ try {
1044
+ apply(await op());
1045
+ setNotice({ kind: "ok", text: okText });
1046
+ } catch (e) {
1047
+ fail(e, t.couldNotSave);
1048
+ } finally {
1049
+ setBusy(null);
1050
+ }
1051
+ };
1052
+ const onSaveName = (e) => {
1053
+ e.preventDefault();
1054
+ if (busy || reauth) return;
1055
+ void run("name", () => api.saveName(draft), t.saved);
1056
+ };
1057
+ const onFile = (e) => {
1058
+ const input = e.currentTarget;
1059
+ const file = input.files?.[0];
1060
+ input.value = "";
1061
+ if (!file || busy || reauth) return;
1062
+ const bad = checkAvatarFile(file);
1063
+ if (bad) {
1064
+ setNotice({ kind: "error", text: bad === "tooLarge" ? t.pictureTooLarge : t.pictureWrongType });
1065
+ return;
1066
+ }
1067
+ void run("avatar", () => api.uploadAvatar(file), t.pictureSaved);
1068
+ };
1069
+ const onRemove = () => {
1070
+ setConfirming(false);
1071
+ void run("avatar", () => api.removeAvatar(), t.pictureRemoved);
1072
+ };
1073
+ const locked = reauth || busy !== null || profile === null;
1074
+ const user = { name: profile?.name ?? void 0, email: profile?.email ?? void 0, picture: profile?.picture ?? void 0 };
1075
+ return /* @__PURE__ */ jsxs("section", { class: "bas-account", "data-testid": "account-page", "aria-busy": busy !== null, children: [
1076
+ reauth ? /* @__PURE__ */ jsxs("div", { class: "bas-account__note is-reauth", role: "alert", "data-testid": "account-reauth", children: [
1077
+ /* @__PURE__ */ jsx("span", { "data-testid": "account-reauth-text", children: t.reauth }),
1078
+ /* @__PURE__ */ jsx(
1079
+ "button",
1080
+ {
1081
+ type: "button",
1082
+ class: "bas-account__btn is-primary",
1083
+ onClick: () => window.location.assign(reauthHref ?? defaultReauthHref()),
1084
+ "data-testid": "account-reauth-signin",
1085
+ children: t.signInAgain
1086
+ }
1087
+ )
1088
+ ] }) : null,
1089
+ notice ? /* @__PURE__ */ jsx(
1090
+ "p",
1091
+ {
1092
+ class: "bas-account__note " + (notice.kind === "ok" ? "is-ok" : "is-error"),
1093
+ role: notice.kind === "ok" ? "status" : "alert",
1094
+ "data-testid": notice.kind === "ok" ? "account-ok" : "account-error",
1095
+ children: notice.text
1096
+ }
1097
+ ) : null,
1098
+ busy === "load" ? /* @__PURE__ */ jsx("p", { class: "bas-account__muted", "data-testid": "account-loading", children: t.loadingProfile }) : null,
1099
+ profile ? /* @__PURE__ */ jsxs(Fragment, { children: [
1100
+ /* @__PURE__ */ jsxs("div", { class: "bas-account__who", children: [
1101
+ /* @__PURE__ */ jsx("span", { class: "bas-account__avatar", "data-testid": "account-avatar", "data-picture": profile.picture ?? "", children: /* @__PURE__ */ jsx(Avatar, { user, size: 72 }, profile.picture ?? "none") }),
1102
+ /* @__PURE__ */ jsxs("div", { class: "bas-account__whotext", children: [
1103
+ /* @__PURE__ */ jsx("div", { class: "bas-account__name", "data-testid": "account-name", children: profile.name ?? "" }),
1104
+ /* @__PURE__ */ jsx("div", { class: "bas-account__email", "data-testid": "account-email", children: profile.email ?? "" })
1105
+ ] })
1106
+ ] }),
1107
+ /* @__PURE__ */ jsxs("div", { class: "bas-account__row", children: [
1108
+ /* @__PURE__ */ jsx(
1109
+ "input",
1110
+ {
1111
+ ref: fileRef,
1112
+ type: "file",
1113
+ accept: AVATAR_TYPES.join(","),
1114
+ hidden: true,
1115
+ tabIndex: -1,
1116
+ onChange: onFile,
1117
+ "data-testid": "account-avatar-file"
1118
+ }
1119
+ ),
1120
+ /* @__PURE__ */ jsx("button", { type: "button", class: "bas-account__btn", disabled: locked, onClick: () => fileRef.current?.click(), "data-testid": "account-avatar-upload", children: busy === "avatar" ? t.saving : t.changePicture }),
1121
+ profile.picture && !confirming ? /* @__PURE__ */ jsx("button", { type: "button", class: "bas-account__btn is-quiet", disabled: locked, onClick: () => setConfirming(true), "data-testid": "account-avatar-remove", children: t.removePicture }) : null,
1122
+ profile.picture && confirming ? /* @__PURE__ */ jsxs("span", { class: "bas-account__confirm", role: "group", "data-testid": "account-avatar-remove-confirmation", children: [
1123
+ /* @__PURE__ */ jsx("span", { children: t.confirmRemovePicture }),
1124
+ /* @__PURE__ */ jsx("button", { type: "button", class: "bas-account__btn is-danger", disabled: locked, onClick: onRemove, "data-testid": "account-avatar-remove-confirm", children: t.remove }),
1125
+ /* @__PURE__ */ jsx("button", { type: "button", class: "bas-account__btn is-quiet", onClick: () => setConfirming(false), "data-testid": "account-avatar-remove-cancel", children: t.cancel })
1126
+ ] }) : null
1127
+ ] }),
1128
+ /* @__PURE__ */ jsxs("form", { class: "bas-account__form", onSubmit: onSaveName, children: [
1129
+ /* @__PURE__ */ jsx("label", { class: "bas-account__label", for: "bas-account-name", children: t.name }),
1130
+ /* @__PURE__ */ jsxs("div", { class: "bas-account__row", children: [
1131
+ /* @__PURE__ */ jsx(
1132
+ "input",
1133
+ {
1134
+ id: "bas-account-name",
1135
+ class: "bas-account__input",
1136
+ type: "text",
1137
+ autocomplete: "name",
1138
+ value: draft,
1139
+ readOnly: reauth,
1140
+ "aria-readonly": reauth,
1141
+ onInput: (e) => {
1142
+ setDraft(e.currentTarget.value);
1143
+ if (notice?.kind === "ok") setNotice(null);
1144
+ },
1145
+ "data-testid": "account-name-input"
1146
+ }
1147
+ ),
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 })
1149
+ ] })
1150
+ ] }),
1151
+ profile.account_url ? /* @__PURE__ */ jsx(NavLink, { href: profile.account_url, class: "bas-account__link", "data-testid": "account-bid-link", children: t.bidSecurity }) : null
1152
+ ] }) : null
1153
+ ] });
1154
+ }
916
1155
 
917
- export { AppShell, Avatar, NotificationBell, PageHeader, PageTabs, Sidebar, TEXT, TopBar, UserMenu, activeNavLabel, badgeLabel, createFetchNotificationSource, createMemoryNotificationSource, initials, isActivePath, isExternal, readFlag, readSet, relativeTime, safeNavigate, useIsMobile, writeFlag, writeSet };
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 };
918
1157
  //# sourceMappingURL=preact.js.map
919
1158
  //# sourceMappingURL=preact.js.map