// Admin dashboard: all lists, their order, create/edit/delete. import { h, icon, api, readBoot, toast, confirmDialog, fmt } from "./common.js"; import { sortable } from "./sortable.js"; import { openListForm } from "./list-form.js"; const boot = readBoot(); const { categories, limits } = boot; const covers = boot.covers || {}; const catByKey = Object.fromEntries(categories.map((c) => [c.key, c])); let lists = boot.lists; const root = document.getElementById("lists"); function newList(category) { openListForm({ categories, limits, category, onSaved: (l) => { location.href = `/admin/lists/${l.id}`; }, }); } function editList(list) { openListForm({ categories, limits, list, onSaved: (saved) => { lists = lists.map((l) => (l.id === saved.id ? saved : l)); render(); toast("List saved"); }, }); } async function deleteList(list) { const entries = list.itemCount ? ` Its ${fmt.format(list.itemCount)} ${list.itemCount === 1 ? "entry" : "entries"} will be removed.` : ""; const ok = await confirmDialog({ title: "Delete list?", message: `“${list.title}” will be deleted.${entries} This can't be undone.`, confirm: "Delete list", danger: true, }); if (!ok) return; try { await api("DELETE", `/api/lists/${list.id}`); lists = lists.filter((l) => l.id !== list.id); render(); toast("List deleted"); } catch (err) { toast(err.message, { error: true }); } } function card(list) { const cat = catByKey[list.category]; const pct = Math.min(100, Math.round((list.itemCount / list.maxItems) * 100)); const imgs = covers[list.id] || []; const art = imgs.length ? h("span", { class: "list-card-covers" + ((list.art || cat.aspect) === "square" ? " is-square" : "") }, imgs.map((src) => h("span", { style: { backgroundImage: `url("${src}")` } }))) : h("span", { class: "shelf-icon" }, icon(cat.icon)); return h("li", { class: "list-card", "data-id": list.id }, h("button", { type: "button", class: "drag-handle", "aria-label": `Reorder ${list.title}. Use arrow keys to move.` }, icon("grip")), h("a", { class: "list-card-main", href: `/admin/lists/${list.id}` }, art, h("span", { style: { minWidth: 0 } }, h("span", { class: "list-card-title" }, list.title), h("span", { class: "list-card-meta" }, h("span", {}, cat.label), h("span", { "aria-hidden": "true" }, "·"), h("span", {}, `${fmt.format(list.itemCount)} / ${fmt.format(list.maxItems)}`), h("span", { class: "list-card-bar", "aria-hidden": "true" }, h("span", { style: { width: pct + "%" } })), !list.visible && h("span", { class: "badge badge--muted" }, icon("eye-off"), "Hidden"), ), ), ), h("div", { class: "list-card-actions" }, h("a", { class: "icon-btn", href: `/l/${list.slug}`, title: "View public page", "aria-label": "View public page" }, icon("external")), h("button", { type: "button", class: "icon-btn", title: "List settings", "aria-label": "List settings", onclick: () => editList(list) }, icon("settings")), h("button", { type: "button", class: "icon-btn icon-btn--danger", title: "Delete", "aria-label": "Delete list", onclick: () => deleteList(list) }, icon("trash")), ), ); } function render() { if (!lists.length) { root.replaceChildren(h("div", { class: "onboard" }, h("div", { class: "empty-art", style: { margin: "0 auto 16px" } }, icon("deck")), h("h2", {}, "Start your first list"), h("p", {}, "Pick a category. You'll choose how many spots it has next."), h("div", { class: "quick-cats" }, categories.map((c) => h("button", { type: "button", onclick: () => newList(c.key) }, icon(c.icon), c.label))), )); return; } const ul = h("ul", { class: "admin-lists", style: { listStyle: "none", margin: 0, padding: 0 } }, lists.map(card)); sortable(ul, { onMove: async () => { const ids = [...ul.children].map((li) => Number(li.dataset.id)); lists = ids.map((id) => lists.find((l) => l.id === id)); try { await api("POST", "/api/lists/order", { ids }); toast("Order saved"); } catch (err) { toast(err.message, { error: true }); } }, }); root.replaceChildren(ul); } document.querySelector("[data-action=new-list]").addEventListener("click", () => newList()); render();