feat(account): Benutzername beim Registrieren, im Konto-Menü und als einmalige Abfrage
- Neu `account/profile.ts`: Muster, Sperrliste und Schlüssel (ein Test vergleicht sie mit `deploy/api/common.js`), Profile lesen (`getUsername`, `usernamesFor` gesammelt in einer Abfrage und zwischengespeichert, bei einem Fehler nur die bekannten Namen), `isUsernameTaken`, `saveUsername` (POST /api/profil, Zwischenspeicher + `onUsernameChange`), `myUsername`/`loadMyUsername` und der Merker `aircard.usernamePrompted`. `IDS.profiles`. - Neu `account/username-dialog.ts`: Dialog „Benutzername wählen“ in den Modi prompt („Später“), required („Abbrechen“) und change (vorbelegt), Fehler am Feld, Busy-Sperre, höchstens ein Dialog zur Zeit; dazu `requireUsername()`. - Registrieren: Pflichtfeld „Benutzername“ vor der E-Mail. Muster, Sperrliste und Verfügbarkeit werden vor dem Anlegen des Kontos geprüft (kein halbes Konto); danach wird der Name gespeichert, scheitert das, schließt der Login-Dialog und der Namensdialog fragt. - Konto-Menü: „@name“ oben, die E-Mail klein darunter, Eintrag „Benutzername ändern“ bzw. „Benutzername wählen“. - main.ts: Name nach Start und Anmeldung lesen; ohne Namen einmal pro Sitzung der Dialog „prompt“ (nicht, wenn der Name nicht lesbar ist). - Texte in `en.ts`, Stile in `account.css`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
19c8151c7e
commit
3e4bc21f00
14 files changed
+2660
-40
No files matched your search
@@ -1,5 +1,5 @@
|
||||
// Konto-Menü im Seitenkopf: abgemeldet „Anmelden“, angemeldet ein Avatar-Kreis mit Initiale
|
||||
// und einem Menü (E-Mail, „Meine Karten“, „Abmelden“). Die Texte stehen in der Sprache der Seite; nach einem Sprachwechsel
|
||||
// Konto-Menü im Seitenkopf: abgemeldet „Anmelden“, angemeldet ein Avatar-Kreis mit Initiale und einem Menü (Benutzername und E-Mail,
|
||||
// „Meine Karten“, „Benutzername ändern“, „Abmelden“). Die Texte stehen in der Sprache der Seite; nach einem Sprachwechsel
|
||||
// baut main.ts das Menü neu auf.
|
||||
import './account.css';
|
||||
import { el } from '../editor/dom';
|
||||
@@ -7,6 +7,8 @@ import { t } from '../i18n/i18n';
|
||||
import { currentUser, logout, onAuthChange } from './auth';
|
||||
import { errorMessage } from './errors';
|
||||
import { requireLogin } from './login-dialog';
|
||||
import { myUsername, onUsernameChange } from './profile';
|
||||
import { usernameDialog } from './username-dialog';
|
||||
|
||||
export interface AccountMenu {
|
||||
/** Zeigt das Menü erst, wenn der Anmeldestatus bekannt ist (kein Aufblitzen von „Anmelden“ bei angemeldeten Nutzern). */
|
||||
@@ -46,11 +48,20 @@ export function mountAccountMenu(slot: HTMLElement): AccountMenu {
|
||||
},
|
||||
initial,
|
||||
);
|
||||
// Mit Benutzernamen steht „@name“ oben und die E-Mail klein darunter; ohne Namen (noch nicht gelesen oder nicht gewählt) nur die E-Mail.
|
||||
const name = myUsername();
|
||||
const email = el('span', { class: 'account-email', title: user.email }, user.email);
|
||||
const head = el(
|
||||
'div',
|
||||
{ class: 'account-head' },
|
||||
...(name ? [el('span', { class: 'account-name', title: `@${name}` }, `@${name}`)] : []),
|
||||
email,
|
||||
);
|
||||
const myCards = el('a', { class: 'account-item', href: '/karten', 'data-link': true }, t('Meine Karten'));
|
||||
const changeName = el('button', { type: 'button', class: 'account-item' }, name ? t('Benutzername ändern') : t('Benutzername wählen'));
|
||||
const signOut = el('button', { type: 'button', class: 'account-item' }, t('Abmelden'));
|
||||
const error = el('span', { class: 'account-error', role: 'alert' });
|
||||
const dropdown = el('div', { class: 'account-dropdown', id: 'account-dropdown', hidden: true }, email, myCards, signOut, error);
|
||||
const dropdown = el('div', { class: 'account-dropdown', id: 'account-dropdown', hidden: true }, head, myCards, changeName, signOut, error);
|
||||
const wrap = el('div', { class: 'account-menu', role: 'group', 'aria-label': t('Konto') }, avatar, dropdown);
|
||||
|
||||
let isOpen = false;
|
||||
@@ -79,6 +90,14 @@ export function mountAccountMenu(slot: HTMLElement): AccountMenu {
|
||||
document.removeEventListener('keydown', onKeydown);
|
||||
};
|
||||
|
||||
changeName.addEventListener('click', () => {
|
||||
setOpen(false);
|
||||
// Der Eintrag im geschlossenen (und nach dem Speichern neu gezeichneten) Menü kann den Fokus nicht halten: er geht an den Avatar.
|
||||
usernameDialog({ mode: 'change' })
|
||||
.catch((err: unknown) => console.error('Der Dialog „Benutzername“ konnte nicht geöffnet werden', err))
|
||||
.finally(() => slot.querySelector<HTMLElement>('.account-avatar')?.focus());
|
||||
});
|
||||
|
||||
signOut.addEventListener('click', () => {
|
||||
signOut.disabled = true;
|
||||
error.textContent = '';
|
||||
@@ -92,6 +111,8 @@ export function mountAccountMenu(slot: HTMLElement): AccountMenu {
|
||||
}
|
||||
|
||||
const off = onAuthChange(render);
|
||||
// Der Name steht fest oder ändert sich (gelesen nach dem Start, gespeichert im Dialog): das Menü zeigt gleich den neuen.
|
||||
const offName = onUsernameChange(render);
|
||||
render();
|
||||
|
||||
return {
|
||||
@@ -101,6 +122,7 @@ export function mountAccountMenu(slot: HTMLElement): AccountMenu {
|
||||
},
|
||||
dispose() {
|
||||
off();
|
||||
offName();
|
||||
detach?.();
|
||||
detach = null;
|
||||
slot.replaceChildren();
|
||||
|
||||
Reference in new issue
Block a user