← back to Dw Theme Compact Toolbar
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();
}
}());