From 75aebdcf040184b777816a0411e8760848cca281 Mon Sep 17 00:00:00 2001 From: "prompt.ac/@jeffrey" Date: Mon, 20 Jul 2026 14:46:19 -0700 Subject: [PATCH] Add editorial Whistlegraph thumbnails --- spec/whistlegraph-admin-spec.mjs | 2 ++ system/netlify/functions/whistlegraph-admin-lib.mjs | 5 +++++ system/public/whistlegraph.org/admin.html | 6 +++--- system/public/whistlegraph.org/index.html | 5 +++-- 4 files changed, 13 insertions(+), 5 deletions(-) diff --git a/spec/whistlegraph-admin-spec.mjs b/spec/whistlegraph-admin-spec.mjs index d88bbec33..6449c00a8 100644 --- a/spec/whistlegraph-admin-spec.mjs +++ b/spec/whistlegraph-admin-spec.mjs @@ -71,6 +71,8 @@ describe("Whistlegraph Desk", () => { expect(() => normalizePostPatch({ works: ["missing"] }, new Set(["sos"]))).toThrowError(/Unknown Whistlegraph code/); expect(normalizeWorkPatch({ title: " Scared of Stairs ", by: "Jeffrey", year: 2021, c: "#B44887" })) .toEqual({ title: "Scared of Stairs", by: "Jeffrey", year: 2021, c: "#b44887" }); + expect(normalizeWorkPatch({ featuredPost: "7143034832430320939" })).toEqual({ featuredPost: "7143034832430320939" }); + expect(() => normalizeWorkPatch({ featuredPost: "not-a-post" })).toThrowError(/only numbers/); expect(normalizeWorkCode(" FEAR ")).toBe("fear"); expect(() => normalizeWorkCode("not-a-code")).toThrowError(/letters or numbers/); }); diff --git a/system/netlify/functions/whistlegraph-admin-lib.mjs b/system/netlify/functions/whistlegraph-admin-lib.mjs index 5d5d0d858..af61ff649 100644 --- a/system/netlify/functions/whistlegraph-admin-lib.mjs +++ b/system/netlify/functions/whistlegraph-admin-lib.mjs @@ -177,6 +177,11 @@ export function normalizeWorkPatch(input) { if (!COLOR_RE.test(color)) throw new Error("Color must be a six-digit hex value."); patch.c = color.toLowerCase(); } + if (own(input, "featuredPost")) { + const featuredPost = cleanText(input.featuredPost, "Featured post ID", 32); + if (featuredPost && !/^\d+$/.test(featuredPost)) throw new Error("Featured post ID must contain only numbers."); + patch.featuredPost = featuredPost; + } if (!Object.keys(patch).length) throw new Error("No editable Whistlegraph fields supplied."); return patch; } diff --git a/system/public/whistlegraph.org/admin.html b/system/public/whistlegraph.org/admin.html index 6ed46036d..cb2eef089 100644 --- a/system/public/whistlegraph.org/admin.html +++ b/system/public/whistlegraph.org/admin.html @@ -193,7 +193,7 @@ document.getElementById("codexOpen").addEventListener("click",openCodex);codexShade.addEventListener("click",closeCodex); function postTitle(p){const text=(p.desc||"").trim();return text?text.slice(0,100):`Post ${p.id}`} - function heroForWork(code){return posts.filter(p=>(p.works||[]).includes(code)).sort((a,b)=>(b.views||0)-(a.views||0))[0]} + function heroForWork(code){const related=posts.filter(p=>(p.works||[]).includes(code)).sort((a,b)=>(b.views||0)-(a.views||0));return related.find(p=>p.id===workByCode[code]?.featuredPost)||related[0]} function buildView(){ const q=query.trim().toLowerCase(); if(mode==="trash"){ @@ -300,8 +300,8 @@

Comma-separated codes. Empty means this post is unfiled.

Comma-separated plot names.

${loadingVisual?'

Loading private machine visual notes…

':machineFields(p)}
`} - function workFields(w,editableCode=false){return `${editableCode?`

Changing this migrates every assigned post and retires the old URL.

`:""}`} - function workPatch(){return {title:drawer.querySelector("#workTitle").value,by:drawer.querySelector("#workBy").value,year:Number(drawer.querySelector("#workYear").value),c:drawer.querySelector("#workColor").value}} + function workFields(w,editableCode=false){return `${editableCode?`

Changing this migrates every assigned post and retires the old URL.

`:""}

Optional. Paste an assigned archive post ID to choose its thumbnail.

`} + function workPatch(){return {title:drawer.querySelector("#workTitle").value,by:drawer.querySelector("#workBy").value,year:Number(drawer.querySelector("#workYear").value),c:drawer.querySelector("#workColor").value,featuredPost:drawer.querySelector("#featuredPost").value.trim()}} async function createWork(){ const button=drawer.querySelector("#createWork"),code=drawer.querySelector("#workCode").value.trim().toLowerCase();button.disabled=true;button.textContent="Creating…"; try{const result=await getJSON(`${API}?action=work`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${token}`},body:JSON.stringify({code,...workPatch()})});curation.createdWorks[result.key]=result.patch;applyCuration();mode="works";document.querySelectorAll("#mode button").forEach(x=>x.classList.toggle("on",x.dataset.mode==="works"));document.getElementById("filter").classList.add("hidden");renderReset();closeEditor();toast(`Created [${result.key}]`)} diff --git a/system/public/whistlegraph.org/index.html b/system/public/whistlegraph.org/index.html index 0b305b7f9..bf5f0ce1f 100644 --- a/system/public/whistlegraph.org/index.html +++ b/system/public/whistlegraph.org/index.html @@ -654,7 +654,8 @@ function loadPosts(){ if(related.length||generatedRelationships.has(work.code)){ work.perf=related.length; work.views=related.reduce((sum,p)=>sum+(p.views||0),0); - if(!work.slug&&related[0]?.thumb) work.thumb=related[0].thumb; + const featured=related.find(post=>post.id===work.featuredPost)||related[0]; + if(!work.slug&&featured?.thumb) work.thumb=featured.thumb; } } WORKS=[...WORKS.slice(0,CURATED_N),...WORKS.slice(CURATED_N).sort((a,b)=>(b.views||0)-(a.views||0))]; @@ -769,7 +770,7 @@ function showDetail(code, keepScroll){ D.videoRule.style.display=""; D.glyphCap.style.display="none"; D.glyphCap.textContent=""; D.video.classList.toggle("wide",!!w.wide); // the Longest film is landscape const aset=w.asset||w.code; // renamed codes keep their original CDN asset key - const hero=posts[0]; // most-seen post fronts the record + const hero=posts.find(post=>post.id===w.featuredPost)||posts[0]; // editorial choice, then most-seen D.video.poster=hero&&hero.thumb?hero.thumb:(w.noGlyph?"":`${IDX}/${aset}.jpg`); D.video.src=hero?hero.src:`${IDX}/${aset}.mp4`; if(w.film){ // the Rhizome film, not a TikTok take — its own provenance + glyph -- 2.51.2