// 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'; 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). */ ready(): void; dispose(): void; } export function mountAccountMenu(slot: HTMLElement): AccountMenu { let isReady = false; // Hebt die Dokument-Listener des gerade offenen Menüs wieder auf (null: kein Menü gezeichnet). let detach: (() => void) | null = null; function render(): void { detach?.(); detach = null; if (!isReady) { slot.replaceChildren(); return; } const user = currentUser(); if (!user) { const signIn = el('button', { type: 'button', class: 'auth-btn auth-btn-small' }, t('Anmelden')); signIn.addEventListener('click', () => void requireLogin('login')); slot.replaceChildren(el('div', { class: 'account-menu' }, signIn)); return; } const initial = Array.from(user.email)[0]?.toUpperCase() ?? '?'; const avatar = el( 'button', { type: 'button', class: 'account-avatar', 'aria-label': t('Konto-Menü'), 'aria-expanded': 'false', 'aria-controls': 'account-dropdown', }, 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 }, head, myCards, changeName, signOut, error); const wrap = el('div', { class: 'account-menu', role: 'group', 'aria-label': t('Konto') }, avatar, dropdown); let isOpen = false; function setOpen(open: boolean): void { isOpen = open; dropdown.hidden = !open; avatar.setAttribute('aria-expanded', String(open)); } avatar.addEventListener('click', () => setOpen(!isOpen)); const onDocumentClick = (event: MouseEvent) => { if (!isOpen) return; const target = event.target as Element | null; if (target && wrap.contains(target) && !target.closest('a')) return; // nur Links schließen das Menü selbst setOpen(false); }; const onKeydown = (event: KeyboardEvent) => { if (event.key !== 'Escape' || !isOpen) return; setOpen(false); avatar.focus(); }; document.addEventListener('click', onDocumentClick); document.addEventListener('keydown', onKeydown); detach = () => { document.removeEventListener('click', onDocumentClick); 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('.account-avatar')?.focus()); }); signOut.addEventListener('click', () => { signOut.disabled = true; error.textContent = ''; // Bei Erfolg zeichnet onAuthChange das Menü neu; nur der Fehlerfall braucht Handarbeit. logout().catch((err: unknown) => { error.textContent = errorMessage(err, 'load'); signOut.disabled = false; }); }); slot.replaceChildren(wrap); } 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 { ready() { isReady = true; render(); }, dispose() { off(); offName(); detach?.(); detach = null; slot.replaceChildren(); }, }; }