[object Object]

← back to Estimate Instant

shopify: add Buy N rolls CTA + TEST banner to calculator UI

646c3eba8ac57d50d95028e32311ce4b9ea52f20 · 2026-07-16 07:39:05 -0700 · Steve Abrams

- Email capture still first (POST /api/lead with created_at — unchanged)
- After estimate: green "Buy N rolls on Designer Wallcoverings" button appears
  linking to https://www.designerwallcoverings.com/cart/<variant_id>:<N>
  where N = rolls-needed from the estimate math
- Real per-roll price and total shown in button pills
- TEST-MODE banner below CTA: "hands off to LIVE Shopify checkout for N rolls;
  PayPal shows when enabled; roll specs are sample; production reads metafields"
- Stand-in note shown when shopify_match=false (all 8 current entries)
- Embed iframe compatibility preserved (postHeight still fires)
- All existing styles/behavior unchanged; only result block extended

Files touched

Diff

commit 646c3eba8ac57d50d95028e32311ce4b9ea52f20
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Thu Jul 16 07:39:05 2026 -0700

    shopify: add Buy N rolls CTA + TEST banner to calculator UI
    
    - Email capture still first (POST /api/lead with created_at — unchanged)
    - After estimate: green "Buy N rolls on Designer Wallcoverings" button appears
      linking to https://www.designerwallcoverings.com/cart/<variant_id>:<N>
      where N = rolls-needed from the estimate math
    - Real per-roll price and total shown in button pills
    - TEST-MODE banner below CTA: "hands off to LIVE Shopify checkout for N rolls;
      PayPal shows when enabled; roll specs are sample; production reads metafields"
    - Stand-in note shown when shopify_match=false (all 8 current entries)
    - Embed iframe compatibility preserved (postHeight still fires)
    - All existing styles/behavior unchanged; only result block extended
---
 public/index.html | 62 ++++++++++++++++++++++++++++++++++++++++++++++++++++++-
 1 file changed, 61 insertions(+), 1 deletion(-)

diff --git a/public/index.html b/public/index.html
index cbc0961..9dbbb45 100644
--- a/public/index.html
+++ b/public/index.html
@@ -2,7 +2,7 @@
 <meta name="viewport" content="width=device-width,initial-scale=1">
 <title>How Many Rolls? — Instant Wallpaper Estimate</title>
 <style>
-  :root{--ink:#1a1a1a;--line:#e6e6e6;--accent:#2a3a4a;--gold:#a98b4a;--bg:#f7f5f2}
+  :root{--ink:#1a1a1a;--line:#e6e6e6;--accent:#2a3a4a;--gold:#a98b4a;--bg:#f7f5f2;--green:#1e6e3a}
   *{box-sizing:border-box}
   body{margin:0;font:16px/1.5 -apple-system,Segoe UI,Roboto,sans-serif;color:var(--ink);background:var(--bg)}
   .wrap{max-width:640px;margin:0 auto;padding:32px 20px 60px}
@@ -29,11 +29,30 @@
   .detail{margin:16px 0 0;font-size:14px;color:#555;display:grid;grid-template-columns:1fr 1fr;gap:6px 18px}
   .detail b{color:var(--ink);font-weight:600}
   .errs{margin-top:16px;background:#fff4f4;border:1px solid #f3c9c9;color:#9a2a2a;border-radius:8px;padding:12px 14px;font-size:14px}
+
+  /* Email capture — first step */
   .capture{margin-top:20px;background:var(--bg);border:1px dashed #d0cabf;border-radius:10px;padding:16px}
   .capture .row2{display:flex;gap:10px;flex-wrap:wrap;margin-top:8px}
   .capture input{flex:1;min-width:180px}
   .capture button{font:inherit;font-weight:600;padding:11px 16px;border:0;border-radius:8px;background:var(--gold);color:#fff;cursor:pointer}
   .done{color:#2a6a3a;font-weight:600;font-size:14px;margin-top:8px}
+
+  /* Shopify checkout CTA — second step, shown after email capture or immediately */
+  .checkout-block{margin-top:18px;border-top:1px solid var(--line);padding-top:18px}
+  .btn-buy{display:flex;align-items:center;justify-content:center;gap:10px;width:100%;
+    font:inherit;font-weight:700;font-size:16px;padding:15px 20px;
+    border:0;border-radius:9px;background:var(--green);color:#fff;
+    cursor:pointer;text-decoration:none;transition:background .15s}
+  .btn-buy:hover{background:#155730}
+  .btn-buy .qty-pill{background:rgba(255,255,255,.22);border-radius:5px;padding:1px 8px;font-size:14px;font-weight:800}
+  .btn-buy .price-pill{background:rgba(255,255,255,.16);border-radius:5px;padding:1px 8px;font-size:14px}
+
+  /* TEST-MODE banner — hard requirement per spec */
+  .test-banner{margin-top:10px;background:#fff8e6;border:1px solid #e8d080;border-radius:8px;
+    padding:9px 13px;font-size:12px;color:#7a5800;line-height:1.5}
+  .test-banner b{font-weight:700}
+  .standin-note{margin-top:6px;font-size:11px;color:#999;font-style:italic}
+
   .foot{margin-top:20px;font-size:12px;color:#aaa;text-align:center}
   .foot a{color:#888}
 </style></head><body>
@@ -62,6 +81,8 @@
         <div class="price" id="pr">$0</div>
       </div>
       <div class="detail" id="detail"></div>
+
+      <!-- Step 1: Email capture (always first) -->
       <div class="capture">
         <label>Email me this quote</label>
         <div class="row2">
@@ -70,6 +91,26 @@
         </div>
         <div class="done" id="done" style="display:none">✓ Saved — a DW rep will follow up with your quote and can help you order.</div>
       </div>
+
+      <!-- Step 2: Shopify checkout handoff -->
+      <div class="checkout-block" id="checkoutBlock" style="display:none">
+        <a id="btnBuy" class="btn-buy" href="#" target="_blank" rel="noopener noreferrer">
+          Buy <span id="buyQty" class="qty-pill">N rolls</span> on Designer Wallcoverings
+          <span id="buyPrice" class="price-pill">$0</span>
+        </a>
+        <!-- TEST-MODE banner — required per spec -->
+        <div class="test-banner">
+          <b>TEST</b> — this button hands off to the <b>live Shopify checkout</b> for exactly
+          <span id="bannerQty">N</span> rolls at <span id="bannerPrice">$0/roll</span> each.
+          PayPal appears at checkout when enabled in Shopify Payments.
+          Roll specs (width/length/repeat) are sample data; production reads these from
+          Shopify metafields keyed by <code>variant_id</code>.
+        </div>
+        <div class="standin-note" id="standinNote" style="display:none">
+          Stand-in variant: the selected pattern's prototype SKU is not yet in the live catalog.
+          A real DW variant is used for the checkout link so the flow works end-to-end.
+        </div>
+      </div>
     </div>
   </div>
   <p class="foot">Estimate only — includes a standard 4" trim + pattern-repeat allowance and rounds up for a safe count. Prices are indicative; confirm quantity, price, and dye lot with your DW rep before ordering.</p>
@@ -85,8 +126,26 @@ async function loadRolls(){
 $('units').addEventListener('click',e=>{ const b=e.target.closest('button'); if(!b)return;
   [...$('units').children].forEach(x=>x.classList.remove('on')); b.classList.add('on');
   unit=b.dataset.u; $('uw').textContent=$('uh').textContent=`(${unit})`; });
+
+function renderCheckout(r) {
+  // r = estimate response with shopify fields
+  if (!r || !r.cart_url) { $('checkoutBlock').style.display='none'; return; }
+  $('checkoutBlock').style.display='block';
+  const qty = r.rollsNeeded;
+  const total = r.price;
+  const perRoll = r.pricePerRoll;
+  $('btnBuy').href = r.cart_url;
+  $('buyQty').textContent = qty + ' roll' + (qty===1?'':'s');
+  $('buyPrice').textContent = '$' + total.toLocaleString();
+  $('bannerQty').textContent = qty;
+  $('bannerPrice').textContent = '$' + perRoll.toLocaleString(undefined,{minimumFractionDigits:2,maximumFractionDigits:2}) + '/roll';
+  // Show stand-in note if this is not an exact SKU match
+  $('standinNote').style.display = r.shopify_match === false ? 'block' : 'none';
+}
+
 async function calc(){
   $('errs').style.display='none'; $('result').classList.remove('show');
+  $('checkoutBlock').style.display='none';
   const payload={ sku:$('pat').value, wallWidthIn:inches($('w').value), wallHeightIn:inches($('h').value) };
   const r=await fetch('/api/estimate',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(payload)}).then(r=>r.json());
   if(!r.ok){ $('errs').innerHTML=(r.errors||['Something went wrong.']).join('<br>'); $('errs').style.display='block'; return; }
@@ -99,6 +158,7 @@ async function calc(){
     ['Price per roll','$'+r.pricePerRoll],['Est. waste',r.wastePct+'%']
   ].map(([k,v])=>`<div>${k}: <b>${v}</b></div>`).join('');
   $('result').classList.add('show'); $('done').style.display='none';
+  renderCheckout(r);
   postHeight();
 }
 // When embedded via embed.js, report our content height so the host iframe can grow.

← 4349f95 shopify: wire server to static catalog — estimate returns ca  ·  back to Estimate Instant  ·  security: bind server to 127.0.0.1 so the tunnel/proxy basic b38a6f6 →