[object Object]

← back to Trending Dw

trending: 'Put on Shopify' button — stages our-own designs as DW products with sequential DWPV SKUs (one SKU per unique design, idempotent); queue viewer w/ staged date+time; queue file deploy-protected

732d40db344192f18d915d34708ea0c5e712eb83 · 2026-07-03 09:24:54 -0700 · Steve

Files touched

Diff

commit 732d40db344192f18d915d34708ea0c5e712eb83
Author: Steve <steve@designerwallcoverings.com>
Date:   Fri Jul 3 09:24:54 2026 -0700

    trending: 'Put on Shopify' button — stages our-own designs as DW products with sequential DWPV SKUs (one SKU per unique design, idempotent); queue viewer w/ staged date+time; queue file deploy-protected
---
 .deploy.conf      |  1 +
 .gitignore        |  1 +
 public/index.html | 46 ++++++++++++++++++++++++++++++++++++++++++++--
 server.js         | 49 +++++++++++++++++++++++++++++++++++++++++++++++++
 4 files changed, 95 insertions(+), 2 deletions(-)

diff --git a/.deploy.conf b/.deploy.conf
index 56ef6d9..034fc27 100644
--- a/.deploy.conf
+++ b/.deploy.conf
@@ -3,3 +3,4 @@ DEPLOY_PATH=/root/Projects/trending-dw
 HEALTH_URL=https://trending.designerwallcoverings.com/
 PORT=9954
 INSTALL_CMD="true"   # zero-dependency node http server — no npm ci (no package-lock.json); "" would fall through to the npm ci default
+RSYNC_EXTRA_EXCLUDES="shopify-queue.json"   # mutable staging state — never clobber prod's copy on deploy (server creates it on first stage)
diff --git a/.gitignore b/.gitignore
index 1924158..df50c78 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,3 +6,4 @@ tmp/
 dist/
 build/
 .next/
+data/shopify-queue.json
diff --git a/public/index.html b/public/index.html
index f6dd77a..edf759c 100644
--- a/public/index.html
+++ b/public/index.html
@@ -51,6 +51,11 @@
   .company{ font-size:12px; } .company b{ color:var(--accent); }
   .chips{ display:flex; flex-wrap:wrap; gap:5px; margin-top:2px; }
   .chip{ font-size:10px; padding:3px 7px; border-radius:20px; background:var(--chip); color:#5a5346; }
+  .shopbtn{ margin-top:9px; width:100%; border:1px solid #1f7a4d; background:#1f7a4d; color:#fff; font-size:12px; font-weight:600; padding:7px 10px; border-radius:8px; cursor:pointer; letter-spacing:.02em; }
+  .shopbtn:hover{ background:#18633e; }
+  .shopbtn.done{ background:#eef4ef; color:#1f7a4d; border-color:#bcd8c7; cursor:default; }
+  .queuebtn{ border:1px solid #d9d2c4; background:#fff; color:#5a5346; font-size:12px; padding:5px 10px; border-radius:8px; cursor:pointer; }
+  .queuebtn b{ color:#1f7a4d; }
   .chip.mk{ background:#1c1a17; color:#fff; }
   .signal{ font-size:11.5px; color:var(--muted); } .signal .dot{ color:var(--accent); }
   .tier{ font-size:11px; color:var(--ink); background:#f6f2e8; border:1px dashed var(--line); border-radius:6px; padding:3px 7px; margin-top:auto; }
@@ -83,6 +88,7 @@
       <div class="count"><b id="cnt">0</b> data points</div>
       <div class="gapcount" id="gapcnt"></div>
       <div class="toggle"><button id="vGrid" class="on">Grid</button><button id="vLanes">Lanes</button></div>
+      <button class="queuebtn" id="queueBtn" onclick="openQueue()">🛍 Shopify queue <b id="qn">0</b></button>
       <div><label>Sort</label>
         <select id="sort">
           <option value="signal">Bestseller signal</option>
@@ -155,8 +161,10 @@ function card(it){
     +'<div class="chips"><span class="chip mk">'+esc(it.marketplace)+'</span><span class="chip">'+esc(it.style)+'</span><span class="chip">'+esc(it.color)+'</span><span class="chip">'+esc(it.priceBand)+'</span></div>'
     +'<div class="signal"><span class="dot">●</span> '+esc(it.signal)+'</div>'
     +'<div class="tier">DW tier → '+esc(it.dwTier)+(it.url?' · <a class="src" href="'+it.url+'" target="_blank" rel="noopener noreferrer">source ↗</a>':'')+'</div>'
+    +(it.isOurOriginal?'<button class="shopbtn" data-id="'+esc(it.id)+'">+ Put on Shopify</button>':'')
     +'</div>';
-  c.style.cursor="pointer"; c.title="Match against our catalog"; c.addEventListener("click",function(e){ if(!e.target.closest("a")) openMatchup(it); });
+  c.style.cursor="pointer"; c.title="Match against our catalog"; c.addEventListener("click",function(e){ if(!e.target.closest("a")&&!e.target.closest(".shopbtn")) openMatchup(it); });
+  var sb=c.querySelector(".shopbtn"); if(sb){ sb.addEventListener("click",function(e){ e.stopPropagation(); putOnShopify(sb); }); }
   return c;
 }
 async function fetchPage(){
@@ -201,7 +209,41 @@ document.getElementById('vGrid').onclick=()=>setView('grid'); document.getElemen
 new IntersectionObserver(es=>{ if(es[0].isIntersecting) fetchPage(); },{rootMargin:'400px'}).observe(document.getElementById('sentinel'));
 
 const _v=new URLSearchParams(location.search).get('view'); if(_v==='lanes'||_v==='grid') state.view=_v;
-loadFacets(); chips(); setView(state.view);
+loadFacets(); chips(); setView(state.view); refreshQueueCount();
+
+/* ---- Put on Shopify: stage our-own design as a DW product (sequential DWPV SKU) ---- */
+function putOnShopify(btn){
+  var id=btn.getAttribute("data-id"); btn.disabled=true; var old=btn.textContent; btn.textContent="Staging…";
+  fetch("/api/put-on-shopify?id="+encodeURIComponent(id)).then(function(r){return r.json();}).then(function(d){
+    if(d.sku){ btn.textContent=(d.already?"✓ "+d.sku+" (already staged)":"✓ "+d.sku+" staged"); btn.classList.add("done"); refreshQueueCount(); }
+    else { btn.textContent=d.error||"Failed"; btn.disabled=false; setTimeout(function(){btn.textContent=old;},2600); }
+  }).catch(function(){ btn.textContent="Error — retry"; btn.disabled=false; setTimeout(function(){btn.textContent=old;},2600); });
+}
+function refreshQueueCount(){ fetch("/api/shopify-queue").then(function(r){return r.json();}).then(function(d){ document.getElementById("qn").textContent=d.count; }).catch(function(){}); }
+function openQueue(){
+  fetch("/api/shopify-queue").then(function(r){return r.json();}).then(function(d){
+    var m=document.getElementById("qmodal");
+    if(!m){ m=document.createElement("div"); m.id="qmodal"; m.style.cssText="position:fixed;inset:0;background:rgba(20,18,15,.6);display:none;align-items:center;justify-content:center;z-index:9999;padding:20px";
+      m.innerHTML='<div id="qbox" style="background:#fff;max-width:820px;width:100%;max-height:88vh;overflow:auto;border-radius:14px"></div>';
+      m.addEventListener("click",function(e){ if(e.target===m) m.style.display="none"; }); document.body.appendChild(m); }
+    var rows=(d.products||[]).map(function(p){
+      var when=p.stagedAt?new Date(p.stagedAt).toLocaleString(undefined,{year:"numeric",month:"short",day:"numeric",hour:"numeric",minute:"2-digit"}):"";
+      return '<tr style="border-top:1px solid #eee"><td style="padding:8px 10px">'
+        +(p.image?'<img src="'+p.image+'" style="width:46px;height:46px;object-fit:cover;border-radius:6px">':'')+'</td>'
+        +'<td style="padding:8px 10px;font-weight:600;color:#1f7a4d;white-space:nowrap">'+esc(p.sku)+'</td>'
+        +'<td style="padding:8px 10px">'+esc((p.title||"").slice(0,46))+'<div style="font-size:11px;color:#888">'+esc(p.style||"")+' · '+esc(p.color||"")+' · '+esc(p.priceTier||"")+'</div></td>'
+        +'<td style="padding:8px 10px;font-size:11px;color:#888;white-space:nowrap" title="'+esc(p.stagedAt||"")+'">🕓 '+esc(when)+'</td>'
+        +'<td style="padding:8px 10px;font-size:11px;color:#b8860b">'+esc(p.status||"staged")+'</td></tr>';
+    }).join("");
+    document.getElementById("qbox").innerHTML='<div style="padding:18px 22px;border-bottom:1px solid #eee;display:flex;align-items:center;gap:12px">'
+      +'<div style="font-size:17px;font-weight:600">🛍 Shopify staging queue</div>'
+      +'<div style="color:#888;font-size:13px">'+(d.count||0)+' × '+esc(d.series||"DWPV")+' — staged, awaiting the gated live push</div>'
+      +'<button onclick="document.getElementById(\'qmodal\').style.display=\'none\'" style="margin-left:auto;border:0;background:#f2efe9;border-radius:8px;padding:8px 12px;cursor:pointer">✕</button></div>'
+      +(d.count?'<table style="width:100%;border-collapse:collapse;font-size:13px"><thead><tr style="text-align:left;color:#999;font-size:11px"><th style="padding:6px 10px"></th><th style="padding:6px 10px">SKU</th><th style="padding:6px 10px">Design</th><th style="padding:6px 10px">Staged</th><th style="padding:6px 10px">Status</th></tr></thead><tbody>'+rows+'</tbody></table>'
+        :'<div style="padding:26px;color:#999">Nothing staged yet — click <b>+ Put on Shopify</b> on any card with an our-own design.</div>');
+    document.getElementById("qmodal").style.display="flex";
+  });
+}
 
 /* ---- trending → catalog matchup modal (internal "look at the item" + yesterday's find-similar) ---- */
 function openMatchup(it){
diff --git a/server.js b/server.js
index f95d5eb..4c017a8 100644
--- a/server.js
+++ b/server.js
@@ -10,6 +10,8 @@ const PUBLIC = path.join(__dirname, 'public');
 const DATA = path.join(__dirname, 'data', 'bestsellers.json');
 const CATALOG = path.join(__dirname, '..', 'pattern-vault', 'data', 'catalog.json');   // our own catalog (for gap scoring)
 const OURCAT = path.join(__dirname, 'data', 'our-catalog.json');   // normalized our-own designs (WPB originals + DW storefront) — built by scripts/attach-images.js
+const SHOPIFY_QUEUE = path.join(__dirname, 'data', 'shopify-queue.json');   // staged DW products awaiting the (gated) live Shopify push
+const DW_SERIES = 'DWPV';   // DW Pattern Vault — AI-original vault designs
 const AUTH = process.env.BASIC_AUTH === undefined ? 'admin:DW2024!' : process.env.BASIC_AUTH; // BASIC_AUTH='' disables (public go-live is gated)
 
 const MIME = { '.html':'text/html', '.js':'application/javascript', '.css':'text/css', '.json':'application/json', '.svg':'image/svg+xml', '.ico':'image/x-icon', '.webp':'image/webp', '.png':'image/png', '.jpg':'image/jpeg', '.jpeg':'image/jpeg', '.gif':'image/gif', '.avif':'image/avif' };
@@ -68,6 +70,39 @@ function applyFilters(items, sp){
 
 function send(res, code, body, type){ res.writeHead(code, { 'Content-Type': type||'application/json', 'Cache-Control':'no-store' }); res.end(body); }
 
+// ---- DW Shopify staging: assign sequential DWPV SKUs to our-own designs, stage payloads ----
+function loadQueue(){ try { return JSON.parse(fs.readFileSync(SHOPIFY_QUEUE,'utf8')) || []; } catch(e){ return []; } }
+function saveQueue(q){ fs.writeFileSync(SHOPIFY_QUEUE, JSON.stringify(q, null, 2)); }
+function nextSku(q){
+  const max = q.reduce((m,p)=>{ const n = parseInt(String(p.sku||'').replace(DW_SERIES+'-',''),10); return isNaN(n)?m:Math.max(m,n); }, 0);
+  return DW_SERIES + '-' + String(max+1).padStart(4,'0');
+}
+// stage a product for one trending item's OUR-OWN design (idempotent per design)
+function stageForItem(it, nowIso){
+  if (!it || !it.image || !it.isOurOriginal) return { error: 'no our-own design to list (GAP lanes need generation first)' };
+  const q = loadQueue();
+  const designKey = it.imageDesign || it.image;
+  const existing = q.find(p => p.designKey === designKey);
+  if (existing) return { sku: existing.sku, staged: true, already: true, product: existing };
+  const product = {
+    sku: nextSku(q),
+    designKey,
+    title: it.imageDesign || it.title,
+    vendor: 'Designer Wallcoverings',
+    series: DW_SERIES,
+    image: it.image,
+    style: it.style,
+    color: it.color,
+    priceTier: it.dwTier,
+    trendSignal: it.signal,
+    signalRank: it.signalRank,
+    status: 'staged',            // staged -> (gated) pushed
+    stagedAt: nowIso
+  };
+  q.push(product); saveQueue(q);
+  return { sku: product.sku, staged: true, already: false, product };
+}
+
 const server = http.createServer((req,res)=>{
   if (AUTH){
     const expect = 'Basic ' + Buffer.from(AUTH).toString('base64');
@@ -89,6 +124,20 @@ const server = http.createServer((req,res)=>{
     return send(res, 200, JSON.stringify({ lanes, ourCatalog: ourCatalogCount() }));
   }
 
+  if (u.pathname === '/api/put-on-shopify'){                // stage an our-own design as a DW product (sequential DWPV SKU)
+    const id = u.searchParams.get('id');
+    const it = loadItems().find(x => x.id === id);
+    if (!it) return send(res, 404, JSON.stringify({ error: 'item not found' }));
+    const nowIso = new Date(Date.now()).toISOString();
+    const r = stageForItem(it, nowIso);
+    return send(res, r.error ? 409 : 200, JSON.stringify(r));
+  }
+
+  if (u.pathname === '/api/shopify-queue'){                 // list staged DW products
+    const q = loadQueue();
+    return send(res, 200, JSON.stringify({ series: DW_SERIES, count: q.length, products: q }));
+  }
+
   if (u.pathname === '/api/matchup'){                       // trending item -> closest designs in OUR OWN catalog
     const q = u.searchParams.get('q') || '';
     const cat = loadOurCatalog();

← de3b2ec deploy: trending-dw live with real our-own images; INSTALL_C  ·  back to Trending Dw  ·  trending: scripts/push-to-shopify.js — creates DW products f bd1056c →