// Ü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; // Ist das Archiv aufgeklappt? let archiveOpen = false; const ROLE_LABEL = { owner: "Eigentümer", editor: "Bearbeiter", viewer: "Nur lesen" }; function formatDate(iso) { if (!iso) return ""; return new Date(iso).toLocaleDateString("de-DE", { day: "2-digit", month: "2-digit", year: "numeric", }); } export async function listsView(root, { openList, onLogout, settings }) { let banner = null; let archived = []; async function reload() { // Zwei Abfragen statt einer mit Filterung im Browser: Das Archiv // wird selten geöffnet und kann mit der Zeit groß werden. const [active, old] = await Promise.all([ get("/api/lists?archived=false"), get("/api/lists?archived=true"), ]); archived = old; set({ lists: active }); } 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", {}, // Archivieren wirkt nur für die eigene Übersicht - deshalb // steht es allen Mitgliedern offen, nicht nur dem Eigentümer. el("p.sub", {}, entry.archived ? `Seit ${formatDate(entry.archived_at)} in deinem Archiv.` : "Archivieren nimmt die Liste aus deiner Übersicht – " + "für die anderen Mitglieder bleibt sie sichtbar."), entry.archived && entry.delete_due_at ? el("p.lead.warn", {}, `Wird am ${formatDate(entry.delete_due_at)} automatisch ` + "gelöscht, wenn du sie nicht zurückholst.") : null, el("div.menu-actions", {}, el("button", { type: "button", onclick: () => guarded(() => post(`/api/lists/${entry.id}/${entry.archived ? "un" : ""}archive`)), }, entry.archived ? "Aus dem Archiv holen" : "Archivieren"), isOwner ? el("button", { type: "button", onclick: () => { openMenuId = null; renamingId = entry.id; render(); }, }, "Umbenennen") : null), isOwner ? el("button.danger.wide", { type: "button", onclick: () => remove(entry), }, "Löschen") : el("button.danger.wide", { 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", {}, archived.length ? "Alle Listen liegen im Archiv." : "Noch keine Liste vorhanden."), el("label", { htmlFor: "new-list" }, "Neue Liste"), createRow()), archived.length ? el("section.card.archive", {}, el("button.archive-head", { type: "button", "aria-expanded": archiveOpen ? "true" : "false", onclick: () => { archiveOpen = !archiveOpen; render(); }, }, el("span", {}, `Archiv (${archived.length})`), el("span.chev", {}, archiveOpen ? "▾" : "▸")), archiveOpen ? el("div", {}, el("p.lead", {}, "Listen, die du weggeräumt hast oder die du länger " + "nicht geöffnet hast. Sie sind nicht gelöscht – ein " + "Klick holt sie zurück."), el("ul.lists", {}, archived.map(row))) : null) : null ); } await reload(); render(); return render; }