[object Object]

← back to CelebritySignatures

web: clicking a signature opens its details popup; Books section (Open Library) for non-artist hands

e2d7d1ceb24806c7c5d09bfc4aca3edf1caaf1eb · 2026-08-05 17:35:53 -0700 · Steve Abrams

Files touched

Diff

commit e2d7d1ceb24806c7c5d09bfc4aca3edf1caaf1eb
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Wed Aug 5 17:35:53 2026 -0700

    web: clicking a signature opens its details popup; Books section (Open Library) for non-artist hands
---
 public/index.html | 42 ++++++++++++++++++++++++++++++++++--------
 1 file changed, 34 insertions(+), 8 deletions(-)

diff --git a/public/index.html b/public/index.html
index 163273b..db3e1a7 100644
--- a/public/index.html
+++ b/public/index.html
@@ -37,7 +37,7 @@
   main { padding:22px 28px 60px; }
   .grid { display:grid; grid-template-columns:repeat(var(--cols), 1fr); gap:18px; }
   .card { background:#fffef9; border:1px solid #ece8df; border-radius:4px; overflow:hidden; display:flex; flex-direction:column; box-shadow:0 1px 3px rgba(0,0,0,.06), 0 4px 16px rgba(0,0,0,.04); }
-  .sig { height:120px; display:flex; align-items:center; justify-content:center; padding:16px; background:#fff; border-bottom:1px solid var(--line); }
+  .sig { height:120px; display:flex; align-items:center; justify-content:center; padding:16px; background:#fff; border-bottom:1px solid var(--line); cursor:pointer; }
   .sig img { max-width:100%; max-height:100%; object-fit:contain; }
   .meta { padding:11px 13px 13px; display:flex; flex-direction:column; gap:6px; flex:1; }
   .name { font-weight:600; font-size:14.5px; line-height:1.25; overflow-wrap:break-word; word-break:break-word; hyphens:auto; }
@@ -423,8 +423,8 @@ function render() {
   $('#grid').innerHTML = rows.map(r => {
     const qid = (r.wikidata||'').split('/').pop();
     const evoN = (EVO[qid]?.sigs?.length) || 0;
-    return `<div class="card">
-      <div class="sig"><img loading="lazy" src="${r.signature_image_url}" alt="Signature of ${r.full_name}" onerror="this.parentNode.innerHTML='<span style=color:#bbb;font-size:12px>image unavailable</span>'"></div>
+    return `<div class="card" data-qid="${qid}">
+      <div class="sig" role="button" tabindex="0" aria-label="Open details for ${r.full_name}"><img loading="lazy" src="${r.signature_image_url}" alt="Signature of ${r.full_name}" onerror="this.parentNode.innerHTML='<span style=color:#bbb;font-size:12px>image unavailable</span>'"></div>
       <div class="meta">
         <div class="name">${r.full_name}</div>
         <a class="info-chip" data-qid="${qid}" href="/a/${qid}">${PORTRAITS[qid]?`<img class="chip-face" loading="lazy" referrerpolicy="no-referrer" src="${esc(PORTRAITS[qid])}" alt="" onerror="this.remove()">`:''}Details${evoN>1?` · ${evoN} signatures`:''}</a>
@@ -571,6 +571,23 @@ async function loadWorks(qid, name){
   } catch {}
   WORKS_CACHE[qid]=works; return works;
 }
+// Books loader — Open Library (keyless, CORS-open) for the non-artist hands
+// (authors, presidents, founders): cover tiles that link to the book page.
+const BOOKS_CACHE = {};
+async function loadBooks(qid, name){
+  if (BOOKS_CACHE[qid]) return BOOKS_CACHE[qid];
+  const books = [];
+  try {
+    const j = await (await fetch(`https://openlibrary.org/search.json?author=${encodeURIComponent(name)}&sort=editions&limit=10&fields=title,cover_i,first_publish_year,key`)).json();
+    for (const d of (j.docs||[])) {
+      if (books.length >= 6) break;
+      if (!d.cover_i) continue;
+      books.push({ img:`https://covers.openlibrary.org/b/id/${d.cover_i}-M.jpg`, title:d.title||'Untitled',
+        date:d.first_publish_year?String(d.first_publish_year):'', url:d.key?`https://openlibrary.org${d.key}`:'https://openlibrary.org', src:'Open Library' });
+    }
+  } catch {}
+  BOOKS_CACHE[qid]=books; return books;
+}
 function openArtist(qid, push=true){
   const r = BY_QID[qid] || DATA.find(x=>(x.wikidata||'').endsWith('/'+qid));
   if (!r) return;
@@ -590,7 +607,7 @@ function openArtist(qid, push=true){
     ${evoStripHTML(qid)}
     ${isArtist
       ? `<div class="ap-sec">Works — open museum collections</div><div class="works" id="apWorks"><span class="ap-sub">loading works…</span></div>`
-      : (portrait ? `<div class="ap-sec">Portrait</div><div class="ap-figure"><img referrerpolicy="no-referrer" src="${esc(portrait.replace('width=120','width=520'))}" alt="Portrait of ${esc(r.full_name)}" onerror="this.closest('.ap-figure').remove()"><div class="ap-sub">Public-domain / CC portrait via Wikimedia Commons</div></div>` : '')}
+      : `${portrait ? `<div class="ap-sec">Portrait</div><div class="ap-figure"><img referrerpolicy="no-referrer" src="${esc(portrait.replace('width=120','width=520'))}" alt="Portrait of ${esc(r.full_name)}" onerror="this.closest('.ap-figure').remove()"><div class="ap-sub">Public-domain / CC portrait via Wikimedia Commons</div></div>` : ''}<div class="ap-sec">Books</div><div class="works" id="apWorks"><span class="ap-sub">loading books…</span></div>`}
   </div>
   <div class="ap-cta"><a href="/murals?name=${encodeURIComponent(r.full_name)}">Order a mural featuring this signature →</a></div>`;
   const pop=$('#artistPop');
@@ -601,11 +618,15 @@ function openArtist(qid, push=true){
   document.querySelector('.ap-x').focus();
   document.body.style.overflow = 'hidden';
   if (push) { const u=new URLSearchParams(location.search); u.set('artist',qid); history.pushState({},'', '?'+u.toString()); }
-  if (isArtist) loadWorks(qid, r.full_name).then(ws=>{
+  const renderTiles = (ws, emptyHtml) => {
     const el=$('#apWorks'); if(!el) return;
     el.innerHTML = ws.length ? ws.map(w=>`<a href="${esc(w.url)}" target="_blank" rel="noopener noreferrer"><img loading="lazy" referrerpolicy="no-referrer" src="${esc(w.img)}" alt="${esc(w.title)}" onerror="this.closest('a').remove()"><span class="wt">${esc(w.title)}${w.date?' · '+esc(w.date):''}${w.src?' · '+esc(w.src):''}</span></a>`).join('')
-      : `<span class="ap-sub">no openly-licensed works found in the open museum APIs — <a href="${esc(r.wikidata)}" target="_blank" rel="noopener noreferrer">see Wikidata</a></span>`;
-  });
+      : emptyHtml;
+  };
+  if (isArtist) loadWorks(qid, r.full_name).then(ws=>renderTiles(ws,
+    `<span class="ap-sub">no openly-licensed works found in the open museum APIs — <a href="${esc(r.wikidata)}" target="_blank" rel="noopener noreferrer">see Wikidata</a></span>`));
+  else loadBooks(qid, r.full_name).then(bs=>renderTiles(bs,
+    `<span class="ap-sub">no books found on Open Library — <a href="${esc(r.wikidata)}" target="_blank" rel="noopener noreferrer">see Wikidata</a></span>`));
 }
 let POP_RETURN_FOCUS = null;
 function closeArtist(push=true){
@@ -617,9 +638,14 @@ function closeArtist(push=true){
 }
 document.addEventListener('click', (e)=>{
   const c=e.target.closest('a.info-chip'); if(c){ e.preventDefault(); openArtist(c.dataset.qid); return; }
+  // clicking the signature itself opens the same details popup (artwork/books)
+  const s=e.target.closest('.card .sig'); if(s){ const card=s.closest('.card'); if(card&&card.dataset.qid){ openArtist(card.dataset.qid); return; } }
   if (e.target.closest('.ap-x')||e.target.classList.contains('ap-back')) closeArtist();
 });
-document.addEventListener('keydown', (e)=>{ if(e.key==='Escape') closeArtist(); });
+document.addEventListener('keydown', (e)=>{
+  if(e.key==='Escape') closeArtist();
+  if((e.key==='Enter'||e.key===' ') && e.target.classList?.contains('sig')){ const card=e.target.closest('.card'); if(card&&card.dataset.qid){ e.preventDefault(); openArtist(card.dataset.qid); } }
+});
 function maybeOpenFromURL(){ const q=new URLSearchParams(location.search).get('artist'); if(q&&DATA.length) openArtist(q,false); else if(!q) closeArtist(false); }
 
 fetch('/api/signatures').then(r=>r.json()).then(d => { DATA=d; readURL(); buildChips(); render(); setView(prefs.view); maybeOpenFromURL(); });

← 91f1e57 iOS app: white gallery theme + all images render + tap-signa  ·  back to CelebritySignatures  ·  auto-save: 2026-08-05T17:44:25 (1 files) — apps/mobile/store ab0c89f →