← 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 & 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>