← back to Dw Theme Boost Fix

assets/recently-viewed-tracker.js

120 lines

/**
 * recently-viewed-tracker.js
 * Tracks product page views in localStorage for the DW Shopify storefront.
 * Reads window.DWProduct (injected by Liquid), persists up to 20 entries under
 * "dw_recently_viewed", and exposes window.DWRecentlyViewed as the public API.
 * Zero dependencies. Safe in private browsing (all localStorage errors caught).
 */

(function () {
  'use strict';

  var STORAGE_KEY = 'dw_recently_viewed';
  var MAX_ITEMS = 20;
  var DEFAULT_LIMIT = 8;

  // Read the stored list; return [] on any error (private browsing, corrupt data).
  function readStorage() {
    try {
      var raw = localStorage.getItem(STORAGE_KEY);
      if (!raw) return [];
      var parsed = JSON.parse(raw);
      return Array.isArray(parsed) ? parsed : [];
    } catch (_) {
      return [];
    }
  }

  // Write the list back; silently swallow QuotaExceededError / SecurityError.
  function writeStorage(items) {
    try {
      localStorage.setItem(STORAGE_KEY, JSON.stringify(items));
    } catch (_) {}
  }

  // Guard: product must be an object with a non-empty handle string.
  function isValidProduct(product) {
    return (
      product !== null &&
      typeof product === 'object' &&
      typeof product.handle === 'string' &&
      product.handle.trim() !== ''
    );
  }

  // Build a safe, normalized entry from window.DWProduct.
  // Only whitelisted fields are copied; viewedAt is added here.
  function buildEntry(src) {
    return {
      handle:       String(src.handle       || '').trim(),
      title:        String(src.title        || '').trim(),
      featuredImage:String(src.featuredImage|| '').trim(),
      price:        String(src.price        || '').trim(),
      vendor:       String(src.vendor       || '').trim(),
      url:          String(src.url          || '').trim(),
      productType:  String(src.productType  || '').trim(),
      viewedAt:     Date.now()
    };
  }

  // Track the current product:
  //   1. Remove any existing entry for the same handle (deduplication).
  //   2. Prepend the new entry (most-recent-first order).
  //   3. Trim to MAX_ITEMS (drop oldest).
  //   4. Persist.
  function trackCurrentProduct() {
    if (!isValidProduct(window.DWProduct)) return;

    var entry = buildEntry(window.DWProduct);
    var items = readStorage().filter(function (p) {
      return p.handle !== entry.handle;
    });

    items.unshift(entry);

    if (items.length > MAX_ITEMS) {
      items = items.slice(0, MAX_ITEMS);
    }

    writeStorage(items);
  }

  // Public API — exposed on window.DWRecentlyViewed.
  var api = {
    // Returns up to `limit` products (default 8), newest first.
    // Shallow-copies each entry so callers cannot mutate stored state.
    getProducts: function (limit) {
      var max = (typeof limit === 'number' && limit > 0)
        ? Math.min(limit, MAX_ITEMS)
        : DEFAULT_LIMIT;
      return readStorage().slice(0, max).map(function (item) {
        return Object.assign({}, item);
      });
    },

    // Clears the entire recently-viewed list from localStorage.
    clearAll: function () {
      try { localStorage.removeItem(STORAGE_KEY); } catch (_) {}
    },

    // Returns the total count of stored products (unaffected by any limit).
    getCount: function () {
      return readStorage().length;
    }
  };

  // Register the public API. Guard against double-inclusion in theme bundles.
  if (!window.DWRecentlyViewed) {
    window.DWRecentlyViewed = api;
  }

  // Run after Liquid-injected <script> tags have executed so window.DWProduct
  // is guaranteed to exist. Works whether this file is deferred or async.
  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', trackCurrentProduct);
  } else {
    trackCurrentProduct();
  }

}());