Hide Sold out on Related Products

Description

  • hide sold out products
  • get new item and fill in related products

(Before – Related with 4 Sold out products)

Hide sold out products on related products 1

(After using code)

Hide sold out products on related products 2

Install Code

Use code to Store Page Header Injection

<!-- @tuanphan - Related Products - Sold Out -->
<script>
(function() {
  const cache = new Map();
  const imageCache = new Map();

  async function fetchJSON(url) {
    try {
      const res = await fetch(url);
      return res.ok ? await res.json() : null;
    } catch (e) {
      console.error('Fetch error:', url, e);
      return null;
    }
  }

  async function getCategoryProducts(categoryUrl) {
    if (cache.has(categoryUrl)) {
      console.log('Using cached products for:', categoryUrl);
      return cache.get(categoryUrl);
    }
    console.log('Fetching products from:', categoryUrl);
    const data = await fetchJSON(categoryUrl + '?format=json');
    const products = data?.items || [];
    console.log('Found products:', products.length);
    cache.set(categoryUrl, products);
    return products;
  }

  async function getProductImage(product) {
    // Check cache first
    if (imageCache.has(product.id)) {
      return imageCache.get(product.id);
    }

    // Try assetUrl first
    let img = product.assetUrl;
    
    // If assetUrl is invalid (ends with just a number/), fetch individual product
    if (!img || /\/\d+\/?$/.test(img)) {
      console.log('Invalid assetUrl for', product.title, ', fetching product detail...');
      const productData = await fetchJSON(product.fullUrl + '?format=json');
      img = productData?.item?.items?.[0]?.assetUrl || 
            productData?.item?.items?.[0]?.mediaFocalPoint?.sourceUrl || 
            '';
    }

    imageCache.set(product.id, img);
    return img;
  }

  function isSoldOut(el) {
    const status = el.querySelector('.product-list-item-status');
    return status && /sold.?out/i.test(status.textContent);
  }

  function isAvailable(product) {
    const variants = product.structuredContent?.variants;
    if (!variants) {
      console.log('Product has no variants, treating as available:', product.id);
      return true;
    }
    const available = variants.some(v => 
      v.unlimited || v.qtyInStock === undefined || v.qtyInStock > 0
    );
    console.log(`Product ${product.id} availability:`, available, 
      'variants:', variants.map(v => ({unlimited: v.unlimited, qty: v.qtyInStock})));
    return available;
  }

  function getProductId(el) {
    return el.getAttribute('data-product-id') || 
           el.querySelector('.product-list-item-link')?.href?.match(/\/p\/([^\/\?]+)/)?.[1];
  }

  async function createProductHTML(product) {
    const url = product.fullUrl;
    const title = product.title || '';
    
    const img = await getProductImage(product);
    console.log(`Image for "${title}":`, img || 'NO IMAGE');

    const variant = product.structuredContent?.variants?.[0];
    const price = variant?.priceMoney ? `$${(variant.priceMoney.value / 100).toFixed(2)}` : '';

    return `
      <div class="product-list-item is-loaded" data-product-id="${product.id}">
        <a class="product-list-item-link" href="${url}" aria-label="${title}">
          <div class="product-list-image-wrapper">
            <figure class="product-list-item-image" data-animation-role="image">
              <div class="grid-image-wrapper">
                ${img ? `<img src="${img}" alt="${title}" class="grid-item-image grid-image-cover loaded" style="object-position: 50% 50%; display: block;" loading="lazy">` : ''}
              </div>
            </figure>
          </div>
          <section class="product-list-item-meta" data-animation-role="content">
            <div class="product-list-title-price">
              <div class="product-list-item-title">${title}</div>
              ${price ? `<div class="product-list-item-price">${price}</div>` : ''}
            </div>
            <div class="product-list-item-status"></div>
          </section>
        </a>
      </div>
    `;
  }

  async function replace() {
    console.log('=== STARTING REPLACE FUNCTION ===');
    
    const container = document.querySelector('.product-related-products .product-list-container');
    if (!container) {
      console.log('No related products container found');
      return;
    }
    console.log('Found related products container');

    const data = await fetchJSON(location.pathname + '?format=json');
    if (!data?.item) {
      console.log('No product data found');
      return;
    }
    console.log('Current product ID:', data.item.id);

    const categories = data.nestedCategories?.itemCategories || [];
    console.log('Product categories:', categories.map(c => c.displayName).join(', '));
    if (!categories.length) {
      console.log('No categories found');
      return;
    }

    const items = Array.from(container.querySelectorAll('.product-list-item'));
    console.log('Total related products:', items.length);
    
    // Build image cache from existing items
    items.forEach(el => {
      const id = getProductId(el);
      const img = el.querySelector('img')?.src;
      if (id && img) {
        imageCache.set(id, img);
      }
    });
    
    const soldOut = items.filter(isSoldOut);
    console.log('Sold out products:', soldOut.length);
    soldOut.forEach((el, i) => {
      const id = getProductId(el);
      const title = el.querySelector('.product-list-item-title')?.textContent;
      console.log(`  ${i+1}. Sold out: ${title} (ID: ${id})`);
    });
    
    if (!soldOut.length) {
      console.log('No sold out products to replace');
      return;
    }

    const excludeIds = new Set([
      data.item.id,
      ...items.map(getProductId).filter(Boolean)
    ]);
    console.log('Excluded IDs:', Array.from(excludeIds));

    const available = [];
    for (const cat of categories) {
      console.log(`\nSearching category: ${cat.displayName} (${cat.fullUrl})`);
      const products = await getCategoryProducts(cat.fullUrl);
      
      for (const p of products) {
        if (excludeIds.has(p.id)) {
          console.log(`  Skipping (already excluded): ${p.id}`);
          continue;
        }
        
        if (!isAvailable(p)) {
          console.log(`  Skipping (not available): ${p.id}`);
          continue;
        }
        
        console.log(`  ✓ Adding available product: ${p.id} - ${p.title}`);
        excludeIds.add(p.id);
        available.push(p);
        
        if (available.length >= soldOut.length) {
          console.log(`  Reached target (${soldOut.length} products needed)`);
          break;
        }
      }
      
      if (available.length >= soldOut.length) break;
    }

    console.log('\n=== REPLACEMENT SUMMARY ===');
    console.log('Sold out items to replace:', soldOut.length);
    console.log('Available products found:', available.length);

    for (let i = 0; i < soldOut.length; i++) {
      const el = soldOut[i];
      if (available[i]) {
        const oldTitle = el.querySelector('.product-list-item-title')?.textContent;
        const newTitle = available[i].title;
        console.log(`Replacing: "${oldTitle}" → "${newTitle}"`);
        
        const newHTML = await createProductHTML(available[i]);
        console.log('Generated HTML length:', newHTML.length);
        
        const temp = document.createElement('div');
        temp.innerHTML = newHTML.trim();
        
        const newElement = temp.firstElementChild;
        if (newElement) {
          el.replaceWith(newElement);
          console.log('✓ Successfully replaced element');
        } else {
          console.error('Failed to create new element');
        }
      } else {
        console.log(`No replacement available for position ${i+1}`);
      }
    }
    
    console.log('=== REPLACE FUNCTION COMPLETE ===\n');
  }

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

    if (window.Squarespace?.onInitialize) {
      window.Squarespace.onInitialize(Y, () => Y.on('mercury:load', replace));
    }
  }

  init();
})();
</script>

Hide sold out products on related products 3

Buy me a coffee