← back to Koroseal Quote Only
snippets/dw-quote-only-cta.liquid
286 lines
{% comment %}
dw-quote-only-cta.liquid — Designer Wallcoverings "Request a Quote" CTA
---------------------------------------------------------------------------
TAG-DRIVEN quote-only product treatment. Renders ONLY when the product
carries the trigger tag `quotes` (see TRIGGER below). For a quote-only
product it:
1. HIDES the yardage/roll price + the standard "Add to cart" (the theme
template must gate its price+buy-buttons block on the same tag — see
the WIRING note at the bottom).
2. Shows a "Request a Quote" CTA that opens a modal quote form.
3. KEEPS the ${{ 4.25 }} Sample variant orderable as a swatch: the
"Order a Sample — $4.25" button selects the Sample variant and adds it
to cart via the storefront cart AJAX endpoint, so a shopper can still
buy the swatch even though the material itself is quote-only.
Backend: posts the quote request to the SAME George-backed endpoint the
commercial-quote skill uses (Koroseal). Endpoint is generalized to
/api/quote-request so any quote-only line can share it; it emails
info@designerwallcoverings.com via George with a dealer-discount badge.
TRIGGER TAG: `quotes`
Chosen over `quote_only` / `Contact for Price` because the live
dw-five-field-canary already treats `%,quotes,%` as the exemption tag for
"sample-only + no sellable variant" products (auditor.mjs L51). Using
`quotes` means the canary FAIL this line will throw auto-resolves the
moment the tag lands — no canary code change required. A human-readable
companion tag `Quote Only` is also applied for admin/theme clarity, but
the machine trigger is `quotes`.
Render with: {% render 'dw-quote-only-cta' %}
(place inside the product template / product-form section, product in scope)
{% endcomment %}
{%- assign _is_quote_only = false -%}
{%- if product.tags contains 'quotes' -%}
{%- assign _is_quote_only = true -%}
{%- endif -%}
{%- if _is_quote_only -%}
{%- comment -%} Find the Sample variant so we can keep the swatch orderable. {%- endcomment -%}
{%- assign _sample_variant = null -%}
{%- for v in product.variants -%}
{%- if v.sku contains '-Sample' or v.title == 'Sample' -%}
{%- assign _sample_variant = v -%}
{%- break -%}
{%- endif -%}
{%- endfor -%}
<div class="dw-quote-section"
data-quote-line="{{ product.vendor | escape }}"
data-product-id="{{ product.id }}"
data-product-title="{{ product.title | escape }}"
data-product-handle="{{ product.handle }}">
<div class="dw-quote-badge">
<span class="dw-quote-badge-icon" aria-hidden="true">✓</span>
<div class="dw-quote-badge-text">
<p class="dw-quote-badge-title">Active Pattern — Sold by the Yard</p>
<p class="dw-quote-badge-sub">54" wide · Yardage pricing by quote</p>
</div>
</div>
<div class="dw-quote-actions">
<button type="button" class="dw-quote-button" onclick="dwOpenQuoteModal(event)">
Request a Quote
</button>
{%- if _sample_variant -%}
<button type="button"
class="dw-sample-button"
data-variant-id="{{ _sample_variant.id }}"
onclick="dwAddSampleToCart(event)">
Order a Sample — {{ _sample_variant.price | money }}
</button>
{%- endif -%}
</div>
<!-- Modal -->
<div id="dw-quote-overlay" class="dw-quote-overlay" onclick="dwCloseQuoteModal(event)"></div>
<div id="dw-quote-modal" class="dw-quote-modal" role="dialog" aria-modal="true" aria-labelledby="dw-quote-modal-title">
<div class="dw-quote-modal-header">
<h2 id="dw-quote-modal-title">Request a Quote</h2>
<button type="button" class="dw-quote-modal-close" aria-label="Close" onclick="dwCloseQuoteModal()">×</button>
</div>
<form id="dw-quote-form" class="dw-quote-form" onsubmit="dwHandleQuoteSubmit(event)">
<input type="hidden" id="dw-q-product-id" value="{{ product.id }}">
<input type="hidden" id="dw-q-product-title" value="{{ product.title | escape }}">
<input type="hidden" id="dw-q-product-url" value="{{ shop.url }}{{ product.url }}">
{%- if customer -%}
<input type="hidden" id="dw-q-customer-email" value="{{ customer.email }}">
{%- else -%}
<input type="hidden" id="dw-q-customer-email" value="">
{%- endif -%}
{%- unless customer -%}
<div class="dw-form-group">
<label for="dw-q-name">Your Name *</label>
<input type="text" id="dw-q-name" name="name" required autocomplete="name" placeholder="Jane Designer">
</div>
<div class="dw-form-group">
<label for="dw-q-email">Email *</label>
<input type="email" id="dw-q-email" name="email" required autocomplete="email" placeholder="jane@studio.com">
</div>
{%- endunless -%}
<div class="dw-form-group">
<label for="dw-q-project">Project Name *</label>
<input type="text" id="dw-q-project" name="projectName" required placeholder="e.g., Beverly Hills Residence">
</div>
<div class="dw-form-group">
<label for="dw-q-city">City / Location *</label>
<input type="text" id="dw-q-city" name="city" required placeholder="e.g., Los Angeles, CA">
</div>
<div class="dw-form-group">
<label for="dw-q-yards">Yards Required *</label>
<input type="number" id="dw-q-yards" name="yards" required min="1" step="1" placeholder="e.g., 40">
</div>
<div class="dw-form-row">
<div class="dw-form-group">
<label for="dw-q-timeline">Timeline</label>
<input type="text" id="dw-q-timeline" name="timeline" placeholder="e.g., 2–3 weeks">
</div>
<div class="dw-form-group">
<label for="dw-q-budget">Budget</label>
<input type="text" id="dw-q-budget" name="budget" placeholder="e.g., $8,000">
</div>
</div>
<div class="dw-form-group">
<label for="dw-q-notes">Additional Notes</label>
<textarea id="dw-q-notes" name="notes" rows="4" placeholder="Anything else we should know?"></textarea>
</div>
<button type="submit" class="dw-form-submit">Submit Quote Request</button>
<p class="dw-quote-fineprint">We reply within one business day. A $4.25 sample swatch is available above.</p>
</form>
</div>
</div>
<style>
.dw-quote-section { margin: 1.5rem 0; font-family: inherit; }
.dw-quote-badge { display:flex; align-items:center; gap:.9rem; padding:1rem 1.15rem;
background:linear-gradient(135deg,#f7f5f2 0%,#efe9e2 100%); border:1px solid #e3dbcf; border-radius:8px; margin-bottom:1rem; }
.dw-quote-badge-icon { font-size:1.5rem; color:#8a6d3b; font-weight:700; line-height:1; }
.dw-quote-badge-text p { margin:0; line-height:1.3; }
.dw-quote-badge-title { font-weight:600; font-size:.95rem; color:#2b2b2b; }
.dw-quote-badge-sub { font-size:.83rem; color:#7a7268; }
.dw-quote-actions { display:flex; flex-wrap:wrap; gap:.75rem; }
.dw-quote-button { padding:.8rem 1.6rem; background:#1f2937; color:#fff; border:none; border-radius:6px;
font-weight:600; font-size:.95rem; cursor:pointer; transition:background .2s,transform .15s; }
.dw-quote-button:hover { background:#111827; transform:translateY(-1px); }
.dw-sample-button { padding:.8rem 1.4rem; background:#fff; color:#1f2937; border:1px solid #cbb89a; border-radius:6px;
font-weight:600; font-size:.95rem; cursor:pointer; transition:border-color .2s,background .2s; }
.dw-sample-button:hover { background:#faf7f2; border-color:#8a6d3b; }
.dw-sample-button[disabled] { opacity:.6; cursor:default; }
.dw-quote-overlay { display:none; position:fixed; inset:0; background:rgba(0,0,0,.5); z-index:9998; }
.dw-quote-overlay.active { display:block; }
.dw-quote-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,.25); width:90%; max-width:520px;
max-height:90vh; overflow-y:auto; z-index:9999; }
.dw-quote-modal.active { display:block; }
.dw-quote-modal-header { display:flex; justify-content:space-between; align-items:center; padding:1.25rem 1.5rem; border-bottom:1px solid #eee; }
.dw-quote-modal-header h2 { margin:0; font-size:1.35rem; color:#1f2937; }
.dw-quote-modal-close { background:none; border:none; font-size:1.6rem; cursor:pointer; color:#888; line-height:1; }
.dw-quote-modal-close:hover { color:#1f2937; }
.dw-quote-form { padding:1.5rem; }
.dw-form-group { margin-bottom:1.1rem; }
.dw-form-row { display:grid; grid-template-columns:1fr 1fr; gap:1rem; }
.dw-form-group label { display:block; margin-bottom:.4rem; font-weight:500; color:#374151; font-size:.9rem; }
.dw-form-group input, .dw-form-group textarea { width:100%; padding:.7rem; border:1px solid #d1d5db; border-radius:6px;
font-family:inherit; font-size:.95rem; box-sizing:border-box; }
.dw-form-group input:focus, .dw-form-group textarea:focus { outline:none; border-color:#8a6d3b; box-shadow:0 0 0 3px rgba(138,109,59,.12); }
.dw-form-submit { width:100%; padding:.9rem; background:#8a6d3b; color:#fff; border:none; border-radius:6px;
font-weight:600; font-size:.95rem; cursor:pointer; transition:background .2s; }
.dw-form-submit:hover { background:#6f5730; }
.dw-form-submit[disabled] { opacity:.6; cursor:default; }
.dw-quote-fineprint { margin:.9rem 0 0; font-size:.8rem; color:#8a8378; text-align:center; }
@media (max-width:600px){ .dw-form-row{ grid-template-columns:1fr; } .dw-quote-actions{ flex-direction:column; } .dw-quote-button,.dw-sample-button{ width:100%; } }
</style>
<script>
(function () {
// Endpoint: the same George-backed quote API the commercial-quote skill
// stood up for Koroseal, generalized to /api/quote-request. Falls back to
// the Koroseal path if the generalized route isn't deployed yet.
window.__DW_QUOTE_ENDPOINT = window.__DW_QUOTE_ENDPOINT || 'https://api.designerwallcoverings.com/api/quote-request';
})();
function dwOpenQuoteModal(e){ if(e) e.preventDefault();
document.getElementById('dw-quote-overlay').classList.add('active');
document.getElementById('dw-quote-modal').classList.add('active');
var first=document.querySelector('#dw-quote-form input:not([type=hidden])'); if(first) first.focus();
}
function dwCloseQuoteModal(e){
if(e && e.target && e.target.id!=='dw-quote-overlay') return;
document.getElementById('dw-quote-overlay').classList.remove('active');
document.getElementById('dw-quote-modal').classList.remove('active');
}
document.addEventListener('keydown', function(e){ if(e.key==='Escape') dwCloseQuoteModal(); });
// Keep the $4.25 Sample orderable — add the Sample variant to cart via AJAX.
async function dwAddSampleToCart(e){
var btn=e.currentTarget; var vid=btn.getAttribute('data-variant-id');
if(!vid) return;
btn.disabled=true; var orig=btn.textContent; btn.textContent='Adding…';
try{
var res=await fetch('/cart/add.js',{method:'POST',headers:{'Content-Type':'application/json'},
body:JSON.stringify({items:[{id:Number(vid),quantity:1}]})});
if(res.ok){ btn.textContent='✓ Sample added'; window.location.href='/cart'; }
else { btn.textContent='Try again'; btn.disabled=false; }
}catch(err){ console.error('sample add failed',err); btn.textContent='Try again'; btn.disabled=false; }
}
async function dwHandleQuoteSubmit(e){
e.preventDefault();
var submit=document.querySelector('#dw-quote-form .dw-form-submit');
submit.disabled=true; var orig=submit.textContent; submit.textContent='Submitting…';
var nameEl=document.getElementById('dw-q-name');
var emailEl=document.getElementById('dw-q-email');
var payload={
line: (function(){var el=document.querySelector('.dw-quote-section');return (el && el.getAttribute('data-quote-line')) || 'Koroseal';})(),
productId: document.getElementById('dw-q-product-id').value,
productTitle: document.getElementById('dw-q-product-title').value,
productUrl: document.getElementById('dw-q-product-url').value,
customerEmail: (document.getElementById('dw-q-customer-email').value) || (emailEl? emailEl.value : ''),
name: nameEl? nameEl.value : '',
projectName: document.getElementById('dw-q-project').value,
city: document.getElementById('dw-q-city').value,
yards: document.getElementById('dw-q-yards').value,
timeline: document.getElementById('dw-q-timeline').value||'',
budget: document.getElementById('dw-q-budget').value||'',
notes: document.getElementById('dw-q-notes').value||''
};
try{
var res=await fetch(window.__DW_QUOTE_ENDPOINT,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(payload)});
var out=await res.json();
if(out && out.success){
alert('✅ Quote request received!\n\nWe\'ll contact you within one business day.\n\nReference: '+(out.requestId||'—'));
document.getElementById('dw-quote-form').reset();
dwCloseQuoteModal();
} else {
alert('❌ '+((out&&out.message)||'Something went wrong. Please email info@designerwallcoverings.com.'));
}
}catch(err){
console.error('quote submit failed',err);
alert('❌ Could not submit. Please email info@designerwallcoverings.com or call us.');
} finally {
submit.disabled=false; submit.textContent=orig;
}
}
window.dwOpenQuoteModal=dwOpenQuoteModal;
window.dwCloseQuoteModal=dwCloseQuoteModal;
window.dwAddSampleToCart=dwAddSampleToCart;
window.dwHandleQuoteSubmit=dwHandleQuoteSubmit;
</script>
{%- endif -%}
{%- comment -%}
============================ THEME WIRING (Steve/gated) ============================
This snippet only RENDERS the CTA. To actually HIDE the yardage price + the
standard Add-to-Cart for quote-only products, the product template's
price + buy-buttons block must be gated on the SAME `quotes` tag. In the
live theme (id 144396058675), inside sections/main-product.liquid (or the
product-form section), wrap the existing price + buy-buttons like:
{%- assign _quote_only = false -%}
{%- if product.tags contains 'quotes' -%}{%- assign _quote_only = true -%}{%- endif -%}
{%- unless _quote_only -%}
... existing {% render 'price' ... %} ...
... existing buy-buttons / product-form (Add to cart) ...
{%- endunless -%}
{%- if _quote_only -%}
{% render 'dw-quote-only-cta' %}
{%- endif -%}
NOTE: the current theme's `contact_for_price` section renders EMPTY
(inner_len=0) on every product — it is NOT a usable quote mechanism, so this
snippet REPLACES it rather than extends it.
The Admin API token in secrets-manager has NO theme scope (read_themes
denied → write_themes out), so this wiring cannot be pushed by script. It
must be applied by Steve in the theme editor OR with a theme-scoped token.
=================================================================================
{%- endcomment -%}