← 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
M .deploy.confM .gitignoreM public/index.htmlM server.js
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 →