← back to Estimate Instant

public/index.html

185 lines

<!doctype html><html lang="en"><head><meta charset="utf-8">
<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;--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}
  .kicker{font-size:12px;letter-spacing:.18em;text-transform:uppercase;color:var(--gold);font-weight:600;margin:0 0 6px}
  h1{font-size:30px;line-height:1.15;margin:0 0 6px;font-weight:700}
  .sub{color:#666;margin:0 0 26px;font-size:15px}
  .card{background:#fff;border:1px solid var(--line);border-radius:12px;padding:22px}
  label{display:block;font-size:12px;letter-spacing:.04em;text-transform:uppercase;color:#888;font-weight:600;margin:0 0 6px}
  .row{display:flex;gap:14px;flex-wrap:wrap}
  .fld{flex:1;min-width:150px;margin-bottom:16px}
  input,select{width:100%;font:inherit;padding:11px 12px;border:1px solid #d8d4cd;border-radius:8px;background:#fff}
  input:focus,select:focus{outline:none;border-color:var(--accent)}
  .units{display:flex;gap:8px;margin-bottom:16px}
  .units button{flex:1;font:inherit;font-size:13px;padding:8px;border:1px solid #d8d4cd;background:#fff;border-radius:8px;cursor:pointer;color:#666}
  .units button.on{background:var(--accent);color:#fff;border-color:var(--accent)}
  .go{width:100%;font:inherit;font-weight:700;font-size:16px;padding:14px;border:0;border-radius:9px;background:var(--accent);color:#fff;cursor:pointer;margin-top:4px}
  .go:hover{background:#1d2a36}
  .result{margin-top:22px;border-top:1px solid var(--line);padding-top:22px;display:none}
  .result.show{display:block}
  .big{display:flex;align-items:baseline;gap:12px;flex-wrap:wrap}
  .rolls{font-size:44px;font-weight:800;line-height:1;color:var(--accent)}
  .rolls small{font-size:16px;font-weight:600;color:#888}
  .price{font-size:22px;font-weight:700;margin-left:auto}
  .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}

  .hint{margin:-6px 0 16px;font-size:12px;color:#8a8578;line-height:1.45}
  .hint b{color:#6a6558;font-weight:600}
  .foot{margin-top:20px;font-size:12px;color:#aaa;text-align:center}
  .foot a{color:#888}
</style></head><body>
<div class="wrap">
  <p class="kicker">Designer Wallcoverings</p>
  <h1>How many rolls do I need?</h1>
  <p class="sub">Enter your wall, pick a pattern — get the exact roll count (with pattern-match waste) and an instant price.</p>

  <div class="card">
    <div class="units" id="units">
      <button data-u="ft" class="on">Feet</button>
      <button data-u="in">Inches</button>
    </div>
    <div class="row">
      <div class="fld"><label>Wall width <span id="uw">(ft)</span></label><input id="w" type="number" min="0" step="0.1" placeholder="e.g. 12"></div>
      <div class="fld"><label>Wall height <span id="uh">(ft)</span></label><input id="h" type="number" min="0" step="0.1" placeholder="e.g. 9"></div>
    </div>
    <p class="hint">Wall width = the <b>total width of every wall</b> you're covering — add them up. Height is floor-to-ceiling.</p>
    <div class="fld"><label>Pattern</label><select id="pat"></select></div>
    <button class="go" id="go">Calculate rolls &amp; price</button>

    <div class="errs" id="errs" style="display:none"></div>

    <div class="result" id="result">
      <div class="big">
        <div class="rolls"><span id="rn">0</span> <small>rolls</small></div>
        <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">
          <input id="email" type="email" placeholder="you@email.com">
          <button id="send">Send quote</button>
        </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>
</div>
<script>
let unit='ft', rolls=[], last=null;
const $=id=>document.getElementById(id);
function inches(v){ return unit==='ft' ? Number(v)*12 : Number(v); }
async function loadRolls(){
  const r=await fetch('/api/rolls').then(r=>r.json()); rolls=r.rolls||[];
  $('pat').innerHTML=rolls.map(x=>`<option value="${x.sku}">${x.pattern} — ${x.roll_width_in}" wide${x.pattern_repeat_in?`, ${x.pattern_repeat_in}" repeat`:', random match'}</option>`).join('');
}
$('units').addEventListener('click',e=>{ const b=e.target.closest('button'); if(!b)return;
  const nu=b.dataset.u; if(nu===unit) return;
  // Convert any entered values in place so toggling units never silently re-scales the number.
  const conv = nu==='in' ? 12 : 1/12;
  ['w','h'].forEach(id=>{ const el=$(id); const v=parseFloat(el.value); if(!isNaN(v)&&v>0) el.value=+(v*conv).toFixed(2); });
  [...$('units').children].forEach(x=>x.classList.remove('on')); b.classList.add('on');
  unit=nu; $('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; }
  last=r;
  $('rn').textContent=r.rollsNeeded; $('pr').textContent='$'+r.price.toLocaleString();
  $('detail').innerHTML=[
    ['Pattern',r.pattern],['SKU',r.sku],
    ['Strips needed',r.totalStrips],['Strips per roll',r.stripsPerRoll],
    ['Cut length / strip',r.cutLengthIn+'"'],['Match',r.match],
    ['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.
// embed.js passes the host origin as ?host=… — target it explicitly, never broadcast to '*'.
const EMBED_HOST=new URLSearchParams(location.search).get('host')||'';
function postHeight(){ if(parent!==window && EMBED_HOST){ try{ parent.postMessage({dwEstimateHeight:document.body.scrollHeight+24},EMBED_HOST); }catch(e){} } }
window.addEventListener('load',postHeight);
$('go').addEventListener('click',calc);
$('send').addEventListener('click',async()=>{
  const email=$('email').value.trim();
  const r=await fetch('/api/lead',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({email,quote:last})}).then(r=>r.json());
  if(r.ok){ $('done').style.display='block'; $('email').value=''; } else { alert(r.error||'Enter a valid email.'); }
});
loadRolls();
</script>
</body></html>