← back to Majilite Quote Cta

snippets/dw-quote-only-cta.liquid

285 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-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&quot; 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()">&times;</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:'Majilite',
        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 -%}