// Übersicht aller Listen des angemeldeten Nutzers. "use strict"; import { del, get, patch, post } from "../api.js"; import * as db from "../db.js"; import { clear, el, mount } from "../dom.js"; import { set, state } from "../store.js"; // Welche Zeile hat gerade ihr Menü offen let openMenuId = null; // Welche Zeile wird gerade umbenannt let renamingId = null; const ROLE_LABEL = { owner: "Eigentümer", editor: "Bearbeiter", viewer: "Nur lesen" }; export async function listsView(root, { openList, onLogout, settings }) { let banner = null; async function reload() { set({ lists: await get("/api/lists") }); } function say(message, kind = "error") { banner = { message, kind }; render(); setTimeout(() => { banner = null; render(); }, 6000); } async function guarded(fn) { try { await fn(); await reload(); render(); } catch (err) { say(err.message); } } // ------------------------------------------------------------------- // Eine Zeile // ------------------------------------------------------------------- function row(entry) { const isOwner = entry.role === "owner"; if (renamingId === entry.id) { const field = el("input", { type: "text", value: entry.name, maxLength: 120, onkeydown: (ev) => { if (ev.key === "Enter") commit(); if (ev.key === "Escape") { renamingId = null; render(); } }, }); function commit() { const name = field.value.trim(); if (!name) { say("Der Name darf nicht leer sein."); return; } renamingId = null; if (name === entry.name) { render(); return; } guarded(() => patch(`/api/lists/${entry.id}`, { name })); } const node = el("li.list-row.renaming", {}, el("div.row", {}, field, el("button.primary", { type: "button", onclick: commit }, "Speichern"), el("button", { type: "button", className: "cancel", onclick: () => { renamingId = null; render(); }, }, "Abbrechen")) ); queueMicrotask(() => { field.focus(); field.select(); }); return node; } const people = entry.member_count === 1 ? "nur ich" : `${entry.member_count} Personen`; return el("li.list-row", {}, el("div.list-main", {}, el("button.list-open", { type: "button", onclick: () => openList(entry.id) }, el("span.name", {}, entry.name), el("span.meta", {}, `${people} · ${ROLE_LABEL[entry.role] || entry.role}`)), el("button.menu-toggle", { type: "button", title: "Weitere Aktionen", "aria-label": `Aktionen für ${entry.name}`, onclick: () => { openMenuId = openMenuId === entry.id ? null : entry.id; render(); }, }, "⋯")), openMenuId === entry.id ? el("div.list-menu", {}, isOwner ? el("div.menu-actions", {}, el("button", { type: "button", onclick: () => { openMenuId = null; renamingId = entry.id; render(); }, }, "Umbenennen"), el("button.danger", { type: "button", onclick: () => remove(entry), }, "Löschen")) : el("div.menu-actions", {}, el("button.danger", { type: "button", onclick: () => leave(entry), }, "Liste verlassen"))) : null ); } function remove(entry) { const extra = entry.member_count > 1 ? `\n\n${entry.member_count - 1} weitere Person(en) verlieren damit den Zugriff.` : ""; if (!confirm(`Liste „${entry.name}“ löschen?${extra}`)) return; openMenuId = null; guarded(async () => { await del(`/api/lists/${entry.id}`); // Zwischenspeicher der gelöschten Liste mit aufräumen, sonst // bleibt ein verwaister Stand in IndexedDB liegen. await forgetLocally(entry.id); }); } function leave(entry) { if (!confirm( `Liste „${entry.name}“ verlassen?\n\n` + "Du siehst sie danach nicht mehr. Der Eigentümer kann dich erneut einladen." )) return; openMenuId = null; guarded(async () => { await post(`/api/lists/${entry.id}/leave`); await forgetLocally(entry.id); }); } async function forgetLocally(listId) { await Promise.allSettled([ db.cacheDelete(`view:${listId}`), db.cacheDelete(`markets:${listId}`), db.cacheDelete(`categories:${listId}`), db.cacheDelete(`meta:${listId}`), db.clearList(listId), ]); } // ------------------------------------------------------------------- // Neue Liste // ------------------------------------------------------------------- function createRow() { const field = el("input", { id: "new-list", type: "text", maxLength: 120, placeholder: "z. B. Wocheneinkauf", onkeydown: (ev) => { if (ev.key === "Enter") create(); }, }); const button = el("button.primary", { type: "button", onclick: () => create() }, "Anlegen"); async function create() { const name = field.value.trim(); if (!name) { say("Bitte einen Namen eingeben."); return; } button.disabled = true; try { await post("/api/lists", { name }); field.value = ""; await reload(); // Ohne diesen Aufruf blieb die neue Liste unsichtbar, bis die // Seite neu geladen wurde: Der Zustandsspeicher war aktuell, // die Ansicht hatte sich aber nie dafür angemeldet. render(); document.getElementById("new-list")?.focus(); } catch (err) { say(err.message); } finally { button.disabled = false; } } return el("div.row", {}, field, button); } // ------------------------------------------------------------------- // Gesamtansicht // ------------------------------------------------------------------- function render() { mount(root, el("header.bar", {}, el("span.who", {}, state.user.display_name || state.user.email), el("span.bar-actions", {}, el("button.linklike", { type: "button", onclick: settings }, "Einstellungen"), el("button.linklike", { type: "button", onclick: onLogout }, "Abmelden"))), banner ? el(banner.kind === "error" ? "p.error" : "p.notice", {}, banner.message) : null, el("section.card", {}, el("h1", {}, "Meine Listen"), state.lists.length ? el("ul.lists", {}, state.lists.map(row)) : el("p.empty", {}, "Noch keine Liste vorhanden."), el("label", { htmlFor: "new-list" }, "Neue Liste"), createRow()) ); } await reload(); render(); return render; }