← 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, '"')}">${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, '"')}">${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 →