Save source descriptions, add "Why it's #N", fix IGDB studio names

Descriptions
- Items keep the source's description (TMDB/TheTVDB overview, IGDB
  summary, Wikipedia intro) in a new items.description column
  (migration 3); editable in the item form
- Deezer/MusicBrainz track counts and album names are now display-only
  hints, not saved as descriptions

Why it's #N
- The owner's note is presented as "Why it's #N" in the editor and the
  public detail view, above an "About" section with the description

IGDB developers
- IGDB credits internal divisions ("Nintendo EAD", "Nintendo EPD
  Production Group No. 8"). A developer whose name shares its first word
  with its parent company is credited to the parent ("Nintendo");
  independently named studios keep their name (Retro Studios, Naughty
  Dog, Landfall Games)
- Fall back to the publisher when IGDB lists no developer

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
cdnutterandClaude Opus 5.5 committed 2026-09-24 18:05:20 -07:00
1 parent 3923444b3b
commit 7320e3f707
16 files changed
+207 -44

No files matched your search

+8 -4
View File
@@ -120,13 +120,15 @@ code { font: 0.9em ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; bac
/* ---------- profile / home ---------- */
.profile { display: flex; align-items: center; gap: 28px; padding-top: 56px; padding-bottom: 28px; }
/* Profile pictures: the image as-is, just cropped to a circle. The flat
accent fill only shows behind the initial when there's no picture. */
.avatar {
flex: none; display: grid; place-items: center; overflow: hidden; border-radius: 50%;
background: linear-gradient(135deg, var(--accent), color-mix(in srgb, var(--accent) 45%, var(--bg)));
color: var(--accent-text); font-weight: 700;
background: var(--accent); color: var(--accent-text); font-weight: 700;
}
.avatar:has(img) { background: none; }
.avatar img { width: 100%; height: 100%; object-fit: cover; }
.avatar--lg { width: 112px; height: 112px; font-size: 44px; box-shadow: 0 0 0 4px var(--bg), 0 0 0 5px var(--border); }
.avatar--lg { width: 112px; height: 112px; font-size: 44px; }
.profile-name { font-size: clamp(30px, 5vw, 46px); font-weight: 780; }
.profile-tagline { margin-top: 8px; color: var(--muted); font-size: 17px; max-width: 60ch; }
.profile-stats { margin-top: 12px; color: var(--faint); font-size: 14px; }
@@ -331,8 +333,10 @@ dialog[open] { animation: pop 0.18s cubic-bezier(.2,.8,.2,1); }
.dlg-meta { display: grid; grid-template-columns: auto 1fr; gap: 6px 16px; margin: 4px 0 0; font-size: 14px; }
.dlg-meta dt { color: var(--faint); }
.dlg-meta dd { margin: 0; }
.dlg-why h3, .dlg-about h3 { font: 700 11.5px/1.2 var(--font); text-transform: uppercase; letter-spacing: 0.08em; color: var(--faint); margin: 6px 0 8px; }
.dlg-desc { color: var(--muted); font-size: 14.5px; line-height: 1.6; white-space: pre-line; }
.dlg-note {
margin: 4px 0 0; padding: 14px 16px; border-left: 3px solid var(--accent);
margin: 0; padding: 14px 16px; border-left: 3px solid var(--accent);
background: var(--accent-soft); border-radius: 0 var(--radius) var(--radius) 0;
font-size: 15px; white-space: pre-line;
}
+9 -4
View File
@@ -271,7 +271,7 @@ function renderResults(res) {
h("div", { style: { minWidth: 0 } },
h("div", { class: "result-title" }, r.title),
sub && h("div", { class: "result-sub" }, sub),
r.overview && h("div", { class: "result-overview" }, r.overview),
(r.overview || r.hint) && h("div", { class: "result-overview" }, r.overview || r.hint),
),
action,
);
@@ -294,6 +294,7 @@ function fromResult(r) {
return {
title: r.title, subtitle: r.subtitle, year: r.year, externalUrl: r.externalUrl,
imageUrl: r.imageUrl, provider: r.provider, providerId: r.providerId, extra: { ...(r.extra || {}) },
description: r.overview || "",
};
}
@@ -344,7 +345,10 @@ function openItemForm({ item, result }) {
const title = h("input", { required: true, maxlength: 500, value: src.title || "" });
const subtitle = h("input", { maxlength: 500, value: src.subtitle || "" });
const year = h("input", { maxlength: 20, value: src.year || "", inputmode: "numeric", style: { maxWidth: "140px" } });
const note = h("textarea", { rows: 3, maxlength: 10000, value: item?.note || "", placeholder: "Why it's here. Shown to visitors." });
const description = h("textarea", { rows: 4, maxlength: 10000, value: src.description || "", placeholder: "What it is, in a few sentences." });
const note = h("textarea", { rows: 3, maxlength: 10000, value: item?.note || "", placeholder: "What earns it this spot? Shown to visitors." });
const source = { tmdb: "TMDB", tvdb: "TheTVDB", igdb: "IGDB", wikipedia: "Wikipedia" }[src.provider];
const rankLabel = editing ? `#${item.rank}` : `#${list.itemCount + 1}`;
const link = h("input", { type: "url", maxlength: 2000, value: src.externalUrl || "", placeholder: "https://" });
// Theater production gets top billing with suggestions.
@@ -438,7 +442,8 @@ function openItemForm({ item, result }) {
field(cat.key === "theater" ? "Year (of this production)" : "Year", year),
),
extraInputs.length > 0 && h("div", { class: "row" }, extraInputs.map((x) => field(x.label, x.input))),
field("Your note", note),
field("Description", description, source ? `From ${source}. Edit it if you like.` : "Optional. Shown to visitors."),
field(`Why it's ${rankLabel}`, note, "Your take on why it earns its place in the ranking."),
field("Link", link, "Where the title links to on your page."),
error,
),
@@ -461,7 +466,7 @@ function openItemForm({ item, result }) {
if (production) newExtra.production = production.value.trim();
for (const x of extraInputs) newExtra[x.key] = x.input.value.trim();
const body = {
title: title.value, subtitle: subtitle.value, year: year.value, note: note.value,
title: title.value, subtitle: subtitle.value, year: year.value, note: note.value, description: description.value,
externalUrl: link.value.trim(), extra: newExtra,
imageUrl: clearImage ? "" : imageUrl.value.trim(), clearImage,
};
+1 -1
View File
@@ -84,7 +84,7 @@ function profileSection() {
h("div", { class: "row" }, field("Your name", ownerName, "Shown as the page heading."), field("Site title", siteTitle, "Shown in the header and browser tab. Leave blank to use “Your name's Canon”.")),
field("Tagline", tagline),
field("Entries per list on the home page", preview, "Visitors see the full list on each list's page. 0 shows only the heading."),
h("label", { class: "check" }, notes, "Show my notes to visitors"),
h("label", { class: "check" }, notes, "Show why each entry is ranked where it is"),
h("div", { class: "settings-actions" }, save),
);
form.addEventListener("submit", async (e) => {
+6 -1
View File
@@ -38,7 +38,12 @@ function renderDialog(list, idx) {
sub && h("p", { class: "dlg-sub" }, sub),
it.badge && h("span", { class: "badge" }, it.badge),
meta.length > 0 && h("dl", { class: "dlg-meta" }, meta.map((m) => [h("dt", {}, m.label), h("dd", {}, m.value)])),
it.note && h("blockquote", { class: "dlg-note" }, it.note),
it.note && h("section", { class: "dlg-why" },
h("h3", {}, `Why it's #${it.rank}`),
h("blockquote", { class: "dlg-note" }, it.note)),
it.description && h("section", { class: "dlg-about" },
h("h3", {}, "About"),
h("p", { class: "dlg-desc" }, it.description)),
h("div", { class: "dlg-actions" },
it.link && h("a", { class: "btn btn--sm", href: it.link, target: "_blank", rel: "noopener noreferrer" },
icon("external"), it.source ? `View on ${it.source}` : "Learn more"),