← back to Designer Wallcoverings
Add contact-for-price PDP section + additive tagging script for 431 sample-only SKUs
0b2efa543fd1d23de799ddec3c59949c2ab9d5c0 · 2026-06-26 23:52:42 -0700 · Steve
Files touched
A shopify/scripts/tag-contact-for-price.jsA shopify/sections/contact-for-price.liquid
Diff
commit 0b2efa543fd1d23de799ddec3c59949c2ab9d5c0
Author: Steve <steve@designerwallcoverings.com>
Date: Fri Jun 26 23:52:42 2026 -0700
Add contact-for-price PDP section + additive tagging script for 431 sample-only SKUs
---
shopify/scripts/tag-contact-for-price.js | 85 ++++++++++++++
shopify/sections/contact-for-price.liquid | 184 ++++++++++++++++++++++++++++++
2 files changed, 269 insertions(+)
diff --git a/shopify/scripts/tag-contact-for-price.js b/shopify/scripts/tag-contact-for-price.js
new file mode 100644
index 00000000..5c6f664b
--- /dev/null
+++ b/shopify/scripts/tag-contact-for-price.js
@@ -0,0 +1,85 @@
+#!/usr/bin/env node
+/**
+ * tag-contact-for-price.js
+ * Adds the ADDITIVE `contact-for-price` tag to the 431 $4.25-sample-only products
+ * from the 2026-06-26 active-last-week review (PR 91 + Quadrille family 340).
+ * That tag is what makes the contact-for-price.liquid PDP block render — the
+ * "price per unit unavailable online" notice + "Contact Us About This SKU" button.
+ *
+ * EXCLUDES the 3 Fentucci grasscloths (anomalous $32–35 "sample" price — separate fix).
+ * Purely additive — preserves all existing tags, never edits price/variants/status.
+ * Fully reversible (remove the tag to revert).
+ *
+ * SAFE BY DEFAULT — dry-run unless --apply. Ledgered + resumable.
+ * node shopify/scripts/tag-contact-for-price.js # dry-run
+ * node shopify/scripts/tag-contact-for-price.js --apply # GATED: write tags
+ */
+const fs = require('fs');
+const path = require('path');
+
+const APPLY = process.argv.includes('--apply');
+const TSV = process.env.TSV || '/tmp/dw_only_sample_no_product.tsv';
+const EXCLUDE_VENDORS = new Set(['Fentucci']);
+const TAG = 'contact-for-price';
+const LEDGER = path.join(__dirname, 'data', 'tag-contact-for-price.ledger.jsonl');
+
+const env = fs.readFileSync(path.join(__dirname, '..', '.env'), 'utf8');
+const ev = k => (env.match(new RegExp('^' + k + '=(.*)$', 'm')) || [])[1];
+const STORE = ev('SHOPIFY_STORE_DOMAIN') || 'designer-laboratory-sandbox.myshopify.com';
+const TOKEN = ev('SHOPIFY_ADMIN_TOKEN');
+const REST = `https://${STORE}/admin/api/2024-10`;
+
+async function rest(method, p, body) {
+ const r = await fetch(REST + p, {
+ method,
+ headers: { 'Content-Type': 'application/json', 'X-Shopify-Access-Token': TOKEN },
+ body: body ? JSON.stringify(body) : undefined,
+ });
+ return { status: r.status, json: await r.json().catch(() => ({})) };
+}
+
+function loadTargets() {
+ return fs.readFileSync(TSV, 'utf8').trim().split('\n').slice(1).map(l => {
+ const [pid, vendor, , , title] = l.split('\t');
+ return { pid, vendor, title: (title || '').replace(/^"|"$/g, '') };
+ }).filter(t => !EXCLUDE_VENDORS.has(t.vendor));
+}
+
+(async () => {
+ if (!TOKEN) { console.error('No SHOPIFY_ADMIN_TOKEN in .env'); process.exit(1); }
+ fs.mkdirSync(path.dirname(LEDGER), { recursive: true });
+ const done = new Set(
+ fs.existsSync(LEDGER)
+ ? fs.readFileSync(LEDGER, 'utf8').trim().split('\n').filter(Boolean)
+ .map(l => JSON.parse(l)).filter(x => x.result === 'tagged' || x.result === 'already').map(x => x.pid)
+ : []
+ );
+ const targets = loadTargets().filter(t => !done.has(t.pid));
+ const byV = {}; targets.forEach(t => byV[t.vendor] = (byV[t.vendor] || 0) + 1);
+ console.log(`tag-contact-for-price ${APPLY ? '[APPLY]' : '[DRY-RUN]'} targets=${targets.length} (excluded vendors: ${[...EXCLUDE_VENDORS].join(',')})`);
+ console.log('by vendor:', JSON.stringify(byV));
+ if (!APPLY) { console.log(`would add tag "${TAG}" to ${targets.length} products (additive).`); return; }
+
+ const stats = { tagged: 0, already: 0, err: 0 };
+ for (let i = 0; i < targets.length; i++) {
+ const t = targets[i];
+ const { json } = await rest('GET', `/products/${t.pid}.json?fields=id,tags`);
+ const tags = (json.product?.tags || '').split(',').map(s => s.trim()).filter(Boolean);
+ if (tags.includes(TAG)) {
+ stats.already++;
+ fs.appendFileSync(LEDGER, JSON.stringify({ pid: t.pid, result: 'already', ts: new Date().toISOString() }) + '\n');
+ continue;
+ }
+ tags.push(TAG);
+ const r = await rest('PUT', `/products/${t.pid}.json`, { product: { id: Number(t.pid), tags: tags.join(', ') } });
+ if (r.status >= 200 && r.status < 300) {
+ stats.tagged++;
+ fs.appendFileSync(LEDGER, JSON.stringify({ pid: t.pid, result: 'tagged', ts: new Date().toISOString() }) + '\n');
+ if (stats.tagged % 25 === 0) console.log(` [${i + 1}/${targets.length}] tagged ${stats.tagged}`);
+ } else {
+ stats.err++;
+ fs.appendFileSync(LEDGER, JSON.stringify({ pid: t.pid, result: 'error', status: r.status, ts: new Date().toISOString() }) + '\n');
+ }
+ }
+ console.log(`done: tagged=${stats.tagged} already=${stats.already} err=${stats.err}`);
+})();
diff --git a/shopify/sections/contact-for-price.liquid b/shopify/sections/contact-for-price.liquid
new file mode 100644
index 00000000..a837599d
--- /dev/null
+++ b/shopify/sections/contact-for-price.liquid
@@ -0,0 +1,184 @@
+{% comment %}
+ contact-for-price.liquid
+ "Price per unit unavailable online" notice + "Contact Us About This SKU" button.
+ Renders ONLY when the product is tagged `contact-for-price` (additive tag set on
+ the sample-only cohort). Sits ALONGSIDE the existing $4.25 sample variant — the
+ customer can still buy a swatch; this block handles the roll/unit price, which is
+ not shown online (no fabricated price; in-house & to-the-trade lines).
+
+ Generalized from koroseal-quote-button.liquid. Button is PUBLIC (price is hidden
+ from everyone); the trade-discount line stays role-gated. Posts to /api/sku-inquiry.
+{% endcomment %}
+
+{% if product.tags contains 'contact-for-price' %}
+{%- assign base_sku = product.selected_or_first_available_variant.sku | remove: '-Sample' | remove: '-sample' -%}
+<div class="cfp-section" data-product-id="{{ product.id }}" data-product-title="{{ product.title | escape }}" data-sku="{{ base_sku | escape }}">
+ <div class="cfp-badge">
+ <div class="cfp-badge-content">
+ <span class="cfp-icon">●</span>
+ <div class="cfp-badge-text">
+ <p class="cfp-title">Current Item — Price Per Unit Unavailable Online</p>
+ <p class="cfp-subtitle">Order a sample below, or contact us for roll/unit pricing on this SKU.</p>
+ {% if customer and customer.tags contains 'interior_designer' or customer.tags contains 'designer' or customer.tags contains 'design_professional' or customer.tags contains 'trade' %}
+ <p class="cfp-trade">✓ Trade pricing applies — your designer discount will be reflected in the quote.</p>
+ {% endif %}
+ </div>
+ </div>
+ <button class="cfp-button" type="button" onclick="openCfpModal(event)">Contact Us About This SKU</button>
+ </div>
+
+ <div id="cfp-overlay" class="cfp-overlay" onclick="closeCfpModal(event)"></div>
+ <div id="cfp-modal" class="cfp-modal" role="dialog" aria-modal="true" aria-labelledby="cfp-modal-title">
+ <div class="cfp-modal-header">
+ <h2 id="cfp-modal-title">Contact Us About This SKU</h2>
+ <button class="cfp-close" type="button" aria-label="Close" onclick="closeCfpModal()">×</button>
+ </div>
+ <form id="cfp-form" class="cfp-form" onsubmit="handleCfpSubmit(event)">
+ <input type="hidden" id="cfp-product-id" value="{{ product.id }}">
+ <input type="hidden" id="cfp-product-title" value="{{ product.title | escape }}">
+
+ <div class="cfp-skuline">
+ <span class="cfp-skulabel">SKU</span>
+ <span class="cfp-skuval">{{ base_sku | default: product.title | escape }}</span>
+ </div>
+ <input type="hidden" id="cfp-sku" value="{{ base_sku | escape }}">
+
+ <div class="cfp-row">
+ <div class="cfp-group">
+ <label for="cfp-name">Your Name *</label>
+ <input type="text" id="cfp-name" name="name" required autocomplete="name">
+ </div>
+ <div class="cfp-group">
+ <label for="cfp-email">Email *</label>
+ <input type="email" id="cfp-email" name="email" required autocomplete="email" value="{{ customer.email }}">
+ </div>
+ </div>
+
+ <div class="cfp-row">
+ <div class="cfp-group">
+ <label for="cfp-phone">Phone</label>
+ <input type="tel" id="cfp-phone" name="phone" autocomplete="tel">
+ </div>
+ <div class="cfp-group">
+ <label for="cfp-company">Company / Business</label>
+ <input type="text" id="cfp-company" name="company" autocomplete="organization">
+ </div>
+ </div>
+
+ <div class="cfp-row">
+ <div class="cfp-group">
+ <label for="cfp-role">I am a *</label>
+ <select id="cfp-role" name="role" required>
+ <option value="">Select…</option>
+ <option value="interior_designer">Interior Designer</option>
+ <option value="trade">Trade / Dealer</option>
+ <option value="architect">Architect / Specifier</option>
+ <option value="homeowner">Homeowner</option>
+ <option value="other">Other</option>
+ </select>
+ </div>
+ <div class="cfp-group">
+ <label for="cfp-qty">Quantity Needed</label>
+ <div class="cfp-qtywrap">
+ <input type="number" id="cfp-qty" name="quantity" min="1" placeholder="e.g., 6">
+ <select id="cfp-unit" name="unit" aria-label="Unit">
+ <option value="rolls">rolls</option>
+ <option value="yards">yards</option>
+ <option value="sq_ft">sq ft</option>
+ <option value="unsure">unsure</option>
+ </select>
+ </div>
+ </div>
+ </div>
+
+ <div class="cfp-row">
+ <div class="cfp-group">
+ <label for="cfp-project">Project Name</label>
+ <input type="text" id="cfp-project" name="projectName" placeholder="e.g., Madison Ave Residence">
+ </div>
+ <div class="cfp-group">
+ <label for="cfp-city">City / Location</label>
+ <input type="text" id="cfp-city" name="city" placeholder="e.g., Los Angeles, CA">
+ </div>
+ </div>
+
+ <div class="cfp-row">
+ <div class="cfp-group">
+ <label for="cfp-timeline">Timeline</label>
+ <input type="text" id="cfp-timeline" name="timeline" placeholder="e.g., 2–3 weeks">
+ </div>
+ <div class="cfp-group">
+ <label for="cfp-budget">Budget</label>
+ <input type="text" id="cfp-budget" name="budget" placeholder="e.g., $10,000">
+ </div>
+ </div>
+
+ <div class="cfp-group">
+ <label for="cfp-notes">Additional Notes</label>
+ <textarea id="cfp-notes" name="notes" rows="3" placeholder="Anything else we should know about your project…"></textarea>
+ </div>
+
+ <button type="submit" class="cfp-submit">Send Inquiry</button>
+ <p class="cfp-reassure">We typically reply within 1 business day. No obligation.</p>
+ </form>
+ </div>
+</div>
+
+<style>
+ .cfp-section{margin:1.5rem 0;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif}
+ .cfp-badge{display:flex;align-items:center;gap:1.25rem;padding:1.1rem 1.25rem;background:#faf8f4;border:1px solid #e6e0d6;border-radius:8px;flex-wrap:wrap}
+ .cfp-badge-content{display:flex;align-items:flex-start;gap:.75rem;flex:1;min-width:220px}
+ .cfp-icon{color:#b9892f;font-size:.9rem;line-height:1.6}
+ .cfp-title{margin:0;font-weight:600;font-size:.98rem;color:#2a2622;letter-spacing:.01em}
+ .cfp-subtitle{margin:.25rem 0 0;font-size:.85rem;color:#6b6357}
+ .cfp-trade{margin:.35rem 0 0;font-size:.8rem;color:#3f7d3f;font-weight:500}
+ .cfp-button{padding:.7rem 1.4rem;background:#2a2622;color:#fff;border:none;border-radius:6px;font-weight:500;cursor:pointer;white-space:nowrap;transition:all .2s}
+ .cfp-button:hover{background:#000;transform:translateY(-1px)}
+ .cfp-overlay{display:none;position:fixed;inset:0;background:rgba(0,0,0,.5);z-index:999}
+ .cfp-overlay.active{display:block}
+ .cfp-modal{display:none;position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);background:#fff;border-radius:12px;box-shadow:0 10px 40px rgba(0,0,0,.2);max-width:560px;width:92%;max-height:92vh;overflow-y:auto;z-index:1000}
+ .cfp-modal.active{display:block}
+ .cfp-modal-header{display:flex;justify-content:space-between;align-items:center;padding:1.25rem 1.5rem;border-bottom:1px solid #eee}
+ .cfp-modal-header h2{margin:0;font-size:1.3rem;color:#2a2622}
+ .cfp-close{background:none;border:none;font-size:1.6rem;cursor:pointer;color:#888;line-height:1}
+ .cfp-form{padding:1.25rem 1.5rem}
+ .cfp-skuline{display:flex;align-items:center;gap:.6rem;margin-bottom:1rem;padding:.5rem .75rem;background:#f6f4f0;border-radius:6px}
+ .cfp-skulabel{font-size:.72rem;letter-spacing:.08em;text-transform:uppercase;color:#8a8276}
+ .cfp-skuval{font-weight:600;color:#2a2622}
+ .cfp-row{display:grid;grid-template-columns:1fr 1fr;gap:1rem}
+ .cfp-group{margin-bottom:1rem}
+ .cfp-group label{display:block;margin-bottom:.4rem;font-weight:500;color:#3a352f;font-size:.9rem}
+ .cfp-group input,.cfp-group select,.cfp-group textarea{width:100%;padding:.65rem;border:1px solid #d6d0c6;border-radius:6px;font-family:inherit;font-size:.92rem;box-sizing:border-box}
+ .cfp-group input:focus,.cfp-group select:focus,.cfp-group textarea:focus{outline:none;border-color:#b9892f;box-shadow:0 0 0 3px rgba(185,137,47,.12)}
+ .cfp-qtywrap{display:flex;gap:.5rem}
+ .cfp-qtywrap input{flex:1}
+ .cfp-qtywrap select{width:auto}
+ .cfp-submit{width:100%;padding:.85rem;background:#b9892f;color:#fff;border:none;border-radius:6px;font-weight:600;font-size:.95rem;cursor:pointer;transition:background .2s}
+ .cfp-submit:hover{background:#a3781f}
+ .cfp-reassure{text-align:center;font-size:.78rem;color:#8a8276;margin:.6rem 0 0}
+ @media(max-width:640px){.cfp-badge{flex-direction:column;align-items:flex-start}.cfp-row{grid-template-columns:1fr}.cfp-modal{width:95%}}
+</style>
+
+<script>
+ function openCfpModal(e){if(e)e.preventDefault();document.getElementById('cfp-overlay').classList.add('active');document.getElementById('cfp-modal').classList.add('active');}
+ function closeCfpModal(e){if(e&&e.target&&e.target!==document.getElementById('cfp-overlay'))return;document.getElementById('cfp-overlay').classList.remove('active');document.getElementById('cfp-modal').classList.remove('active');}
+ document.addEventListener('keydown',function(e){if(e.key==='Escape')closeCfpModal();});
+ async function handleCfpSubmit(e){
+ e.preventDefault();
+ var val=function(id){var el=document.getElementById(id);return el?el.value:'';};
+ var payload={
+ productId:val('cfp-product-id'), productTitle:val('cfp-product-title'), sku:val('cfp-sku'),
+ name:val('cfp-name'), email:val('cfp-email'), phone:val('cfp-phone'), company:val('cfp-company'),
+ role:val('cfp-role'), quantity:val('cfp-qty'), unit:val('cfp-unit'),
+ projectName:val('cfp-project'), city:val('cfp-city'), timeline:val('cfp-timeline'),
+ budget:val('cfp-budget'), notes:val('cfp-notes')
+ };
+ try{
+ var r=await fetch('/api/sku-inquiry',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(payload)});
+ var j=await r.json();
+ if(j.success){alert('✅ Inquiry received!\n\nWe\'ll get back to you within 1 business day.\n\nReference: '+(j.requestId||'')); document.getElementById('cfp-form').reset(); closeCfpModal();}
+ else{alert('❌ '+(j.message||'Something went wrong. Please try again.'));}
+ }catch(err){console.error('cfp submit',err);alert('❌ Could not send inquiry. Please email info@designerwallcoverings.com.');}
+ }
+</script>
+{% endif %}
← fb914813 Add dry-run-first fix scripts for active-last-week 2-variant
·
back to Designer Wallcoverings
·
auto-save: 2026-06-27T00:06:21 (7 files) — pending-approval/ d497aec8 →