fix(cards): Editor an Karte und Konto koppeln (Neue Karte, Abmelden, Kontowechsel)
- Editor zeigt „Bearbeitet: <Name>“, solange eine gespeicherte Karte gebunden ist, und bietet „Neue Karte“ (setzt zurück, bricht Laden ab, entfernt ?karte=; fragt bei ungespeicherten Änderungen per eigenem Dialog „Verwerfen?“) - EditorState.savedConfig als Vergleichsbasis für ungespeicherte Änderungen - bindEditorToAuth: Abmelden leert den Editor, Kontowechsel A -> B entkoppelt die Karte (Entwurf bleibt), Anmelden ohne Vorgänger und abgelaufene Sitzung behalten den Entwurf; auth.onLogout unterscheidet eigenes Abmelden von bemerktem Ablauf - Speichern nimmt den Schnappschuss pro Klick (auch für die Wiederholung nach 401) - save: row_not_found beim Update legt eine neue Karte an (inkl. Aufräumen) - Karten-Seite: Editor-Abgleich auch nach dem Verlassen der Seite, Löschen-Dialog wird beim Verlassen geschlossen, keine doppelten Vorschau-URLs - confirm-dialog als gemeinsames Modul (Löschen, Verwerfen) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
ba4d9f76d4
commit
38b361ad7a
20 files changed
+1038
-116
No files matched your search
+19
-53
@@ -9,6 +9,7 @@ import { LoginCancelled, withLogin } from '../account/with-login';
|
||||
import { clampCrop } from '../card/crop';
|
||||
import { CARD_W } from '../card/geometry';
|
||||
import { exportCard, loadCardAssets } from '../card/render';
|
||||
import { confirmDialog } from '../editor/confirm-dialog';
|
||||
import { el } from '../editor/dom';
|
||||
import { downloadFileName, pngSizeWarning, saveBlobAs } from '../editor/download';
|
||||
import type { EditorStore } from '../editor/state';
|
||||
@@ -29,56 +30,10 @@ export function formatDate(iso: string): string {
|
||||
return `${pad(date.getDate())}.${pad(date.getMonth() + 1)}.${date.getFullYear()}`;
|
||||
}
|
||||
|
||||
/** Fragt, ob die Karte wirklich gelöscht werden soll (eigener Dialog statt window.confirm). */
|
||||
function confirmDelete(name: string): Promise<boolean> {
|
||||
return new Promise<boolean>((resolve, reject) => {
|
||||
const cancel = el('button', { type: 'button', class: 'auth-btn' }, 'Abbrechen');
|
||||
const remove = el('button', { type: 'button', class: 'auth-btn cards-btn-danger' }, 'Löschen');
|
||||
const dialog = el(
|
||||
'dialog',
|
||||
{ class: 'confirm-dialog', 'aria-labelledby': 'confirm-title' },
|
||||
el(
|
||||
'div',
|
||||
{ class: 'confirm-card' },
|
||||
el('h2', { id: 'confirm-title', class: 'confirm-title' }, `Karte „${name}“ löschen?`),
|
||||
el('p', { class: 'confirm-text' }, 'Die Karte und ihr Bild werden endgültig gelöscht.'),
|
||||
el('div', { class: 'confirm-actions' }, cancel, remove),
|
||||
),
|
||||
);
|
||||
|
||||
let answered = false;
|
||||
const finish = (ok: boolean) => {
|
||||
if (answered) return;
|
||||
answered = true;
|
||||
if (dialog.open) dialog.close();
|
||||
dialog.remove();
|
||||
resolve(ok);
|
||||
};
|
||||
cancel.addEventListener('click', () => finish(false));
|
||||
remove.addEventListener('click', () => finish(true));
|
||||
// Esc schließt nativ und löst `close` aus.
|
||||
dialog.addEventListener('close', () => finish(false));
|
||||
// Hintergrund = Klick auf das <dialog> selbst (die Karte füllt es ganz aus).
|
||||
dialog.addEventListener('click', (event) => {
|
||||
if (event.target === dialog) finish(false);
|
||||
});
|
||||
|
||||
document.body.append(dialog);
|
||||
try {
|
||||
dialog.showModal();
|
||||
} catch (err) {
|
||||
answered = true;
|
||||
dialog.remove();
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
cancel.focus(); // das Ungefährliche zuerst
|
||||
});
|
||||
}
|
||||
|
||||
export function mountCardsPage(root: HTMLElement, store: EditorStore): () => void {
|
||||
const service = cardsService();
|
||||
let disposed = false;
|
||||
const disposal = new AbortController(); // schließt einen offenen Löschen-Dialog beim Verlassen der Seite
|
||||
let state: 'loading' | 'loggedOut' | 'error' | 'list' = 'loading';
|
||||
let loadToken = 0;
|
||||
let cards: SavedCard[] = [];
|
||||
@@ -123,7 +78,8 @@ export function mountCardsPage(root: HTMLElement, store: EditorStore): () => voi
|
||||
const blob = await withLogin(() => service.loadPreview(card));
|
||||
// Die Karte kann inzwischen gelöscht oder die Liste neu geladen worden sein.
|
||||
if (disposed || !cards.some((c) => c.previewFileId === card.previewFileId)) return;
|
||||
previewUrls.set(card.previewFileId, URL.createObjectURL(blob));
|
||||
// Schon vorhanden (z. B. durch einen früheren Lauf): nicht noch eine URL anlegen, die niemand freigibt.
|
||||
if (!previewUrls.has(card.previewFileId)) previewUrls.set(card.previewFileId, URL.createObjectURL(blob));
|
||||
} catch {
|
||||
if (disposed) return;
|
||||
previewFailed.add(card.previewFileId);
|
||||
@@ -193,7 +149,7 @@ export function mountCardsPage(root: HTMLElement, store: EditorStore): () => voi
|
||||
});
|
||||
}
|
||||
// Die Karte im Editor gibt es nicht mehr: ein späteres Speichern legt eine neue an, statt ins Leere zu schreiben.
|
||||
if (change.removedId && s.saved.id === change.removedId) store.set({ saved: null });
|
||||
if (change.removedId && s.saved.id === change.removedId) store.set({ saved: null, savedConfig: null });
|
||||
}
|
||||
|
||||
function renderList(focusId?: string, focusAction?: string): void {
|
||||
@@ -230,7 +186,7 @@ export function mountCardsPage(root: HTMLElement, store: EditorStore): () => voi
|
||||
const openButton = action('open', 'Öffnen', 'auth-btn auth-btn-small auth-btn-primary');
|
||||
const downloadButton = action('download', 'Herunterladen');
|
||||
const renameButton = action('rename', 'Umbenennen');
|
||||
const deleteButton = action('delete', 'Löschen', 'auth-btn auth-btn-small cards-btn-danger');
|
||||
const deleteButton = action('delete', 'Löschen', 'auth-btn auth-btn-small auth-btn-danger');
|
||||
const buttons = [openButton, downloadButton, renameButton, deleteButton];
|
||||
|
||||
const tile = el(
|
||||
@@ -284,19 +240,28 @@ export function mountCardsPage(root: HTMLElement, store: EditorStore): () => voi
|
||||
|
||||
deleteButton.addEventListener('click', () => {
|
||||
void run(async () => {
|
||||
if (!(await confirmDelete(card.name))) return;
|
||||
const confirmed = await confirmDialog(
|
||||
{
|
||||
title: `Karte „${card.name}“ löschen?`,
|
||||
text: 'Die Karte und ihr Bild werden endgültig gelöscht.',
|
||||
confirmLabel: 'Löschen',
|
||||
},
|
||||
disposal.signal,
|
||||
);
|
||||
if (!confirmed) return;
|
||||
try {
|
||||
await withLogin(() => service.remove(card));
|
||||
} catch (err) {
|
||||
// Schon gelöscht (anderer Tab): das Ziel ist erreicht.
|
||||
if (errorType(err) !== 'row_not_found') throw err;
|
||||
}
|
||||
// Der Editor-Zustand überlebt die Seite: auch wenn sie inzwischen verlassen wurde, gibt es die Karte nicht mehr.
|
||||
syncEditor({ removedId: card.id });
|
||||
if (disposed) return;
|
||||
const url = previewUrls.get(card.previewFileId);
|
||||
if (url) URL.revokeObjectURL(url);
|
||||
previewUrls.delete(card.previewFileId);
|
||||
cards = cards.filter((c) => c.id !== card.id);
|
||||
syncEditor({ removedId: card.id });
|
||||
renderList();
|
||||
if (cards.length === 0) heading.focus();
|
||||
else body.querySelector<HTMLElement>('.card-tile [data-action="open"]')?.focus();
|
||||
@@ -345,10 +310,10 @@ export function mountCardsPage(root: HTMLElement, store: EditorStore): () => voi
|
||||
cancel.disabled = false;
|
||||
throw err;
|
||||
}
|
||||
syncEditor({ renamed: updated }); // auch nach dem Verlassen der Seite: sonst überschreibt Speichern den Namen
|
||||
if (disposed) return;
|
||||
// Der Server sortiert nach „zuletzt geändert“; die umbenannte Karte steht danach vorn.
|
||||
cards = [updated, ...cards.filter((c) => c.id !== updated.id)];
|
||||
syncEditor({ renamed: updated });
|
||||
renderList(updated.id, 'rename');
|
||||
});
|
||||
});
|
||||
@@ -410,6 +375,7 @@ export function mountCardsPage(root: HTMLElement, store: EditorStore): () => voi
|
||||
|
||||
return () => {
|
||||
disposed = true;
|
||||
disposal.abort();
|
||||
offAuth();
|
||||
queue.length = 0;
|
||||
revokePreviews();
|
||||
|
||||
Reference in new issue
Block a user