feat(cards): Karten speichern und Meine Karten

- account/cards.ts: Kartendienst (neu/bestehend speichern mit Aufräumen halber
  Uploads, Umbenennen, Löschen, Laden) über ein schmales CardsApi-Interface,
  Appwrite-Anschluss mit 22.4.1-Objektparametern, fetchFile über
  client.prepareRequest (Projekt-Header + Cookie, kein getFileView)
- account/with-login.ts: bei 401 Nutzer neu prüfen, einmal anmelden, wiederholen
- editor/save.ts: Speichern-Ablauf (ein Lauf zur Zeit, 401 -> Login -> einmal
  wiederholen, deutsche Fehlermeldung)
- Editor: Speichern mit Schnappschuss und 480-px-Vorschau, Karte öffnen über
  ?karte=<id>, Bildwechsel während des Speicherns gesperrt
- pages/cards-page.ts: /karten mit Kacheln, Öffnen, Herunterladen, Umbenennen,
  Löschen (eigener Dialog), Abmelde- und Leer-Ansicht
- Tests für Dienst, withLogin, Speichern-Ablauf, Karten-Seite und Editor-Anbindung

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Opus 5.5 committed 2026-10-04 09:18:27 +00:00
1 parent a147d3b496
commit ba4d9f76d4
14 files changed
+2849 -16

No files matched your search

+17 -2
View File
@@ -17,6 +17,8 @@ export interface PreviewView {
hasImage: boolean;
zoom: number;
saving: boolean;
/** true für kurze Zeit nach erfolgreichem Speichern (Knopf zeigt „Gespeichert“). */
saved: boolean;
exporting: boolean;
}
@@ -27,9 +29,14 @@ export interface Preview {
setDragging(active: boolean): void;
setError(message: string | null): void;
setWarning(message: string | null): void;
/** Neutraler Hinweis, z. B. „Karte wird geladen …“. */
setNotice(message: string | null): void;
}
const NO_IMAGE_HINT = 'Erst ein Bild wählen';
const SAVE_LABEL = 'Speichern';
const SAVING_LABEL = 'Speichert …';
const SAVED_LABEL = 'Gespeichert';
const KEY_STEP = 24;
const KEY_STEP_BIG = 96;
const KEY_ZOOM_STEP = 0.1;
@@ -185,13 +192,14 @@ export function createPreview(handlers: PreviewHandlers): Preview {
// Meldungen
const errorBox = el('p', { class: 'msg msg-error', role: 'alert' });
const warningBox = el('p', { class: 'msg msg-warning', role: 'status' });
const noticeBox = el('p', { class: 'msg msg-notice', role: 'status' });
// Knopfleiste
const replaceButton = el('button', { type: 'button', class: 'btn btn-ghost' }, 'Bild ersetzen');
replaceButton.addEventListener('click', openPicker);
const downloadButton = el('button', { type: 'button', class: 'btn btn-primary' }, 'Herunterladen');
downloadButton.addEventListener('click', () => handlers.onDownload());
const saveButton = el('button', { type: 'button', class: 'btn btn-primary' }, 'Speichern');
const saveButton = el('button', { type: 'button', class: 'btn btn-primary btn-save' }, SAVE_LABEL);
saveButton.addEventListener('click', () => handlers.onSave());
const actions = el(
'div',
@@ -202,7 +210,7 @@ export function createPreview(handlers: PreviewHandlers): Preview {
const note = el('p', { class: 'note' }, icon('lock', 'note-icon'), 'Dein Bild bleibt auf deinem Gerät, bis du speicherst.');
const inner = el('div', { class: 'preview-inner' }, stage, zoomRow, errorBox, warningBox, actions, note, input);
const inner = el('div', { class: 'preview-inner' }, stage, zoomRow, errorBox, warningBox, noticeBox, actions, note, input);
const section = el('section', { class: 'editor-center', 'aria-label': 'Kartenvorschau' }, inner);
function setDisabled(button: HTMLButtonElement, disabled: boolean, hint: string | null): void {
@@ -227,6 +235,10 @@ export function createPreview(handlers: PreviewHandlers): Preview {
zoomValue.textContent = `${Math.round(view.zoom * 100)} %`;
setDisabled(downloadButton, !view.hasImage || view.exporting, view.hasImage ? null : NO_IMAGE_HINT);
setDisabled(saveButton, !view.hasImage || view.saving, view.hasImage ? null : NO_IMAGE_HINT);
saveButton.textContent = view.saving ? SAVING_LABEL : view.saved ? SAVED_LABEL : SAVE_LABEL;
// Ein Bildwechsel mitten im Speichern würde die gespeicherte Karte durcheinanderbringen.
replaceButton.disabled = view.saving;
input.disabled = view.saving;
downloadButton.setAttribute('aria-busy', String(view.exporting));
saveButton.setAttribute('aria-busy', String(view.saving));
},
@@ -239,5 +251,8 @@ export function createPreview(handlers: PreviewHandlers): Preview {
setWarning(message) {
setMessage(warningBox, message);
},
setNotice(message) {
setMessage(noticeBox, message);
},
};
}