Product Card Template after customization using the Refresh theme
Below is the updated product card code, customized to fit your product data.
function loadScript(src) { const script = document.createElement('script'); script.src = src; script.defer = true; return document.body.appendChild(script); } loadScript('https://store-for-mozilla-mac.myshopify.com/cdn/shop/t/3/assets/quick-add.js') function stringToSlug(text) { if (!text) return text; return text .toLowerCase() .replace(/[^\w ]+/g, "") .replace(/ +/g, "-"); } function setElementAttribute({ element, attributeName, value }) { if (element && element.hasAttribute(attributeName)) { element.setAttribute(attributeName, value); } } function setSelectedOption({ element, optionName }) { const optionsButton = element.target.offsetParent .querySelector(`.options-button-${stringToSlug(optionName)}`) .querySelectorAll(`button`); const defaultSelectedClassChanger = () => { optionsButton.forEach((button, index) => { button.classList.remove("sledge__product-variant-size-swatch-active"); element.target.className += " sledge__product-variant-size-swatch-active"; }); }; const colorSelectedClassChanger = () => { optionsButton.forEach((button, index) => { button.classList.remove("sledge__product-variant-color-swatch-active"); element.target.className += " sledge__product-variant-color-swatch-active"; }); }; switch (optionName) { case "Color": colorSelectedClassChanger(); break; case "Size": defaultSelectedClassChanger(); break; default: defaultSelectedClassChanger(); } } function addToCartTrigger({ sourceApp, productId }) { window?.sledge?.addToCartTrigger?.({ sourceApp, productId, }); } function productClickTrigger({ sourceApp, productId }) { window?.sledge?.productClickTrigger?.({ sourceApp, productId, }); } function setSelectedVariant({ id, element, value, optionIndex, sourceApp }) { const productCard = element?.target?.offsetParent; const selectedInput = productCard.querySelector( `.sledge__product-grid-card-selected-option[data-product-id='${id}']` ); setElementAttribute({ element: selectedInput, attributeName: `data-selected-option${optionIndex}`, value: value, }); // define option1 and option 2 const option1 = `[data-option-1="${stringToSlug( selectedInput?.attributes?.["data-selected-option1"]?.value )}"]`; const option2 = `${ selectedInput?.attributes?.["data-selected-option2"] ? `[data-option-2="${stringToSlug( selectedInput?.attributes?.["data-selected-option2"]?.value )}"]` : "" }`; // define selected option const selectOption = productCard.querySelector( `select option${option1}${option2}` ); const variantId = selectOption?.attributes?.[ "data-graphql-id" ]?.value?.replace(/^gid:\/\/shopify\/ProductVariant\/(\d+)$/, "$1"); const imageId = selectOption?.attributes?.["data-image-id"]?.value; const inventoryQuantity = selectOption?.attributes?.["data-inventory-quantity"]?.value; const inventoryManagement = selectOption?.attributes?.["data-inventory-management"]?.value; const inventoryPolicy = selectOption?.attributes?.["data-inventory-policy"]?.value; const availability = selectOption?.attributes?.["data-availability"]?.value; const price = selectOption?.attributes?.["data-price"]?.value; const compareAtPrice = selectOption?.attributes?.["data-compare-at-price"]?.value; const salePercent = selectOption?.attributes?.["data-sale-percent"]?.value; const isOnSale = parseFloat(String(compareAtPrice)) ? parseFloat(String(compareAtPrice)) > parseFloat(String(price)) : false; const links = productCard.querySelectorAll(`a[data-product-url="true"]`); links?.forEach((link) => { if (link?.href) { const url = new URL(link.href); url.searchParams.set("variant", variantId); link.href = url.toString(); } }); const setOther = () => { //set data-variant-id attribute setElementAttribute({ element: selectedInput, attributeName: "data-variant-id", value: selectOption?.attributes?.["data-graphql-id"]?.value || "", }); const variantInputs = productCard.querySelectorAll("input[name=id]"); variantInputs?.forEach((inputElement) => { if (inputElement) { setElementAttribute({ element: inputElement, attributeName: "value", value: selectOption?.attributes?.["data-graphql-id"]?.value?.replace( /gid:\/\/shopify\/ProductVariant\/(\d+)/, "$1" ) || "", }); } }); setElementAttribute({ element: selectedInput, attributeName: "data-inventory-quantity", value: inventoryQuantity || "", }); setElementAttribute({ element: selectedInput, attributeName: "data-availability", value: availability || "", }); setElementAttribute({ element: selectedInput, attributeName: "data-inventory-management", value: inventoryManagement || "", }); setElementAttribute({ element: selectedInput, attributeName: "data-inventory-policy", value: inventoryPolicy || "", }); setElementAttribute({ element: selectedInput, attributeName: "data-price", value: price || "", }); setElementAttribute({ element: selectedInput, attributeName: "data-compare-at-price", value: compareAtPrice || "", }); setElementAttribute({ element: selectedInput, attributeName: "data-sale-percent", value: salePercent || "", }); //change product image by variant if (imageId && imageId !== "null") { const imageElement = productCard.querySelector( `img.sledge__product-grid-card-image-featured-image` ); const sourceImage = productCard.querySelector( `div.sledge__product-grid-card-variant-images img[id="${imageId}"]` ).src; if (imageElement) imageElement.src = sourceImage; } }; updateBadge({ productCard, availability, variantId, sourceApp, isOnSale, salePercent, }); updatePrice({ price, compareAtPrice, productCard, isOnSale }); setOther(); const result = { variantId, imageId, }; return result; } function updateBadge({ productCard, availability, variantId, sourceApp, isOnSale, salePercent, }) { const instantSearchSettings = JSON.parse( String(localStorage.getItem("sledge-instant-search-setting") || null) ); const language_sold_out = instantSearchSettings?.languages?.["out of stock"] || "Out of Stock"; const badgeList = productCard.querySelector( ".sledge__product-grid-card-image" ); if (!badgeList) return; const existingSoldOutBadge = badgeList.querySelector( ".sledge__product-grid-card-out-of-stock" ); const existingOnSaleBadge = badgeList.querySelector( ".sledge__product-grid-badge-on-sale" ); const isOutOfStock = !( availability && availability.toLowerCase() === "in stock" ); if (existingSoldOutBadge) { badgeList.removeChild(existingSoldOutBadge); } if (existingOnSaleBadge) { badgeList.removeChild(existingOnSaleBadge); } if (isOutOfStock) { const soldOutBadge = document.createElement("div"); soldOutBadge.className = "sledge__product-grid-card-out-of-stock"; soldOutBadge.textContent = language_sold_out; badgeList.insertAdjacentElement("beforeend", soldOutBadge); } if (isOnSale) { const onSaleBadge = document.createElement("span"); onSaleBadge.className = "sledge__product-grid-badge-on-sale"; onSaleBadge.textContent = `On Sale`; badgeList.insertAdjacentElement("beforeend", onSaleBadge); } updateAddToCartButton({ productCard, isOutOfStock, variantId, sourceApp }); } function updatePrice({ price, compareAtPrice, productCard, isOnSale }) { const priceList = productCard.querySelector( ".sledge__product-grid-card-price" ); if (!priceList) return; const salePriceElement = priceList.querySelector( ".sledge__product-grid-card-price div:not(.sledge__product-grid-card-compare-at-price)" ); const existingCompareAtPriceElement = priceList.querySelector( ".sledge__product-grid-card-compare-at-price" ); if (salePriceElement) { salePriceElement.innerHTML = window?.sledge?.shopifyFormatMoney?.(price); } if (existingCompareAtPriceElement) { priceList.removeChild(existingCompareAtPriceElement); } if (isOnSale) { const compareAtPriceElement = document.createElement("div"); compareAtPriceElement.className = "sledge__product-grid-card-compare-at-price"; compareAtPriceElement.innerHTML = `${window?.sledge?.shopifyFormatMoney?.( compareAtPrice )}`; priceList.insertAdjacentElement("beforeend", compareAtPriceElement); } else { } } function updateAddToCartButton({ productCard, isOutOfStock, variantId, sourceApp, }) { const instantSearchSettings = JSON.parse( String(localStorage.getItem("sledge-instant-search-setting") || null) ); const language_add_to_cart = instantSearchSettings?.languages?.add_to_cart || "Add to cart"; const language_sold_out = instantSearchSettings?.languages?.["out of stock"] || "Out of Stock"; const productHandle = productCard .querySelector("[data-product-handle]") .getAttribute("data-product-handle"); let productUrl = `/products/${productHandle}`; const buttonContainer = productCard.querySelector( ".sledge__product-grid-button-wrapper" ); if (!buttonContainer) return; const productId = productCard.getAttribute("data-product-id"); if (!productId) return; const addToCartButtonDisabled = ` <button class="sledge__button sledge__product-grid-button-add-to-cart" data-button-color-type="light" data-button-full-width="false" type="button" disabled> <span class="sledge-icon__bag"> <svg width="15" height="15" viewBox="0 0 18 17" fill="none" xmlns="http://www.w3.org/2000/svg"> <g id="vuesax/bold/bag-2"> <g id="bag-2"> <path id="Vector" d="M14.3711 6.04627C13.919 5.54693 13.2374 5.25677 12.2927 5.15555V4.64271C12.2927 3.71824 11.9013 2.82752 11.2131 2.20671C10.518 1.57241 9.6138 1.2755 8.67584 1.36322C7.06309 1.51842 5.70676 3.07719 5.70676 4.76417V5.15555C4.76205 5.25677 4.08051 5.54693 3.6284 6.04627C2.97385 6.77505 2.9941 7.74674 3.06833 8.42154L3.54068 12.1801C3.68238 13.496 4.21547 14.8455 7.11707 14.8455H10.8824C13.784 14.8455 14.3171 13.496 14.4588 12.1869L14.9312 8.41479C15.0054 7.74674 15.0189 6.77505 14.3711 6.04627ZM8.77031 2.30118C9.4451 2.24045 10.0862 2.44963 10.5855 2.90174C11.0781 3.34711 11.3548 3.98141 11.3548 4.64271V5.11506H6.64472V4.76417C6.64472 3.56304 7.63666 2.40915 8.77031 2.30118ZM6.58399 8.87365H6.57724C6.2061 8.87365 5.90245 8.56999 5.90245 8.19885C5.90245 7.82772 6.2061 7.52406 6.57724 7.52406C6.95512 7.52406 7.25878 7.82772 7.25878 8.19885C7.25878 8.56999 6.95512 8.87365 6.58399 8.87365ZM11.3075 8.87365H11.3008C10.9296 8.87365 10.626 8.56999 10.626 8.19885C10.626 7.82772 10.9296 7.52406 11.3008 7.52406C11.6787 7.52406 11.9823 7.82772 11.9823 8.19885C11.9823 8.56999 11.6787 8.87365 11.3075 8.87365Z" fill="currentColor"></path> </g> </g> </svg> </span> <span>${language_sold_out}</span> </button>`; const addToCartButtonEnabled = ` <form method="post" action="/cart/add" id="product_form_${productId}" accept-charset="UTF-8" class="shopify-product-form" enctype="multipart/form-data" is="product-form" onsubmit="addToCartTrigger({ sourceApp: '${sourceApp}', productId: '${productId}' })" > <input type="hidden" name="form_type" value="product" /> <input type="hidden" name="utf8" value="✓" /> <input type="hidden" name="id" value="${variantId}" /> <input type="hidden" name="product-id" value="${productId}"> <button class="sledge__button sledge__product-grid-button-add-to-cart" data-button-color-type="light" data-button-full-width="false" type="submit"> <span class="sledge-icon__bag"> <svg width="15" height="15" viewBox="0 0 18 17" fill="none" xmlns="http://www.w3.org/2000/svg"> <g id="vuesax/bold/bag-2"> <g id="bag-2"> <path id="Vector" d="M14.3711 6.04627C13.919 5.54693 13.2374 5.25677 12.2927 5.15555V4.64271C12.2927 3.71824 11.9013 2.82752 11.2131 2.20671C10.518 1.57241 9.6138 1.2755 8.67584 1.36322C7.06309 1.51842 5.70676 3.07719 5.70676 4.76417V5.15555C4.76205 5.25677 4.08051 5.54693 3.6284 6.04627C2.97385 6.77505 2.9941 7.74674 3.06833 8.42154L3.54068 12.1801C3.68238 13.496 4.21547 14.8455 7.11707 14.8455H10.8824C13.784 14.8455 14.3171 13.496 14.4588 12.1869L14.9312 8.41479C15.0054 7.74674 15.0189 6.77505 14.3711 6.04627ZM8.77031 2.30118C9.4451 2.24045 10.0862 2.44963 10.5855 2.90174C11.0781 3.34711 11.3548 3.98141 11.3548 4.64271V5.11506H6.64472V4.76417C6.64472 3.56304 7.63666 2.40915 8.77031 2.30118ZM6.58399 8.87365H6.57724C6.2061 8.87365 5.90245 8.56999 5.90245 8.19885C5.90245 7.82772 6.2061 7.52406 6.57724 7.52406C6.95512 7.52406 7.25878 7.82772 7.25878 8.19885C7.25878 8.56999 6.95512 8.87365 6.58399 8.87365ZM11.3075 8.87365H11.3008C10.9296 8.87365 10.626 8.56999 10.626 8.19885C10.626 7.82772 10.9296 7.52406 11.3008 7.52406C11.6787 7.52406 11.9823 7.82772 11.9823 8.19885C11.9823 8.56999 11.6787 8.87365 11.3075 8.87365Z" fill="currentColor"></path> </g> </g> </svg> </span> <span>${language_add_to_cart}</span> </button> </form>`; if (isOutOfStock) { buttonContainer.innerHTML = addToCartButtonDisabled; } else { buttonContainer.innerHTML = addToCartButtonEnabled; } } function renderProductCard({ product, sourceApp }) { const { id, admin_graphql_api_id, title, image, handle, url, vendor, product_type, body_html, currency, variants, review, options: productOptions, created_at, } = product || {}; const variantsInStock = variants?.filter( (variant) => variant?.availability?.toLowerCase() === "in stock" ); const { id: variant_id = "", admin_graphql_api_id: variant_admin_graphql_api_id = "", title: variant_title = "", price = "", compare_at_price = "", sku = "", availability, sale_percent, inventory_quantity, } = variantsInStock?.length ? variantsInStock[0] : variants?.[0]; const { total: totalReview, average: averageReview } = review || {}; const instantSearchSettings = JSON.parse( String(localStorage.getItem("sledge-instant-search-setting") || "null") ) || {}; const generalSettings = JSON.parse( String(localStorage.getItem("sledge-general-setting") || "null") ) || {}; const { show_price = true, show_vendor = true, show_sku = true, } = instantSearchSettings?.display?.search || {}; const isOnSale = parseFloat(String(compare_at_price)) ? parseFloat(String(compare_at_price)) > parseFloat(String(price)) : false; const isOutOfStock = !Boolean(availability === "in stock"); const isLowInStock = !isOutOfStock && Number(inventory_quantity) <= 20; const isVeryLowInStock = !isOutOfStock && Number(inventory_quantity) <= 10; const productUrl = `/products/${handle}?variant=${variant_id}`; const productImageReplacingSrc = ({ src, width = 500 }) => { if (!src) return "https://sledgeassets.nyc3.cdn.digitaloceanspaces.com/images/blank-image.png"; try { const imageUrl = new URL(src); imageUrl.searchParams.set("width", width); return imageUrl.toString(); } catch (error) { // Handle invalid URLs return src; } }; const productImageUrl = productImageReplacingSrc({ src: image?.src, width: 500, }); const options = product?.options ? Object.entries(product.options) : []; const images = product?.images ?? []; const defaultSelected = {}; const secondaryImage = images?.[1]; const secondaryProductImageUrl = secondaryImage ? productImageReplacingSrc({ src: secondaryImage?.src, width: 500, }) : null; const ratingTotalAttribute = typeof totalReview !== "undefined" ? `data-rating-total="${totalReview}"` : ""; const ratingAverageAttribute = typeof averageReview !== "undefined" ? `data-rating-average="${averageReview}"` : ""; function setDefaultFunction() { let variantList = []; if (variantsInStock?.length) { variantList = variantsInStock; } else if (variants?.length) { variantList = variants; } else { return; } defaultSelected["data-product-id"] = id; defaultSelected["data-product-handle"] = handle; defaultSelected["data-selected-option1"] = variantList[0].option1; if (variantList[0].option2) { defaultSelected["data-selected-option2"] = variantList[0].option2; } defaultSelected["data-variant-id"] = variantList[0].admin_graphql_api_id; defaultSelected["data-inventory-quantity"] = variantList[0].inventory_quantity; defaultSelected["data-inventory-management"] = variantList[0].inventory_management; defaultSelected["data-inventory-policy"] = variantList[0].inventory_policy; } setDefaultFunction(); const language_add_to_cart = instantSearchSettings?.languages?.add_to_cart || "Add to cart"; const language_sold_out = instantSearchSettings?.languages?.["out of stock"] || "Out of Stock"; const language_in_stock = instantSearchSettings?.languages?.["in stock"] || "In Stock"; const variantImagesHtml = images ?.map?.( (image) => `<img decoding="async" loading="lazy" alt="${image?.alt || ""}" id="${ image?.id }" src="${image?.src}" />` ) ?.join?.("") || ""; const variantOptionsHtml = variants ?.map?.((variant) => { const { title, option1, option2, position, id, admin_graphql_api_id, image_id, inventory_quantity, inventory_management, inventory_policy, price, compare_at_price, sale_percent, availability, } = variant; let optionAttributes = { "data-option-1": stringToSlug(option1), "data-option-2": option2 ? stringToSlug(option2) : "", "data-inventory-quantity": inventory_quantity, "data-inventory-management": inventory_management, "data-inventory-policy": inventory_policy, "data-position": position, "data-id": id, "data-graphql-id": admin_graphql_api_id, "data-image-id": image_id, "data-price": price, "data-compare-at-price": compare_at_price, "data-sale-percent": sale_percent, "data-availability": availability, "data-product-handle": handle, }; return `<option ${Object.keys(optionAttributes).reduce( (acc, item) => acc + `${item}="${optionAttributes[item]}"`, "" )}>${title}</option>`; }) ?.join?.("") || ""; const colorOptionsHtml = options ?.filter((option) => option?.[0] === "Color") ?.map?.((option) => { const optionName = option?.[0]; const optionValues = option?.[1]; const optionIndex = options.findIndex((item) => item[0] === optionName) + 1; const initialVisibleCount = 2; const hiddenOptionsCount = optionValues.length - initialVisibleCount; const showMoreText = `${hiddenOptionsCount}+`; const selectedOption = defaultSelected[`data-selected-option${optionIndex}`]; const showMoreButton = ` <a class="sledge-custom__product-card-link ${ optionName === "Color" ? "sledge__product-variant-color-swatch" : "sledge__product-variant-size-swatch" }" href="${productUrl}" data-show-more > ${showMoreText} </a> `; if (optionValues[0] === "Default Title") return ""; const colorButtonsHtml = optionValues ?.slice(0, initialVisibleCount) ?.map?.((item, index) => { const defaultOptionClass = `${ selectedOption === item ? "sledge__product-variant-size-swatch-active" : "" } sledge__product-variant-size-swatch`; const colorOptionClass = `${ selectedOption === item ? "sledge__product-variant-color-swatch-active" : "" } sledge__product-variant-color-swatch`; const colorDataSettings = instantSearchSettings?.colors || []; const getColorSwatch = colorDataSettings?.filter?.(({ name }) => name === item)[0] || {}; const colorSwatch = getColorSwatch?.image ? `url(${getColorSwatch?.image})` : getColorSwatch?.rgb || item; return ` <button type="button" class="${ optionName === "Color" ? colorOptionClass : defaultOptionClass }" style="${ optionName === "Color" ? ` background: ${colorSwatch}; background-size: contain; ` : "" }" onclick="setSelectedVariant({ id: '${id}', element: event, value: '${item}', optionIndex: ${optionIndex}, sourceApp: '${sourceApp}' }); setSelectedOption({ element: event, optionName: '${optionName}' })" title="${item}" > ${optionName === "Color" ? "" : item} </button>`; }) ?.join?.("") || ""; return ` <div class="sledge__product-variant-size-swatch-flex options-button-${stringToSlug( optionName )}"> ${colorButtonsHtml} ${hiddenOptionsCount > 0 ? showMoreButton : ""} </div>`; }) ?.join?.("") || ""; const otherOptionsHtml = options ?.filter((option) => option?.[0] !== "Color") ?.map?.((option) => { const optionName = option?.[0]; const optionValues = option?.[1]; const optionIndex = options.findIndex((item) => item[0] === optionName) + 1; if (optionValues[0] === "Default Title") return ""; const defaultValue = defaultSelected[`data-selected-option${optionIndex}`]; const optionItemsHtml = optionValues .map( (item, index) => `<option ${ item == defaultValue ? "selected" : "" } value="${item}">${item}</option>` ) ?.join?.("") || ""; return `<select class="sledge__product-variant-size-swatch-select options-button-${stringToSlug( optionName )}" data-option-name="${optionName}" onchange="setSelectedVariant({ id: '${id}', element: event, value: event.target.value, optionIndex: ${optionIndex}, sourceApp: '${sourceApp}' });"> ${optionItemsHtml} </select>`; }) ?.join?.("") || ""; return ` <div class="card-wrapper product-card-wrapper underline-links-hover"> <div class=" card card--card card--media color-scheme-1 gradient " style="--ratio-percent: 125.0%;"> <div class="card__inner ratio" style="--ratio-percent: 125.0%;"> <div class="card__media"> <div class="media media--transparent media--hover-effect"> <img src="${productImageUrl}" srcset="${productImageUrl}" class="motion-reduce width="635" height="560" alt="${title || "Product Image"}" loading="lazy" > ${ secondaryProductImageUrl ? ` <img srcset="${secondaryProductImageUrl}" src="${secondaryProductImageUrl}" class="motion-reduce" loading="lazy" width="635" height="560" alt="${title || " Product Secondary Image"}" /> ` : "" } </div> </div> <div class="card__content"> <div class="card__information"> <h3 class="card__heading"> <a href="${productUrl}" class="full-unstyled-link" > ${title} </a> </h3> </div> <div class="card__badge top left"></div> </div> </div> <div class="card__content"> <div class="card__information"> <h3 class="card__heading h5" > <a href="${productUrl}" class="full-unstyled-link" > ${title} </a> </h3> <div class="card-information"> <span class="caption-large light"></span> <div class=" price "> <div class="price__container"> <div class="price__regular"> <span class="visually-hidden visually-hidden--inline">Regular price</span> <span class="price-item price-item--regular"> ${ window?.sledge?.shopifyFormatMoney?.(price) || price }</span> </div> ${ isOnSale ? `<div class="price__sale"> <span class="visually-hidden visually-hidden--inline">Regular price</span> <span> <s class="price-item price-item--regular"></s> </span> <span class="visually-hidden visually-hidden--inline">Sale price</span> <span class="price-item price-item--sale price-item--last"> ${ window?.sledge?.shopifyFormatMoney?.( compare_at_price ) || compare_at_price } </span> </div>` : "" } <small class="unit-price caption hidden"> <span class="visually-hidden">Unit price</span> <span class="price-item price-item--last"> <span></span> <span aria-hidden="true">/</span> <span class="visually-hidden"> per </span> <span></span> </span> </small> </div> </div> </div> </div> <div class="quick-add no-js-hidden"> <product-form data-section-id="template--18666325246107__product-grid"> <form method="post" action="/cart/add" id="product_form_${id}" accept-charset="UTF-8"= enctype="multipart/form-data" is="product-form" onsubmit="addToCartTrigger({ sourceApp: '${sourceApp}', productId: '${id}' })" > <input type="hidden" name="form_type" value="product" /> <input type="hidden" name="utf8" value="✓" /> <input type="hidden" name="id" value="${variant_id}" /> <input type="hidden" name="product-id" value="${id}"> <button id="quick-add-template--18666325246107__product-grid8471445733531-submit" type="submit" name="add" class="quick-add__submit button button--full-width button--secondary" aria-haspopup="dialog" aria-labelledby="quick-add-template--18666325246107__product-grid8471445733531-submit title-template--18666325246107__product-grid-8471445733531" aria-live="polite" data-sold-out-message="true"> <span>Add to cart </span> <span class="sold-out-message hidden"> Sold out </span> <div class="loading__spinner hidden"> <svg xmlns="http://www.w3.org/2000/svg" class="spinner" viewBox="0 0 66 66"> <circle stroke-width="6" cx="33" cy="33" r="30" fill="none" class="path"></circle> </svg> </div> </button> </form> </product-form> </div> <div class="card__badge top left"></div> </div> </div> </div> `; }