[object Object]

← back to All Designerwallcoverings

grid: Book/Series facet in left rail + Website/Internal buttons on every card & list row (graceful disable)

a5f3545fad87f1e51d8d17784bf87ab18d98559a · 2026-07-07 07:18:59 -0700 · Steve

Files touched

Diff

commit a5f3545fad87f1e51d8d17784bf87ab18d98559a
Author: Steve <steve@designerwallcoverings.com>
Date:   Tue Jul 7 07:18:59 2026 -0700

    grid: Book/Series facet in left rail + Website/Internal buttons on every card & list row (graceful disable)
---
 public/index.html | 53 ++++++++++++++++++++++++++++++++++++++++++++---------
 1 file changed, 44 insertions(+), 9 deletions(-)

diff --git a/public/index.html b/public/index.html
index b64d540..bb5c6b9 100644
--- a/public/index.html
+++ b/public/index.html
@@ -76,8 +76,23 @@
   .when { margin-top:5px; font-size:calc(10px*var(--fs)); color:var(--mut); white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
   .when b { font-weight:700; letter-spacing:.03em; }
   .when.life-active b { color:#7fc98a; } .when.life-staged b { color:#c9dae8; } .when.life-archived b { color:#e0b3b3; }
+  /* per-card Website + Internal buttons — always resolve to the right link or disable */
+  .acts { display:flex; gap:5px; padding:0 calc(9px*var(--fs)) calc(9px*var(--fs)); margin-top:auto; }
+  .acts a { flex:1; text-align:center; font-size:calc(10.5px*var(--fs)); font-weight:600; padding:4px 5px; border-radius:5px;
+    text-decoration:none; border:1px solid var(--line); cursor:pointer; white-space:nowrap; letter-spacing:.02em; }
+  .acts a.web { background:var(--accent); color:#1a1407; border-color:var(--accent); }
+  .acts a.web:hover { filter:brightness(1.08); }
+  .acts a.int { background:transparent; color:var(--accent); }
+  .acts a.int:hover { border-color:var(--accent); }
+  .acts a.disabled { opacity:.3; pointer-events:none; background:transparent; color:var(--mut); border-color:var(--line); }
+  /* list-view compact link buttons */
+  td.acts-td { white-space:nowrap; }
+  td.acts-td a { display:inline-block; font-size:11px; font-weight:600; padding:3px 8px; border-radius:5px; text-decoration:none; border:1px solid var(--line); margin-right:4px; }
+  td.acts-td a.web { background:var(--accent); color:#1a1407; border-color:var(--accent); }
+  td.acts-td a.int { color:var(--accent); }
+  td.acts-td a.disabled { opacity:.3; pointer-events:none; color:var(--mut); }
   /* CARD FIELDS visibility toggles — all off = pure image-only texture wall */
-  .grid.h-sku .sku, .grid.h-ttl .ttl, .grid.h-vt .vt, .grid.h-fac .fac, .grid.h-price .price, .grid.h-when .when { display:none; }
+  .grid.h-sku .sku, .grid.h-ttl .ttl, .grid.h-vt .vt, .grid.h-fac .fac, .grid.h-price .price, .grid.h-when .when, .grid.h-act .acts { display:none; }
   .grid.h-sku.h-ttl.h-vt.h-fac.h-price.h-when .meta { display:none; }
 
   /* ── list view ── */
@@ -119,6 +134,7 @@
       <option value="title">Title</option>
       <option value="vendor">Vendor</option>
       <option value="type">Type</option>
+      <option value="series">Book / Series</option>
       <option value="sku">SKU</option>
       <option value="pattern">Pattern</option>
       <option value="color">Color (family)</option>
@@ -139,6 +155,7 @@
     <details class="sec" data-k="fields"><summary>Card fields<span class="n" id="nFields"></span></summary><div class="fbody" id="fieldsRow"></div></details>
     <details class="sec" data-k="vendor"><summary>Vendor<span class="n" id="nVendor"></span></summary><div class="fbody" id="vendorRow"></div></details>
     <details class="sec" data-k="type"><summary>Type<span class="n" id="nType"></span></summary><div class="fbody" id="typeRow"></div></details>
+    <details class="sec" data-k="series"><summary>Book / Series<span class="n" id="nSeries"></span></summary><div class="fbody" id="seriesRow"></div></details>
     <details class="sec" data-k="color"><summary>Color<span class="n" id="nColor"></span></summary><div class="fbody" id="colorRow"></div></details>
     <details class="sec" data-k="style"><summary>Style<span class="n" id="nStyle"></span></summary><div class="fbody" id="styleRow"></div></details>
     <details class="sec" data-k="material"><summary>Material<span class="n" id="nMaterial"></span></summary><div class="fbody" id="matRow"></div></details>
@@ -173,7 +190,7 @@ let state = {
   dir: urlq.get('dir') || localStorage.getItem('all_dir') || 'asc',
   view: urlq.get('view') || localStorage.getItem('all_view') || 'grid',
   q: urlq.get('q') || '',
-  vendors: loadSet('all_vendors'), types: loadSet('all_types'), lifecycles: loadSet('all_lifecycles'),
+  vendors: loadSet('all_vendors'), types: loadSet('all_types'), series: loadSet('all_series'), lifecycles: loadSet('all_lifecycles'),
   colors: loadSet('all_colors'), styles: loadSet('all_styles'), materials: loadSet('all_materials'),
   prices: loadSet('all_prices'), images: loadSet('all_images'),
   offset: 0, total: 0, loading: false, done: false,
@@ -181,6 +198,7 @@ let state = {
 // per-row UI state: expanded? + type-ahead needle (vendor/type rows have hundreds of values)
 const rowUI = {
   vendorRow: { expand: false, find: '', typeahead: true }, typeRow: { expand: false, find: '', typeahead: true },
+  seriesRow: { expand: false, find: '', typeahead: true },
   colorRow: { expand: false, find: '' }, styleRow: { expand: false, find: '' }, matRow: { expand: false, find: '' },
   priceRow: { expand: false, find: '' }, lifecycleRow: { expand: false, find: '' }, imgRow: { expand: false, find: '' },
 };
@@ -203,7 +221,7 @@ document.querySelectorAll('details.sec').forEach((d) => {
 });
 
 /* CARD FIELDS: toggle which card elements render — all off = image-only texture wall */
-const FIELD_DEFS = [['sku', 'SKU'], ['ttl', 'Title'], ['vt', 'Vendor · Type'], ['fac', 'Color / Style / Material'], ['price', 'Price'], ['when', 'Active / Staged date']];
+const FIELD_DEFS = [['sku', 'SKU'], ['ttl', 'Title'], ['vt', 'Vendor · Type'], ['fac', 'Color / Style / Material'], ['price', 'Price'], ['when', 'Active / Staged date'], ['act', 'Website / Internal buttons']];
 let hiddenFields = loadSet('all_hidden_fields');
 function drawFieldToggles() {
   const row = document.getElementById('fieldsRow'); row.innerHTML = '';
@@ -260,7 +278,7 @@ const COLS = [
   { label: 'Type', key: 'type' }, { label: 'Pattern', key: 'pattern' }, { label: 'Color', key: 'color' },
   { label: 'Style', key: 'style' }, { label: 'Material', key: 'material' },
   { label: 'Price', key: 'price', num: true }, { label: 'Lifecycle', key: 'lifecycle' },
-  { label: 'Shopify status', key: 'status' }, { label: 'Date', key: 'created' },
+  { label: 'Shopify status', key: 'status' }, { label: 'Date', key: 'created' }, { label: 'Links', key: null },
 ];
 function drawCols() {
   const tr = document.getElementById('cols'); tr.innerHTML = '';
@@ -283,14 +301,14 @@ drawCols();
 
 /* ── filters (shared with facets API) ── */
 const SETS = {
-  vendors: 'all_vendors', types: 'all_types', lifecycles: 'all_lifecycles', colors: 'all_colors',
+  vendors: 'all_vendors', types: 'all_types', series: 'all_series', lifecycles: 'all_lifecycles', colors: 'all_colors',
   styles: 'all_styles', materials: 'all_materials', prices: 'all_prices', images: 'all_images',
 };
 function persistFacets() { for (const [k, key] of Object.entries(SETS)) localStorage.setItem(key, JSON.stringify([...state[k]])); }
 function facetParams(extra = {}) {
   return new URLSearchParams({
     sort: state.sort, dir: state.dir, q: state.q,
-    vendors: [...state.vendors].join(','), types: [...state.types].join(','),
+    vendors: [...state.vendors].join(','), types: [...state.types].join(','), series: [...state.series].join(','),
     lifecycles: [...state.lifecycles].join(','), colors: [...state.colors].join(','),
     styles: [...state.styles].join(','), materials: [...state.materials].join(','),
     prices: [...state.prices].join(','), images: [...state.images].join(','), ...extra,
@@ -346,6 +364,7 @@ function renderFacets(f) {
   LAST_FACETS = f;
   facetList('vendorRow', f.vendor, state.vendors, { cap: 10 });
   facetList('typeRow', f.type, state.types, { cap: 10 });
+  facetList('seriesRow', f.series, state.series, { cap: 12 });
   facetList('colorRow', f.color, state.colors, { order: f.family_order, cap: 12 });
   facetList('styleRow', f.style, state.styles, { cap: 10 });
   facetList('matRow', f.material, state.materials, { cap: 10 });
@@ -355,10 +374,10 @@ function renderFacets(f) {
   renderActive();
 }
 
-const SECN = { Vendor: 'nVendor', Type: 'nType', Color: 'nColor', Style: 'nStyle', Material: 'nMaterial', Price: 'nPrice', Lifecycle: 'nLifecycle', Image: 'nImage' };
+const SECN = { Vendor: 'nVendor', Type: 'nType', Book: 'nSeries', Color: 'nColor', Style: 'nStyle', Material: 'nMaterial', Price: 'nPrice', Lifecycle: 'nLifecycle', Image: 'nImage' };
 function renderActive() {
   const row = document.getElementById('activeRow'); row.innerHTML = '';
-  const dims = [['Vendor', state.vendors], ['Type', state.types], ['Color', state.colors], ['Style', state.styles],
+  const dims = [['Vendor', state.vendors], ['Type', state.types], ['Book', state.series], ['Color', state.colors], ['Style', state.styles],
     ['Material', state.materials], ['Price', state.prices], ['Lifecycle', state.lifecycles], ['Image', state.images]];
   for (const [label, set] of dims) { // selection-count badge on each collapsed section header
     const b = document.getElementById(SECN[label]);
@@ -408,6 +427,18 @@ function whenChip(r) { // returns { html, textOnly } for grid card / list cell
   };
 }
 
+// Website → customer-facing storefront page; Internal → Shopify admin product view.
+// Each degrades to a disabled pill (never a dead link) when its URL can't resolve.
+function actButtonsHTML(r) {
+  const web = r.url
+    ? `<a class="web" href="${r.url}" target="_blank" rel="noopener noreferrer">Website</a>`
+    : `<a class="web disabled" title="Not live on the storefront yet">Website</a>`;
+  const int = r.internal_url
+    ? `<a class="int" href="${r.internal_url}" target="_blank" rel="noopener noreferrer">Internal</a>`
+    : `<a class="int disabled" title="No internal page for this SKU">Internal</a>`;
+  return web + int;
+}
+
 function card(r) {
   const el = document.createElement('div'); el.className = 'card';
   const stBadge = (r.lifecycle && r.lifecycle !== 'Active on site') ? `<span class="badge ${lifeClass(r)}">${r.lifecycle}</span>` : '';
@@ -417,7 +448,8 @@ function card(r) {
   const vt = [r.vendor, r.type].filter(Boolean).join(' · ');
   const fac = [...(r.colors || []), ...(r.styles || []), ...(r.materials || [])].slice(0, 4).join(' · ');
   const price = r.price ? `<span class="price">$${r.price.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}</span>` : '';
-  el.innerHTML = `${thumb}<div class="meta"><div class="sku">${r.sku || '—'}</div><div class="ttl" title="${(r.title || '').replace(/"/g, '&quot;')}">${r.title || ''}</div>${vt ? `<div class="vt">${vt}</div>` : ''}${fac ? `<div class="fac">${fac}</div>` : ''}${price}${whenChip(r).html}</div>`;
+  el.innerHTML = `${thumb}<div class="meta"><div class="sku">${r.sku || '—'}</div><div class="ttl" title="${(r.title || '').replace(/"/g, '&quot;')}">${r.title || ''}</div>${vt ? `<div class="vt">${vt}</div>` : ''}${fac ? `<div class="fac">${fac}</div>` : ''}${price}${whenChip(r).html}</div><div class="acts">${actButtonsHTML(r)}</div>`;
+  const acts = el.querySelector('.acts'); if (acts) acts.addEventListener('click', (e) => e.stopPropagation());
   if (r.url) { el.style.cursor = 'pointer'; el.onclick = () => window.open(r.url, '_blank', 'noopener,noreferrer'); }
   return el;
 }
@@ -439,6 +471,9 @@ function listRow(r) {
   tr.appendChild(cell(r.lifecycle ? `<span class="when ${lifeClass(r)}"><b>${r.lifecycle}</b></span>` : ''));
   tr.appendChild(cell(r.status || ''));
   tr.appendChild(cell(r.created ? `<span title="${new Date(r.created).toISOString()}">${fmtDate(r.created)}</span>` : ''));
+  const actTd = cell(actButtonsHTML(r), 'acts-td');
+  actTd.addEventListener('click', (e) => e.stopPropagation());
+  tr.appendChild(actTd);
   if (r.url) { tr.className = 'linked'; tr.onclick = () => window.open(r.url, '_blank', 'noopener,noreferrer'); }
   return tr;
 }

← 3876a0d api: carry series (Book) facet + website/internal_url per pr  ·  back to All Designerwallcoverings  ·  internal: expand per-card Internal into Admin / Line Viewer 9ca2518 →