[object Object]

← back to Dw Collections Viewer

bulk product remove-from-list (server endpoint + UI button)

3f6faec63a142cbdf49d30c430c798861683c4e9 · 2026-05-06 13:03:43 -0700 · SteveStudio2

Files touched

Diff

commit 3f6faec63a142cbdf49d30c430c798861683c4e9
Author: SteveStudio2 <stevestudio2@SteveStacStudio.lan>
Date:   Wed May 6 13:03:43 2026 -0700

    bulk product remove-from-list (server endpoint + UI button)
---
 public/index.html | 17 +++++++++++++++++
 server.js         | 17 +++++++++++++++++
 2 files changed, 34 insertions(+)

diff --git a/public/index.html b/public/index.html
index 5794923..0683a6b 100644
--- a/public/index.html
+++ b/public/index.html
@@ -451,6 +451,7 @@ function renderProductsSlider() {
       <div class="actions-mini">
         ${selCount ? `<button onclick="clearProductSelection()">Clear (${selCount})</button>` : ''}
         ${selCount ? `<button onclick="copySelectedSkus()">Copy SKUs</button>` : ''}
+        ${selCount ? `<button onclick="removeSelectedFromList()">Remove (${selCount})</button>` : ''}
         <button onclick="selectAllProducts()">Select all</button>
       </div>
     </div>
@@ -472,6 +473,22 @@ function copySelectedSkus() {
   const skus = Array.from(PRODUCT_SELECTED).join('\n');
   navigator.clipboard.writeText(skus).then(() => toast(`copied ${PRODUCT_SELECTED.size} SKUs to clipboard`));
 }
+async function removeSelectedFromList() {
+  const slug = CURRENT;
+  const skus = Array.from(PRODUCT_SELECTED);
+  if (!skus.length) return;
+  if (!confirm(`Remove ${skus.length} product(s) from ${slug}'s local products.json? Live site is unchanged until you redeploy.`)) return;
+  toast(`removing ${skus.length} from ${slug}…`);
+  const r = await fetch(`api/site/${slug}/products/remove`, {
+    method: 'POST', headers: { 'Content-Type': 'application/json' },
+    body: JSON.stringify({ skus }),
+  });
+  const j = await r.json();
+  if (!r.ok) return toast('remove failed: ' + (j.error || r.status), false);
+  toast(`removed ${j.removed} · ${j.before} → ${j.after} (not deployed)`, true, j.removed > 0 ? 'down' : null);
+  PRODUCT_SELECTED.clear();
+  await fetchProducts(slug);
+}
 
 function renderDetail() {
   const s = SITES[CURRENT];
diff --git a/server.js b/server.js
index 980e43b..30f6c8d 100644
--- a/server.js
+++ b/server.js
@@ -416,6 +416,23 @@ app.get('/api/site/:slug/products', (req, res) => {
   res.json({ slug, count: products.length, products: indexed.map(x => x.p) });
 });
 
+// Remove SKUs from a site's local products.json (does NOT touch Shopify).
+// Body: { skus: ["SKU1", ...] }. Persists filtered products.json to disk.
+app.post('/api/site/:slug/products/remove', (req, res) => {
+  const { slug } = req.params;
+  if (!validSlug(slug)) return res.status(400).json({ error: 'invalid slug' });
+  const skus = Array.isArray(req.body.skus) ? req.body.skus.filter(s => typeof s === 'string' && s) : [];
+  if (!skus.length) return res.status(400).json({ error: 'skus[] required' });
+  const fp = path.join(HOME, 'Projects', slug, 'data', 'products.json');
+  const products = readJsonSafe(fp) || [];
+  const before = products.length;
+  const skuSet = new Set(skus);
+  const filtered = products.filter(p => !skuSet.has(String(p.sku || p.handle || '')));
+  const removed = before - filtered.length;
+  if (removed > 0) fs.writeFileSync(fp, JSON.stringify(filtered, null, 2));
+  res.json({ slug, removed, before, after: filtered.length });
+});
+
 // ─── All-products view + bulk delete + bulk add-to-collection ───────────
 // Read uses public /products.json (no token). Writes need Admin token in env.
 const SHOPIFY_STORE = process.env.SHOPIFY_STORE || 'designerwallcoverings.com';

← 8a47889 press C anywhere to focus collection search input  ·  back to Dw Collections Viewer  ·  Click site → show all products as a responsive grid (was hor f016e8f →