← back to Dw Theme Compact Toolbar

assets/recently-viewed.js

227 lines

/**
 * recently-viewed.js - DW Recently Viewed Carousel — cards link directly to product pages
 * Requires: recently-viewed-tracker.js (window.DWRecentlyViewed)
 * Section:  #rv-section | Track: #rv-track
 */
(function () {
  'use strict';

  var CARD_LIMIT  = 8;
  var _activeCard = null;

  /* HELPER: Check if a price is real and non-zero. */
  function formatPrice(rawPrice) {
    if (!rawPrice) return '';
    var cleaned = String(rawPrice).replace(/[\$,\s]/g, '');
    if (!cleaned) return '';
    var num = parseFloat(cleaned);
    if (isNaN(num) || num <= 0) return '';
    return num.toFixed(2);
  }

  /* Fetch live price from Shopify storefront for products with missing/zero price.
     Updates the DOM element AND fixes the localStorage entry so it does not recur. */
  function fetchLivePrice(handle, priceElement) {
    if (!handle || !priceElement) return;
    fetch('/products/' + encodeURIComponent(handle) + '.js', { credentials: 'same-origin' })
      .then(function (res) { return res.ok ? res.json() : null; })
      .then(function (data) {
        if (!data) return;
        // Find highest non-sample variant price (in cents)
        var bestPrice = 0;
        if (Array.isArray(data.variants)) {
          for (var i = 0; i < data.variants.length; i++) {
            var v = data.variants[i];
            var titleLower = (v.title || '').toLowerCase();
            if (titleLower === 'sample' || titleLower.indexOf('sample') >= 0) continue;
            if (v.price > bestPrice) bestPrice = v.price;
          }
        }
        // Fallback to price_max (already in cents in .js endpoint)
        if (bestPrice === 0 && data.price_max > 0) bestPrice = data.price_max;
        if (bestPrice > 0) {
          var dollars = (bestPrice / 100).toFixed(2);
          priceElement.textContent = '$' + dollars;
          // Fix localStorage so this product shows correct price next time
          try {
            var items = JSON.parse(localStorage.getItem('dw_recently_viewed') || '[]');
            for (var j = 0; j < items.length; j++) {
              if (items[j].handle === handle) { items[j].price = dollars; break; }
            }
            localStorage.setItem('dw_recently_viewed', JSON.stringify(items));
          } catch (_) {}
        }
      })
      .catch(function () {});
  }

  function init() {
    if (typeof window.DWRecentlyViewed === 'undefined' ||
        typeof window.DWRecentlyViewed.getProducts !== 'function'
    ) { return hideSection(); }

    var products = window.DWRecentlyViewed.getProducts(CARD_LIMIT);
    if (!products || products.length === 0) { return hideSection(); }

    renderCards(products);
    var s = document.getElementById('rv-section');
    if (s) s.style.display = '';
    initArrows();
    injectJsonLd(products);
  }

  function hideSection() {
    var s = document.getElementById('rv-section');
    if (s) s.style.display = 'none';
  }

  function renderCards(products) {
    var track = document.getElementById('rv-track');
    if (!track) return;
    var frag = document.createDocumentFragment();
    for (var i = 0; i < products.length; i++) {
      frag.appendChild(createCard(products[i], i));
    }
    track.appendChild(frag);
    updateArrowVisibility();
  }

  function createCard(product, index) {
    var item = document.createElement('div');
    item.className = 'rv-track__item';
    item.setAttribute('role', 'listitem');

    var card = document.createElement('a');
    card.className = 'rv-card';
    card.href = product.url || '/products/' + product.handle;
    card.setAttribute('aria-label', product.title);

    var imgWrap = document.createElement('div');
    imgWrap.className = 'rv-card__img-wrap';

    var img = document.createElement('img');
    img.className  = 'rv-card__img';
    img.src        = product.featuredImage || '';
    img.alt        = '';
    img.loading    = index < 3 ? 'eager' : 'lazy';
    img.decoding   = 'async';
    img.onerror    = function () {
      this.parentNode.style.background = '#ece9e4';
      this.style.display = 'none';
    };
    imgWrap.appendChild(img);

    var body = document.createElement('div');
    body.className = 'rv-card__body';

    var vendor = document.createElement('p');
    vendor.className   = 'rv-card__vendor';
    vendor.textContent = product.vendor || '';

    var title  = document.createElement('h3');
    title.className   = 'rv-card__title';
    title.textContent = product.title || '';

    var price  = document.createElement('p');
    price.className   = 'rv-card__price';
    // FIX: Use robust formatPrice - catches 0, 0.00, $0.00, empty, null
    var formattedPrice = formatPrice(product.price);
    if (formattedPrice) {
      price.textContent = '$' + formattedPrice;
    } else {
      price.textContent = '';
      // Fetch live price for products with stale/zero cached price
      fetchLivePrice(product.handle, price);
    }

    body.appendChild(vendor);
    body.appendChild(title);
    body.appendChild(price);
    card.appendChild(imgWrap);
    card.appendChild(body);
    item.appendChild(card);

    return item;
  }

  function initArrows() {
    var track    = document.getElementById('rv-track');
    var btnLeft  = document.getElementById('rv-arrow-left');
    var btnRight = document.getElementById('rv-arrow-right');
    if (!track) return;
    if (btnLeft)  btnLeft.addEventListener('click',  function () { scrollTrack('left');  });
    if (btnRight) btnRight.addEventListener('click', function () { scrollTrack('right'); });
    track.addEventListener('scroll', updateArrowVisibility, { passive: true });
    if (typeof ResizeObserver !== 'undefined') {
      new ResizeObserver(updateArrowVisibility).observe(track);
    } else {
      window.addEventListener('resize', updateArrowVisibility, { passive: true });
    }
  }

  function scrollTrack(dir) {
    var track = document.getElementById('rv-track');
    if (!track) return;
    var card  = track.querySelector('.rv-card');
    var w     = card ? card.getBoundingClientRect().width : 200;
    var gap   = parseFloat(
      getComputedStyle(document.documentElement).getPropertyValue('--rv-gap')
    ) || 20;
    track.scrollBy({ left: dir === 'left' ? -(w + gap) : (w + gap), behavior: 'smooth' });
  }

  function updateArrowVisibility() {
    var track    = document.getElementById('rv-track');
    var btnLeft  = document.getElementById('rv-arrow-left');
    var btnRight = document.getElementById('rv-arrow-right');
    if (!track) return;
    var atStart = track.scrollLeft <= 2;
    var atEnd   = track.scrollLeft + track.clientWidth >= track.scrollWidth - 2;
    setArrowHidden(btnLeft,  atStart);
    setArrowHidden(btnRight, atEnd);
  }

  function setArrowHidden(btn, hide) {
    if (!btn) return;
    btn.classList.toggle('rv-arrow--hidden', hide);
    if (hide) { btn.setAttribute('aria-hidden', 'true'); }
    else      { btn.removeAttribute('aria-hidden'); }
  }

  function scheduleBtnReset(btn, ms) {
    setTimeout(function () { resetSampleBtn(btn); }, ms);
  }

  function injectJsonLd(products) {
    var items = [];
    for (var i = 0; i < products.length; i++) {
      var p = products[i];
      items.push({
        '@type':    'ListItem',
        position:   i + 1,
        name:       p.title         || '',
        url:        p.url           || '',
        image:      p.featuredImage || ''
      });
    }
    var script = document.createElement('script');
    script.type = 'application/ld+json';
    script.textContent = JSON.stringify({
      '@context': 'https://schema.org',
      '@type':    'ItemList',
      name:       'Recently Viewed Products',
      itemListElement: items
    });
    document.head.appendChild(script);
  }

  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', init);
  } else {
    init();
  }

}());