β back to Dw Photo Capture
Create 22 net-new GRS drafts (full sheet info, photo->live) + per-SKU photo gallery: πΌ Photos shows every image with β
Feature / β Remove / π· Add (keep existing). /api/images,/api/image/delete,/api/image/feature
087b386d1fe246ba800c902d7adcceb876b1ada7 Β· 2026-06-24 18:53:04 -0700 Β· steve@designerwallcoverings.com
Files touched
A create_22_new.pyM data/build.jsonM data/queue.jsonM public/index.htmlM server.js
Diff
commit 087b386d1fe246ba800c902d7adcceb876b1ada7
Author: steve@designerwallcoverings.com <steve@designerwallcoverings.com>
Date: Wed Jun 24 18:53:04 2026 -0700
Create 22 net-new GRS drafts (full sheet info, photo->live) + per-SKU photo gallery: πΌ Photos shows every image with β
Feature / β Remove / π· Add (keep existing). /api/images,/api/image/delete,/api/image/feature
---
create_22_new.py | 56 +++++++++++++++
data/build.json | 5 +-
data/queue.json | 198 ++++++++++++++++++++++++++++++++++++++++++++++++++++++
public/index.html | 73 ++++++++++++++++++++
server.js | 26 +++++++
5 files changed, 356 insertions(+), 2 deletions(-)
diff --git a/create_22_new.py b/create_22_new.py
new file mode 100644
index 0000000..3a42139
--- /dev/null
+++ b/create_22_new.py
@@ -0,0 +1,56 @@
+#!/usr/bin/env python3
+"""Bulk-create the net-new GRS sheet items as DRAFT Shopify products with full info
+ (no image yet β they go live once photographed). DRY default; CONFIRM=1 writes."""
+import os,sys,json,re,time,urllib.request
+SHOP="designer-laboratory-sandbox.myshopify.com";API="2024-10";DRY=os.environ.get("CONFIRM","0")!="1"
+TOKEN=""
+for l in open(os.path.expanduser("~/Projects/secrets-manager/.env")):
+ if l.startswith("SHOPIFY_ADMIN_TOKEN="): TOKEN=l.strip().split("=",1)[1];break
+H={"X-Shopify-Access-Token":TOKEN,"Content-Type":"application/json"}
+def api(m,p,pl=None):
+ d=json.dumps(pl).encode() if pl is not None else None
+ r=urllib.request.Request(f"https://{SHOP}/admin/api/{API}"+p,data=d,headers=H,method=m)
+ with urllib.request.urlopen(r,timeout=60) as x: b=x.read().decode();return x.status,(json.loads(b) if b.strip() else {})
+SHEET={x["grs"].upper():x for x in json.load(open(os.path.expanduser("~/Projects/dw-photo-capture/data/sheet_grs.json")))}
+# the net-new list straight from the app
+import urllib.request as u, base64
+req=u.Request("http://127.0.0.1:9890/api/new",headers={"Authorization":"Basic "+base64.b64encode(b"admin:DW2024!").decode()})
+NEW=json.loads(u.urlopen(req,timeout=20).read())["items"]
+def title(s):
+ name=(s.get("name") or "").strip(); color=(s.get("color") or "").strip()
+ nl,cl=name.lower(),color.lower()
+ col=(" "+color) if (color and nl!=cl and cl not in nl and nl not in cl) else ""
+ if name: return f"{name}{col} Grasscloth Wallcovering | Fentucci"
+ if s.get("material"): return f"Fentucci {s['material']} Grasscloth Wallcovering"+((" β "+color) if color else "")
+ return "Fentucci Grasscloth Wallcovering"+((" β "+color) if color else "")
+def body(s):
+ lead=s.get("name") or (("Fentucci "+s["material"]+" grasscloth") if s.get("material") else "This Fentucci grasscloth")
+ de=(s.get("desc") or "").strip()
+ if de and de.lower().startswith("is "): return f"{lead} {de}"
+ return de or f"{lead} is a natural woven grasscloth wallcovering with handcrafted natural-fiber texture."
+def mf(ns,k,v): return {"namespace":ns,"key":k,"value":str(v),"type":"single_line_text_field"} if v else None
+print(f"{len(NEW)} net-new to create as DRAFT (full info, no image)")
+created=0
+for it in NEW:
+ s=SHEET.get(it["dw_sku"].upper())
+ if not s: print(f" skip {it['dw_sku']} (no sheet row)"); continue
+ grs=s["grs"]; price=s.get("price"); col=(s.get("color") or "").strip()
+ tags=["Grasscloth","Natural Wallcovering","Fentucci","TWIL Naturals","display_variant","Needs-Image"]+([f"color:{col}"] if col else [])+([] if price else ["Needs-Cost"])
+ roll={"option1":"Roll","sku":grs,"inventory_management":"shopify","inventory_quantity":2026}
+ if price: roll["price"]=str(price)
+ payload={"product":{"title":title(s),"body_html":body(s),"vendor":"Fentucci","product_type":"Wallcovering",
+ "status":"draft","published_scope":"global","tags":", ".join(tags),"options":[{"name":"Size"}],
+ "variants":[roll,{"option1":"Sample","sku":grs+"-Sample","price":"4.25","inventory_management":"shopify","inventory_quantity":2026}],
+ "metafields":[x for x in [mf("global","width",s.get("width") or "36 Inches"),mf("global","length",s.get("length") or "8 Yards"),
+ mf("global","unit_of_measure","Priced Per Single Roll"),mf("global","Content","Natural Grasscloth"),
+ mf("global","Brand","Fentucci"),mf("global","Collection","TWIL Naturals"),mf("global","Color-Way",col),mf("custom","color",col),
+ mf("custom","manufacturer_sku",s.get("mfr")),mf("dwc","manufacturer_sku",s.get("mfr")),
+ mf("custom","pattern_name",s.get("name")),mf("dwc","pattern_name",s.get("name"))] if x]}}
+ if DRY: print(f" [dry] {grs:12} {('$'+str(price)) if price else 'no-price':9} {title(s)[:46]}"); continue
+ try:
+ st,r=api("POST","/products.json",payload); created+=1
+ print(f" created {grs} -> {r.get('product',{}).get('id')} (draft)"); time.sleep(0.5)
+ except Exception as e:
+ bd=getattr(e,'read',lambda:b'')() if hasattr(e,'read') else b''; print(f" ERR {grs}: {e} {bd[:140]}")
+if DRY: print(f"\n[DRY] would create {len(NEW)} drafts. CONFIRM=1 to create.")
+else: print(f"\nDONE β created {created} draft products (Needs-Image; photograph to go live).")
diff --git a/data/build.json b/data/build.json
index d30ab2b..9a222da 100644
--- a/data/build.json
+++ b/data/build.json
@@ -1,5 +1,5 @@
{
- "next": 16,
+ "next": 17,
"map": {
"578af86f": 2,
"bc95fdb0": 3,
@@ -14,6 +14,7 @@
"5ef2ebe2": 12,
"788487db": 13,
"3a1be30f": 14,
- "d0f0df55": 15
+ "d0f0df55": 15,
+ "9905b54e": 16
}
}
\ No newline at end of file
diff --git a/data/queue.json b/data/queue.json
index 7ca6d22..434d1ab 100644
--- a/data/queue.json
+++ b/data/queue.json
@@ -26,6 +26,15 @@
"price": "32.85",
"done": false
},
+ {
+ "product_id": "7867561902131",
+ "title": "Assisi Straw Grasscloth Wallcovering | Fentucci",
+ "dw_sku": "GRS-27490",
+ "mfr": "EG501",
+ "status": "DRAFT",
+ "price": "50.40",
+ "done": false
+ },
{
"product_id": "7822226030643",
"title": "Belluno Sage Green Grasscloth Wallcovering | Fentucci",
@@ -215,6 +224,24 @@
"price": "36.30",
"done": false
},
+ {
+ "product_id": "7867561771059",
+ "title": "Creamy Grasscloth Wallcovering | Fentucci",
+ "dw_sku": "GRS-30600",
+ "mfr": "NAT4018",
+ "status": "DRAFT",
+ "price": "32.85",
+ "done": false
+ },
+ {
+ "product_id": "7867562065971",
+ "title": "Cremona Parchment Grasscloth Wallcovering | Fentucci",
+ "dw_sku": "GRS-27560",
+ "mfr": "75085W-01",
+ "status": "DRAFT",
+ "price": "47.55",
+ "done": false
+ },
{
"product_id": "7822227341363",
"title": "Cuneo Grasscloth Wallcovering | Fentucci",
@@ -269,6 +296,33 @@
"price": "38.40",
"done": false
},
+ {
+ "product_id": "7867561443379",
+ "title": "Fentucci Grasscloth Wallcovering",
+ "dw_sku": "GRS-26870",
+ "mfr": "wl332t",
+ "status": "DRAFT",
+ "price": "38.85",
+ "done": false
+ },
+ {
+ "product_id": "7867561508915",
+ "title": "Fentucci Grasscloth Wallcovering",
+ "dw_sku": "GRS-27100",
+ "mfr": "md5056t",
+ "status": "DRAFT",
+ "price": "32.85",
+ "done": false
+ },
+ {
+ "product_id": "7867561574451",
+ "title": "Fentucci Grasscloth Wallcovering",
+ "dw_sku": "GRS-27110",
+ "mfr": "mc1717",
+ "status": "DRAFT",
+ "price": "37.65",
+ "done": false
+ },
{
"product_id": "7822227603507",
"title": "Ferrara Black Wash Grasscloth Wallcovering | Fentucci",
@@ -512,6 +566,15 @@
"price": "32.85",
"done": false
},
+ {
+ "product_id": "7867562098739",
+ "title": "Modena Gold Grasscloth Wallcovering | Fentucci",
+ "dw_sku": "GRS-27570",
+ "mfr": "C3529",
+ "status": "DRAFT",
+ "price": "36.45",
+ "done": false
+ },
{
"product_id": "7822228815923",
"title": "Murano Grasscloth Wallcovering | Fentucci",
@@ -521,6 +584,33 @@
"price": "45.30",
"done": false
},
+ {
+ "product_id": "7867562164275",
+ "title": "Napoli Caramel Grasscloth Wallcovering | Fentucci",
+ "dw_sku": "GRS-27600",
+ "mfr": "MG2020",
+ "status": "DRAFT",
+ "price": "53.85",
+ "done": false
+ },
+ {
+ "product_id": "7867561607219",
+ "title": "Natural Grasscloth Wallcovering | Fentucci",
+ "dw_sku": "GRS-27180",
+ "mfr": "sh1521T",
+ "status": "DRAFT",
+ "price": "38.55",
+ "done": false
+ },
+ {
+ "product_id": "7867562131507",
+ "title": "Oliva Khaki Grasscloth Wallcovering | Fentucci",
+ "dw_sku": "GRS-27580",
+ "mfr": "BA227",
+ "status": "DRAFT",
+ "price": "37.35",
+ "done": false
+ },
{
"product_id": "7822229995571",
"title": "Oristano Grasscloth Wallcovering | Fentucci",
@@ -539,6 +629,15 @@
"price": "36.00",
"done": false
},
+ {
+ "product_id": "7867561967667",
+ "title": "Palermo Wheat Grasscloth Wallcovering | Fentucci",
+ "dw_sku": "GRS-27530",
+ "mfr": "BA454",
+ "status": "DRAFT",
+ "price": "35.25",
+ "done": false
+ },
{
"product_id": "7822230192179",
"title": "Parma Grasscloth Wallcovering | Fentucci",
@@ -719,6 +818,15 @@
"price": "54.15",
"done": false
},
+ {
+ "product_id": "7867562000435",
+ "title": "Salerno Sand Grasscloth Wallcovering | Fentucci",
+ "dw_sku": "GRS-27540",
+ "mfr": "wS4723T",
+ "status": "DRAFT",
+ "price": "38.85",
+ "done": false
+ },
{
"product_id": "7822231273523",
"title": "Sanremo Grasscloth Wallcovering | Fentucci",
@@ -818,6 +926,24 @@
"price": "32.85",
"done": false
},
+ {
+ "product_id": "7867561934899",
+ "title": "Sorrento Natural Grasscloth Wallcovering | Fentucci",
+ "dw_sku": "GRS-27520",
+ "mfr": "sh1518T",
+ "status": "DRAFT",
+ "price": "38.55",
+ "done": false
+ },
+ {
+ "product_id": "7867562033203",
+ "title": "Spoleto Bone Grasscloth Wallcovering | Fentucci",
+ "dw_sku": "GRS-27550",
+ "mfr": "C4539T",
+ "status": "DRAFT",
+ "price": "35.70",
+ "done": false
+ },
{
"product_id": "7822231666739",
"title": "Stresa Grasscloth Wallcovering | Fentucci",
@@ -935,6 +1061,15 @@
"price": "33.90",
"done": false
},
+ {
+ "product_id": "7867561869363",
+ "title": "Verona Butter Grasscloth Wallcovering | Fentucci",
+ "dw_sku": "GRS-27480",
+ "mfr": "SH1519T",
+ "status": "DRAFT",
+ "price": "38.55",
+ "done": false
+ },
{
"product_id": "7822226063411",
"title": "Vigevano Beige Blue On Lightr Ochre Grasscloth Wallcovering | Fentucci",
@@ -944,6 +1079,24 @@
"price": "36.00",
"done": false
},
+ {
+ "product_id": "7867561738291",
+ "title": "Classic Denim Blue Grasscloth Wallcovering | Fentucci",
+ "dw_sku": "GRS-30580",
+ "mfr": "zg15326",
+ "status": "DRAFT",
+ "price": "0.00",
+ "done": false
+ },
+ {
+ "product_id": "7867561836595",
+ "title": "Cream, Black, Gold Grasscloth Wallcovering | Fentucci",
+ "dw_sku": "GRS-30740",
+ "mfr": "sg1069",
+ "status": "DRAFT",
+ "price": "0.00",
+ "done": false
+ },
{
"product_id": "7822232453171",
"title": "Fentucci Abaca Grasscloth Wallcovering",
@@ -989,6 +1142,33 @@
"price": "4.25",
"done": false
},
+ {
+ "product_id": "7867561476147",
+ "title": "Fentucci Grasscloth Wallcovering",
+ "dw_sku": "GRS-26990",
+ "mfr": "tsl7553",
+ "status": "DRAFT",
+ "price": "0.00",
+ "done": false
+ },
+ {
+ "product_id": "7867561672755",
+ "title": "Fentucci Grasscloth Wallcovering",
+ "dw_sku": "GRS-30500",
+ "mfr": "m62012",
+ "status": "DRAFT",
+ "price": "0.00",
+ "done": false
+ },
+ {
+ "product_id": "7867561705523",
+ "title": "Fentucci Grasscloth Wallcovering",
+ "dw_sku": "GRS-30520",
+ "mfr": "mnr2122",
+ "status": "DRAFT",
+ "price": "0.00",
+ "done": false
+ },
{
"product_id": "7822233010227",
"title": "Fentucci Herringbone Grasscloth Wallcovering",
@@ -1151,6 +1331,24 @@
"price": null,
"done": false
},
+ {
+ "product_id": "7867561803827",
+ "title": "Olivia Green Grasscloth Wallcovering | Fentucci",
+ "dw_sku": "GRS-30610",
+ "mfr": "SG5059T",
+ "status": "DRAFT",
+ "price": "0.00",
+ "done": false
+ },
+ {
+ "product_id": "7867561639987",
+ "title": "Teal Green Blue Grasscloth Wallcovering | Fentucci",
+ "dw_sku": "GRS-27290",
+ "mfr": "mnr1213",
+ "status": "DRAFT",
+ "price": "0.00",
+ "done": false
+ },
{
"product_id": "7822232715315",
"title": "Trieste Grasscloth Wallcovering | Fentucci",
diff --git a/public/index.html b/public/index.html
index 0c61c6b..6a7cc7a 100644
--- a/public/index.html
+++ b/public/index.html
@@ -54,10 +54,28 @@
.shoot input{position:absolute;inset:0;opacity:0;font-size:200px;cursor:pointer}
.skip{background:#16140f;border:1px solid var(--line);color:var(--muted);border-radius:12px;padding:13px 14px;font-size:14px;cursor:pointer}
.enh{background:#16140f;border:1px solid var(--gold);color:var(--gold);border-radius:12px;padding:13px 14px;font-size:14px;font-weight:600;cursor:pointer;white-space:nowrap}
+ .gal{background:#16140f;border:1px solid var(--line);color:var(--ink);border-radius:12px;padding:13px 12px;font-size:14px;font-weight:600;cursor:pointer;white-space:nowrap}
.card.done .shoot{background:#16140f;color:var(--gold);border:1px solid var(--gold)}
.empty{grid-column:1/-1;text-align:center;color:var(--muted);padding:40px 10px}
.toast{position:fixed;left:50%;bottom:24px;transform:translateX(-50%);background:#16140f;border:1px solid var(--gold);color:var(--ink);padding:11px 18px;border-radius:12px;font-size:14px;z-index:50;opacity:0;transition:opacity .2s;pointer-events:none}
.toast.show{opacity:1}
+ /* photo gallery */
+ .gallery{position:fixed;inset:0;z-index:90;background:rgba(10,9,7,.98);display:flex;flex-direction:column}
+ .gallery[hidden]{display:none}
+ .gal-head{position:sticky;top:0;display:flex;align-items:center;gap:10px;padding:max(12px,env(safe-area-inset-top)) 14px 12px;border-bottom:1px solid var(--line);background:rgba(10,9,7,.98)}
+ .gal-x{background:#16140f;border:1px solid var(--line);color:var(--ink);border-radius:10px;padding:9px 13px;font-size:14px;cursor:pointer}
+ .gal-title{flex:1;min-width:0;line-height:1.2} .gal-title b{display:block;font:600 15px/1.2 "SF Pro Display",sans-serif} .gal-title span{font:600 12px/1 ui-monospace,Menlo,monospace;color:var(--gold)}
+ .gal-add{position:relative;overflow:hidden;background:var(--gold);color:#1b1407;border-radius:10px;padding:9px 14px;font-weight:700;font-size:14px;cursor:pointer}
+ .gal-add input{position:absolute;inset:0;opacity:0;font-size:120px;cursor:pointer}
+ .gal-grid{flex:1;overflow-y:auto;padding:14px;display:grid;grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:12px;align-content:start}
+ .gtile{position:relative;border:1px solid var(--line);border-radius:12px;overflow:hidden;background:#0c0b09;aspect-ratio:1}
+ .gtile img{width:100%;height:100%;object-fit:cover;display:block}
+ .gtile.feat{border-color:var(--gold);box-shadow:0 0 0 2px rgba(200,162,74,.4)}
+ .gfeat-badge{position:absolute;top:6px;left:6px;background:var(--gold);color:#1b1407;font:700 10px/1 sans-serif;padding:3px 7px;border-radius:99px}
+ .gtile-acts{position:absolute;bottom:0;left:0;right:0;display:flex;gap:1px}
+ .gtile-acts button{flex:1;border:none;padding:9px 4px;font-size:12px;font-weight:600;cursor:pointer;background:rgba(15,14,12,.92);color:var(--ink)}
+ .gtile-acts .g-feat{color:var(--gold)} .gtile-acts .g-del{color:var(--red)}
+ .gal-empty{grid-column:1/-1;text-align:center;color:var(--muted);padding:40px 10px}
/* ===== Photo editor (opens after capture, before upload) ===== */
.editor{position:fixed;inset:0;z-index:100;background:rgba(8,7,6,.97);display:flex;flex-direction:column}
@@ -118,6 +136,16 @@
<main id="grid"></main>
<div class="toast" id="toast"></div>
+<!-- ===== Photo gallery (all photos for a SKU: keep / remove / feature / add) ===== -->
+<div class="gallery" id="gallery" hidden>
+ <div class="gal-head">
+ <button class="gal-x" id="galClose">βΉ Back</button>
+ <div class="gal-title"><b id="galName"></b><span id="galSku"></span></div>
+ <label class="gal-add">π· Add<input type="file" accept="image/*" capture="environment" id="galAdd"></label>
+ </div>
+ <div class="gal-grid" id="galGrid"></div>
+</div>
+
<!-- ===== Photo editor modal (review before upload) ===== -->
<div class="editor" id="editor" hidden>
<div class="ed-stage"><canvas id="edcanvas"></canvas></div>
@@ -261,12 +289,14 @@ function render(){
${(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>':''}
+ ${x.product_id?'<button class="gal">πΌ Photos</button>':''}
<label class="shoot">${isNew?'π· Create + Go Live':photographed?'π· Update Photo':'π· Add Photo'}<input type="file" accept="image/*" capture="environment"></label>
${(x.done||x.live||x.image||isNew)?'':'<button class="skip">Skip</button>'}
</div>
</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 gal=el.querySelector('.gal'); if(gal) gal.addEventListener('click',()=>openGallery(x));
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()));
@@ -292,6 +322,48 @@ async function shoot(x,file){
openEditor(x,dataUrl);
}
+/* ===== Photo gallery: see every photo on a SKU, keep / remove / feature / add ===== */
+let GAL_X=null;
+async function openGallery(x){
+ if(!x.product_id){ toast('Create the product first (take a photo)'); return; }
+ GAL_X=x;
+ $('#galName').textContent=(x.title||'').replace(/ \| Fentucci$/,'');
+ $('#galSku').textContent=x.dw_sku||'';
+ $('#gallery').hidden=false; document.body.style.overflow='hidden';
+ await renderGallery();
+}
+function closeGallery(){ $('#gallery').hidden=true; document.body.style.overflow=''; GAL_X=null; if(filter!=='any'&&filter!=='shop'&&filter!=='new') load(); }
+async function renderGallery(){
+ const g=$('#galGrid'); g.innerHTML='<div class="gal-empty">Loadingβ¦</div>';
+ try{
+ const r=await fetch('/api/images?pid='+GAL_X.product_id); const d=await r.json();
+ const imgs=d.images||[];
+ if(!imgs.length){ g.innerHTML='<div class="gal-empty">No photos yet β tap π· Add to take one.</div>'; return; }
+ g.innerHTML='';
+ imgs.forEach((im,i)=>{
+ const feat=im.position===1;
+ const t=document.createElement('div'); t.className='gtile'+(feat?' feat':'');
+ t.innerHTML=`<img src="${im.src}" loading="lazy">${feat?'<span class="gfeat-badge">β
FEATURED</span>':''}
+ <div class="gtile-acts">${feat?'':'<button class="g-feat">β
Feature</button>'}<button class="g-del">β Remove</button></div>`;
+ const f=t.querySelector('.g-feat'); if(f) f.addEventListener('click',()=>imgAction('feature',im.id));
+ t.querySelector('.g-del').addEventListener('click',()=>{ if(confirm('Remove this photo?')) imgAction('delete',im.id); });
+ g.appendChild(t);
+ });
+ }catch(e){ g.innerHTML='<div class="gal-empty">Couldn\'t load photos.</div>'; }
+}
+async function imgAction(act,image_id){
+ toast(act==='delete'?'Removingβ¦':'Setting featuredβ¦');
+ try{ const r=await fetch('/api/image/'+act,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({pid:GAL_X.product_id,image_id})});
+ const d=await r.json(); if(d.ok){ toast(act==='delete'?'Removed':'Featured β'); renderGallery(); } else toast('Failed'); }catch(e){ toast('Failed'); }
+}
+$('#galClose').addEventListener('click',closeGallery);
+$('#galAdd').addEventListener('change',e=>{ const f=e.target.files[0]; if(!f||!GAL_X)return;
+ // add a photo to this SKU's gallery (keep existing), via the editor
+ const gx={...GAL_X, keep_images:true, _replaceAll:false, _fromGallery:true};
+ $('#gallery').hidden=true; document.body.style.overflow='';
+ shoot(gx,f);
+});
+
// Send the (possibly edited) photo to the server β local save + Shopify push + activation gate.
async function uploadPhoto(x,dataUrl){
toast((x.needs_create?'Creating ':'Uploading ')+x.dw_sku+'β¦');
@@ -301,6 +373,7 @@ async function uploadPhoto(x,dataUrl){
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();
+ if(x._fromGallery&&x.product_id) setTimeout(()=>openGallery(x),400); // back to the gallery to see the added photo
} else toast('Error: '+(d.err||'failed'));
}catch(e){toast('Upload failed β check connection');}
}
diff --git a/server.js b/server.js
index 7596f47..fecedd9 100644
--- a/server.js
+++ b/server.js
@@ -210,6 +210,32 @@ const server = http.createServer((req, res) => {
return send(res, 404, { err: 'not found' });
}
+ // Photo gallery: list every image on a product (keep/remove/feature from the UI).
+ if (u.pathname === '/api/images' && req.method === 'GET') {
+ const pid = u.searchParams.get('pid');
+ if (!pid) return send(res, 400, { err: 'pid required' });
+ (async () => {
+ const r = await shopifyReq('GET', `/products/${pid}/images.json`);
+ const imgs = ((r.body && r.body.images) || []).map(i => ({ id: i.id, src: i.src, position: i.position }));
+ send(res, 200, { images: imgs });
+ })();
+ return;
+ }
+ if ((u.pathname === '/api/image/delete' || u.pathname === '/api/image/feature') && req.method === 'POST') {
+ let body = ''; req.on('data', c => body += c);
+ req.on('end', async () => {
+ let p; try { p = JSON.parse(body); } catch (e) { return send(res, 400, { err: 'bad json' }); }
+ if (!p.pid || !p.image_id) return send(res, 400, { err: 'pid + image_id required' });
+ if (u.pathname === '/api/image/delete') {
+ const r = await shopifyReq('DELETE', `/products/${p.pid}/images/${p.image_id}.json`);
+ return send(res, 200, { ok: r.status >= 200 && r.status < 300 });
+ }
+ const r = await shopifyReq('PUT', `/products/${p.pid}/images/${p.image_id}.json`, { image: { id: p.image_id, position: 1 } });
+ return send(res, 200, { ok: r.status >= 200 && r.status < 300 });
+ });
+ return;
+ }
+
// Same-origin proxy for a product's CURRENT Shopify image β lets the editor
// load + re-adjust a done item that has no local photo (e.g. shot on another
// device, or a pre-existing image), without a cross-origin canvas taint.
β 62e3b37 auto-save: 2026-06-24T18:28:52 (1 files) β data/queue.json
Β·
back to Dw Photo Capture
Β·
Add collections to search: each product carries its Shopify 418e972 β