← back to Dw Photo Capture
All-Shopify cards: per-card 'Replace all — make this the only image' toggle (DTD verdict C); default keeps existing imagery, explicit opt-in to wipe
708fac74cd8724a222802f15d3cbe6fa6ab9bfbe · 2026-06-24 17:56:37 -0700 · Steve
Files touched
M data/build.jsonM public/index.htmlM server.js
Diff
commit 708fac74cd8724a222802f15d3cbe6fa6ab9bfbe
Author: Steve <steve@designerwallcoverings.com>
Date: Wed Jun 24 17:56:37 2026 -0700
All-Shopify cards: per-card 'Replace all — make this the only image' toggle (DTD verdict C); default keeps existing imagery, explicit opt-in to wipe
---
data/build.json | 5 +++--
public/index.html | 8 +++++++-
server.js | 6 ++++++
3 files changed, 16 insertions(+), 3 deletions(-)
diff --git a/data/build.json b/data/build.json
index d75c025..45cda72 100644
--- a/data/build.json
+++ b/data/build.json
@@ -1,5 +1,5 @@
{
- "next": 13,
+ "next": 14,
"map": {
"578af86f": 2,
"bc95fdb0": 3,
@@ -11,6 +11,7 @@
"09a337f9": 9,
"423881b4": 10,
"76a90668": 11,
- "5ef2ebe2": 12
+ "5ef2ebe2": 12,
+ "788487db": 13
}
}
\ No newline at end of file
diff --git a/public/index.html b/public/index.html
index 311e93e..073c241 100644
--- a/public/index.html
+++ b/public/index.html
@@ -45,6 +45,10 @@
.price{color:var(--green);font-weight:700}
.price.no{color:var(--muted)}
.when{font-size:11px;color:var(--muted)}
+ /* "make this the only image" — destructive opt-in, only shown when an existing image would be wiped */
+ .onlyimg{display:flex;align-items:center;gap:7px;font-size:11.5px;color:var(--muted);margin-top:2px;cursor:pointer;user-select:none}
+ .onlyimg input{accent-color:var(--red);width:15px;height:15px;flex:0 0 auto}
+ .onlyimg.armed{color:var(--red);font-weight:600}
.actions{display:flex;gap:8px;margin-top:auto;padding-top:6px}
.shoot{flex:1;position:relative;overflow:hidden;background:var(--gold);color:#1b1407;border:none;border-radius:12px;padding:13px;font-weight:700;font-size:15px;text-align:center;cursor:pointer}
.shoot input{position:absolute;inset:0;opacity:0;font-size:200px;cursor:pointer}
@@ -239,6 +243,7 @@ function render(){
</div>
${x.ts?`<div class="when">🕓 ${fmt(x.ts)}${x.live?(x.created?' · 🟢 CREATED + LIVE':' · 🟢 LIVE on store'):x.shopify_pushed?(x.live_reason?(' · photo on Shopify ('+x.live_reason+')'):' · photo on Shopify'):''}</div>`:''}
${x.push_err?`<div class="when" style="color:var(--red)">${x.push_err}</div>`:''}
+ ${(x.keep_images&&x.image)?`<label class="onlyimg${x._replaceAll?' armed':''}"><input type="checkbox" class="onlyimg-cb"${x._replaceAll?' checked':''}> Replace all — make this the only image</label>`:''}
<div class="actions">
${adjustable?'<button class="enh">✨ Enhance</button>':''}
<label class="shoot">${isNew?'📷 Create + Go Live':photographed?'📷 Update Photo':'📷 Add Photo'}<input type="file" accept="image/*" capture="environment"></label>
@@ -247,6 +252,7 @@ function render(){
</div>`;
const editSrc=()=> (x.photo||('/api/current-image?pid='+x.product_id))+(x.photo?'?':'&')+'t='+Date.now();
el.querySelector('.shoot input').addEventListener('change',e=>shoot(x,e.target.files[0]));
+ const oi=el.querySelector('.onlyimg-cb'); if(oi) oi.addEventListener('change',e=>{ x._replaceAll=e.target.checked; el.querySelector('.onlyimg').classList.toggle('armed',e.target.checked); });
const enh=el.querySelector('.enh'); if(enh) enh.addEventListener('click',()=>openEditor(x,editSrc()));
const th=el.querySelector('.thumb.tap'); if(th) th.addEventListener('click',()=>openEditor(x,editSrc()));
const sk=el.querySelector('.skip'); if(sk) sk.addEventListener('click',()=>skip(x));
@@ -276,7 +282,7 @@ async function uploadPhoto(x,dataUrl){
toast((x.needs_create?'Creating ':'Uploading ')+x.dw_sku+'…');
try{
const r=await fetch('/api/photo',{method:'POST',headers:{'Content-Type':'application/json'},
- body:JSON.stringify({dw_sku:x.dw_sku,product_id:x.product_id,dataUrl,keep_images:!!x.keep_images})});
+ body:JSON.stringify({dw_sku:x.dw_sku,product_id:x.product_id,dataUrl,keep_images:!!x.keep_images&&!x._replaceAll})});
const d=await r.json();
if(d.ok){ Object.assign(x,{done:true,needs_create:false,created:d.created,photo:d.photo,ts:new Date().toISOString(),shopify_pushed:d.shopify_pushed,push_err:d.push_err,live:d.live,live_reason:d.live_reason});
toast(d.created&&d.live?('🟢 Created + LIVE · '+x.dw_sku):d.created?('Created (draft) · '+x.dw_sku+(d.live_reason?' — '+d.live_reason:'')):d.live?('🟢 '+x.dw_sku+' is LIVE!'):d.shopify_pushed?('✓ Photo on Shopify · '+x.dw_sku+(d.live_reason?(' (needs '+d.live_reason.replace('needs ','')+')'):'')):('Saved '+x.dw_sku+(d.push_err?' (push failed)':''))); render();
diff --git a/server.js b/server.js
index ae8e977..0ea5571 100644
--- a/server.js
+++ b/server.js
@@ -182,6 +182,12 @@ const server = http.createServer((req, res) => {
return send(res, 200, { total: matches.length, items: matches, indexed: INDEX.length });
}
+ // All sheet GRS items that don't exist in Shopify yet — the "create on photo" worklist.
+ if (u.pathname === '/api/new' && req.method === 'GET') {
+ const items = INDEX.filter(x => x.needs_create);
+ return send(res, 200, { total: items.length, items });
+ }
+
if (u.pathname === '/api/reindex' && req.method === 'POST') {
buildCatalog().then(() => {}).catch(() => {});
return send(res, 200, { ok: true, indexing: true, current: CATALOG.length });
← 05042e0 Any-SKU: include all 170 TWIL-sheet GRS items; photographing
·
back to Dw Photo Capture
·
auto-save: 2026-06-24T17:58:43 (3 files) — data/build.json p 06a420e →