← back to Dw Pairs Well

deploy/shopify/similar-designs.liquid

208 lines

{%- comment -%}
    Similar Designs v1 — visual ALTERNATIVES to this product (NOT coordinates).
      • Grid of look-alike designs: same motif / material / era / palette
      • Density slider 3-12 cols (default 8), localStorage-persisted
      • Each card shows palette dots + a "why it's similar" chip
    Service: https://pairs.designerwallcoverings.com/api/similar  (~/Projects/dw-pairs-well)
    Self-contained .similar-designs__* namespace + IIFE — coexists with the
    Design Coordinate section (#pairs-well-with) on the same product/pattern page.
  {%- endcomment -%}
  <section
    id="similar-designs"
    class="sd-section"
    data-similar-endpoint="https://pairs.designerwallcoverings.com/api/similar"
    data-product-handle="{{ product.handle }}"
    data-product-sku="{{ product.metafields.custom.manufacturer_sku | default: product.selected_or_first_available_variant.sku | escape }}"
    style="margin: 1.5em 0 2em; clear: both;"
    hidden
  >
    <button type="button" class="similar-designs__cta" aria-expanded="false">
      <span class="similar-designs__cta-text">Similar Designs</span>
      <span class="similar-designs__cta-icon" aria-hidden="true">+</span>
    </button>

    <div class="similar-designs__panel" id="similar-designs-panel" hidden>
      <div class="similar-designs__panel-head">
        <h3>Designs like this <span class="count" id="similar-designs-count"></span></h3>
        <div class="similar-designs__controls">
          <label for="similar-designs-cols">Columns</label>
          <input id="similar-designs-cols" type="range" min="3" max="12" value="8" step="1">
          <span class="similar-designs__cols-val" id="similar-designs-cols-val">8</span>
        </div>
      </div>
      <div id="similar-designs-grid" class="similar-designs__grid"></div>
    </div>
  </section>

  <style>
    #similar-designs { font-family: inherit; --sd-cols: 8; }
    .similar-designs__cta {
      display: flex; align-items: center; justify-content: space-between;
      width: 100%; padding: 18px 24px; margin: 0;
      background: #111; color: #fff;
      border: 0; border-radius: 999px;
      font-size: 15px; font-weight: 500; letter-spacing: 0.06em;
      text-transform: uppercase; cursor: pointer;
      transition: background 160ms ease, transform 120ms ease;
    }
    .similar-designs__cta:hover  { background: #2a2a2a; }
    .similar-designs__cta:active { transform: scale(0.995); }
    .similar-designs__cta[aria-expanded="true"] .similar-designs__cta-icon { transform: rotate(45deg); }
    .similar-designs__cta-icon { display: inline-block; font-size: 22px; line-height: 1; transition: transform 200ms ease; }

    .sd-dot { display: inline-block; width: 12px; height: 12px; border-radius: 50%; border: 1px solid rgba(0,0,0,0.12); vertical-align: middle; }

    .similar-designs__panel-head {
      display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap;
      gap: 12px; margin: 24px 0 12px;
    }
    .similar-designs__panel-head h3 { margin: 0; font: 500 18px/1.2 ui-serif, Georgia, serif; }
    .similar-designs__panel-head .count { color: #888; font-size: 12px; margin-left: 6px; font-family: -apple-system, sans-serif; font-weight: 400; }
    .similar-designs__controls {
      display: flex; align-items: center; gap: 10px;
      font-size: 11px; color: #777; text-transform: uppercase; letter-spacing: 0.06em;
    }
    .similar-designs__controls input[type=range] { width: 160px; accent-color: #1a1a1a; }
    .similar-designs__cols-val { font-variant-numeric: tabular-nums; color: #1a1a1a; min-width: 1.5em; text-align: right; font-weight: 600; }

    .similar-designs__grid {
      display: grid; gap: 14px; margin-top: 6px;
      grid-template-columns: repeat(var(--sd-cols), minmax(0, 1fr));
    }
    @media (max-width: 1100px) { .similar-designs__grid { grid-template-columns: repeat(min(6, var(--sd-cols)), minmax(0, 1fr)); } }
    @media (max-width: 900px)  { .similar-designs__grid { grid-template-columns: repeat(min(4, var(--sd-cols)), minmax(0, 1fr)); } }
    @media (max-width: 600px)  { .similar-designs__grid { grid-template-columns: repeat(min(2, var(--sd-cols)), minmax(0, 1fr)); } }

    .similar-designs__card {
      display: block; text-decoration: none; color: inherit;
      background: #fff; border: 1px solid #e7e7e7; border-radius: 6px;
      overflow: hidden; transition: box-shadow 160ms ease, transform 160ms ease;
    }
    .similar-designs__card:hover { box-shadow: 0 6px 20px rgba(0,0,0,0.08); transform: translateY(-2px); }
    .similar-designs__img-wrap { aspect-ratio: 1 / 1; background: #f4f4f4; overflow: hidden; }
    .similar-designs__img { width: 100%; height: 100%; object-fit: cover; display: block; }
    .similar-designs__meta { padding: 10px 12px 12px; }
    .similar-designs__title { margin: 0 0 4px; font-size: 13px; font-weight: 600; line-height: 1.3; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
    .similar-designs__vendor { margin: 0 0 6px; font-size: 10px; color: #777; letter-spacing: 0.04em; text-transform: uppercase; }
    .similar-designs__why { display: inline-block; padding: 2px 7px; background: #eef0f4; color: #34425a; border-radius: 999px; font-size: 10px; line-height: 1.4; margin-bottom: 8px; }
    .similar-designs__palette { display: flex; gap: 3px; margin: 0; flex-wrap: wrap; }

    .similar-designs__skeleton { background: linear-gradient(90deg, #eee 0%, #f6f6f6 50%, #eee 100%); background-size: 200% 100%; animation: sdShim 1.2s linear infinite; aspect-ratio: 1 / 1; border-radius: 6px; }
    @keyframes sdShim { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } }
    .similar-designs__empty { padding: 18px; color: #777; font-size: 14px; grid-column: 1 / -1; }
  </style>

  <script>
    (function () {
      var root = document.getElementById('similar-designs');
      if (!root) return;

      var endpoint = root.getAttribute('data-similar-endpoint');
      var handle   = root.getAttribute('data-product-handle');
      var sku      = root.getAttribute('data-product-sku');
      // Pattern page (no Shopify product object) can set data-source-handle directly.
      if (!handle) handle = root.getAttribute('data-source-handle') || '';
      if (!sku)    sku    = root.getAttribute('data-source-sku') || '';
      var hasIdentifier = (sku && sku.length) || (handle && handle.length);
      if (!hasIdentifier) return;
      root.hidden = false;

      var cta      = root.querySelector('.similar-designs__cta');
      var panel    = document.getElementById('similar-designs-panel');
      var grid     = document.getElementById('similar-designs-grid');
      var cols     = document.getElementById('similar-designs-cols');
      var colsVal  = document.getElementById('similar-designs-cols-val');
      var countEl  = document.getElementById('similar-designs-count');
      var loaded   = false;

      function esc(s) {
        return String(s == null ? '' : s)
          .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
          .replace(/"/g, '&quot;').replace(/'/g, '&#39;');
      }

      // Density slider, localStorage-persisted (Steve's standing grid rule).
      function wireSlider() {
        function apply() {
          var n = Math.max(3, Math.min(12, parseInt(cols.value, 10) || 8));
          colsVal.textContent = n;
          root.style.setProperty('--sd-cols', n);
          try { localStorage.setItem('sd.cols', n); } catch (_) {}
        }
        try {
          var s = parseInt(localStorage.getItem('sd.cols'), 10);
          if (!isNaN(s) && s >= 3 && s <= 12) cols.value = s;
        } catch (_) {}
        cols.addEventListener('input', apply);
        apply();
      }
      wireSlider();

      function renderSkeletons(n) {
        var html = '';
        for (var i = 0; i < n; i++) html += '<div class="similar-designs__skeleton"></div>';
        grid.innerHTML = html;
      }

      function renderCard(p) {
        var img = p.image_url ? esc(p.image_url) : '';
        var why = (p.why && p.why[0]) ? esc(p.why[0]) : '';
        var palette = (p.palette || []).map(function (c) {
          var hex = typeof c === 'string' ? c : c.hex;
          return '<span class="sd-dot" style="background:' + esc(hex) + '"></span>';
        }).join('');
        var href = p.url ? esc(p.url) : (p.handle ? '/products/' + esc(p.handle) : '#');
        return '<a class="similar-designs__card" href="' + href + '" title="' + esc((p.why || []).join(' · ')) + '">'
          +   '<div class="similar-designs__img-wrap">' + (img ? '<img class="similar-designs__img" src="' + img + '" alt="' + esc(p.title) + '" loading="lazy">' : '') + '</div>'
          +   '<div class="similar-designs__meta">'
          +     '<p class="similar-designs__title">' + esc(p.title || '') + '</p>'
          +     '<p class="similar-designs__vendor">' + esc(p.vendor || '') + '</p>'
          +     (why ? '<span class="similar-designs__why">' + why + '</span>' : '')
          +     (palette ? '<div class="similar-designs__palette">' + palette + '</div>' : '')
          +   '</div>'
          + '</a>';
      }

      function renderGrid(items) {
        if (!items || !items.length) { grid.innerHTML = '<div class="similar-designs__empty">No similar designs found.</div>'; return; }
        grid.innerHTML = items.map(renderCard).join('');
      }

      function buildUrl(limit) {
        var qs = [];
        if (sku)    qs.push('dw_sku=' + encodeURIComponent(sku));
        if (handle) qs.push('handle=' + encodeURIComponent(handle));
        if (limit)  qs.push('limit=' + limit);
        return endpoint + '?' + qs.join('&');
      }

      function load() {
        if (loaded) return;
        loaded = true;
        panel.hidden = false;
        renderSkeletons(12);
        fetch(buildUrl(12), { credentials: 'omit' })
          .then(function (r) { return r.ok ? r.json() : Promise.reject(new Error('http ' + r.status)); })
          .then(function (d) {
            if (!d || d.ok === false) throw new Error((d && d.error) || 'bad payload');
            var items = d.similar || [];
            renderGrid(items);
            countEl.textContent = '(' + items.length + ')';
            if (!items.length) root.hidden = true;   // nothing similar → hide the whole section
          })
          .catch(function () { grid.innerHTML = '<div class="similar-designs__empty">Could not load similar designs right now.</div>'; });
      }

      cta.addEventListener('click', function () {
        var open = cta.getAttribute('aria-expanded') === 'true';
        if (open) {
          cta.setAttribute('aria-expanded', 'false');
          panel.hidden = true;
          return;
        }
        cta.setAttribute('aria-expanded', 'true');
        load();
      });
    })();
  </script>