233 lines
6.9 KiB
JavaScript
233 lines
6.9 KiB
JavaScript
// Ü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;
|
|
}
|