Add titles-only list export and Custom lists
Export - GET /l/<list>/export?format=csv|txt downloads a list's titles in rank order, one per line, with no header or other fields (CSV gets a UTF-8 BOM for Excel) - Export menu in the list editor and, for the owner, on the public list page; hidden lists stay owner-only Custom lists - New "Custom" category for anything not built in (sports teams, drinks…): owner-named, slug from the title, entries added by hand or from an unfiltered Wikipedia search - Per-list artwork shape (square or tall), stored in a new lists.art column (migration 2) - Art can be uploaded while adding an entry, not only after Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
e9a75a968a
commit
58a29b7339
23 files changed
+314
-37
No files matched your search
@@ -11,7 +11,8 @@ function sizeHint(n, limits) {
|
||||
return { text: `Up to ${fmt.format(limits.max)} allowed; ${limits.recommended} or fewer recommended.`, cls: "" };
|
||||
}
|
||||
|
||||
const autoTitle = (n, cat) => `Top ${n} ${cat.label}`;
|
||||
// Custom lists have no sensible default name; the owner names them.
|
||||
const autoTitle = (n, cat) => (cat.key === "custom" ? "" : `Top ${n} ${cat.label}`);
|
||||
|
||||
/**
|
||||
* @param {object} o
|
||||
@@ -32,18 +33,29 @@ export function openListForm({ categories, limits, list, category, onSaved }) {
|
||||
let titleTouched = editing && list.title !== autoTitle(list.maxItems, catByKey[list.category]);
|
||||
|
||||
const title = h("input", { name: "title", required: true, maxlength: 200, value: list?.title || autoTitle(state.size, catByKey[state.category]) });
|
||||
title.addEventListener("input", () => { titleTouched = true; });
|
||||
const syncTitle = () => { if (!titleTouched) title.value = autoTitle(state.size, catByKey[state.category]); };
|
||||
title.addEventListener("input", () => { titleTouched = title.value !== ""; });
|
||||
const isCustom = () => state.category === "custom";
|
||||
const syncTitle = () => {
|
||||
title.placeholder = isCustom() ? "e.g. Favorite Sports Teams" : "";
|
||||
if (!titleTouched) title.value = autoTitle(state.size, catByKey[state.category]);
|
||||
};
|
||||
|
||||
const lockedCat = editing && list.itemCount > 0;
|
||||
const catPicker = h("div", { class: "cat-picker", role: "radiogroup", "aria-label": "Category" },
|
||||
categories.map((c) => h("label", { class: "cat-option" },
|
||||
categories.map((c) => h("label", { class: "cat-option" + (c.key === "custom" ? " cat-option--wide" : "") },
|
||||
h("input", {
|
||||
type: "radio", name: "category", value: c.key, checked: c.key === state.category,
|
||||
disabled: lockedCat && c.key !== state.category,
|
||||
onchange: () => { state.category = c.key; syncTitle(); },
|
||||
onchange: () => {
|
||||
state.category = c.key;
|
||||
syncTitle();
|
||||
artField.hidden = !isCustom();
|
||||
if (isCustom() && !title.value) title.focus();
|
||||
},
|
||||
}),
|
||||
h("span", {}, icon(c.icon), c.label),
|
||||
c.key === "custom"
|
||||
? h("span", {}, icon(c.icon), h("b", {}, c.label), h("small", { class: "muted" }, "Anything else: sports teams, drinks, restaurants…"))
|
||||
: h("span", {}, icon(c.icon), c.label),
|
||||
)),
|
||||
);
|
||||
|
||||
@@ -61,6 +73,7 @@ export function openListForm({ categories, limits, list, category, onSaved }) {
|
||||
};
|
||||
sizeInput.addEventListener("input", () => { state.size = Number(sizeInput.value); renderSize(); syncTitle(); });
|
||||
renderSize();
|
||||
syncTitle();
|
||||
|
||||
const desc = h("textarea", { name: "description", rows: 2, maxlength: 1000, placeholder: "Optional: a line about what makes this list", value: list?.description || "" });
|
||||
const slug = editing && h("input", { name: "slug", value: list.slug, maxlength: 60, pattern: "[a-z0-9-]+", spellcheck: false });
|
||||
@@ -69,6 +82,14 @@ export function openListForm({ categories, limits, list, category, onSaved }) {
|
||||
h("label", {}, h("input", { type: "radio", name: "layout", value: "grid", checked: layout === "grid" }), icon("grid"), "Artwork grid"),
|
||||
h("label", {}, h("input", { type: "radio", name: "layout", value: "rows", checked: layout === "rows" }), icon("rows"), "Ranked list"),
|
||||
);
|
||||
// Custom lists choose their artwork shape; built-in categories have one.
|
||||
const art = list?.art || "square";
|
||||
const artField = h("div", { class: "field", hidden: !isCustom() },
|
||||
h("span", { class: "field-label" }, "Artwork shape"),
|
||||
h("div", { class: "radio-cards" },
|
||||
h("label", {}, h("input", { type: "radio", name: "art", value: "square", checked: art === "square" }), "Square (logos, covers)"),
|
||||
h("label", {}, h("input", { type: "radio", name: "art", value: "poster", checked: art === "poster" }), "Tall (posters, photos)"),
|
||||
));
|
||||
const visible = h("input", { type: "checkbox", name: "visible", checked: list ? list.visible : true });
|
||||
const error = h("p", { class: "alert alert--error", role: "alert", hidden: true });
|
||||
const save = h("button", { type: "submit", class: "btn btn--primary" }, editing ? "Save changes" : "Create list");
|
||||
@@ -86,6 +107,7 @@ export function openListForm({ categories, limits, list, category, onSaved }) {
|
||||
editing && h("label", { class: "field" }, h("span", { class: "field-label" }, "Address"),
|
||||
h("div", { class: "input-group", style: { alignItems: "center" } }, h("span", { class: "muted" }, "/l/"), slug)),
|
||||
h("div", { class: "field" }, h("span", { class: "field-label" }, "Layout"), layoutGroup),
|
||||
artField,
|
||||
h("label", { class: "check" }, visible, "Show this list on my public page"),
|
||||
error,
|
||||
),
|
||||
@@ -98,6 +120,12 @@ export function openListForm({ categories, limits, list, category, onSaved }) {
|
||||
form.addEventListener("submit", async (e) => {
|
||||
e.preventDefault();
|
||||
error.hidden = true;
|
||||
if (!title.value.trim()) {
|
||||
error.hidden = false;
|
||||
error.textContent = "Give your list a name.";
|
||||
title.focus();
|
||||
return;
|
||||
}
|
||||
const n = Number(sizeInput.value);
|
||||
if (!Number.isInteger(n) || n < 1 || n > limits.max) {
|
||||
error.hidden = false;
|
||||
@@ -111,6 +139,7 @@ export function openListForm({ categories, limits, list, category, onSaved }) {
|
||||
description: desc.value,
|
||||
layout: form.querySelector("input[name=layout]:checked").value,
|
||||
visible: visible.checked,
|
||||
art: isCustom() ? form.querySelector("input[name=art]:checked").value : "",
|
||||
};
|
||||
if (editing) body.slug = slug.value;
|
||||
busy(save, true);
|
||||
|
||||
Reference in new issue
Block a user