${
- item.productVariant.compare_at_price ?
+ item.productVariant.compare_at_price ?
`
${item.productVariant.compare_at_price}` : ''
}
@@ -174,9 +196,9 @@ function cartTemplate(item) {
@@ -220,7 +242,7 @@ async function updateCartDrawer() {
if (item.productVariant.compare_at_price) {
const usePrecision = shouldUsePrecision(item.productVariant.compare_at_price);
item.productVariant.compare_at_price = formatCurrency(
- item.productVariant.compare_at_price,
+ item.productVariant.compare_at_price,
currencyCode,
customerLocale,
);
@@ -348,13 +370,22 @@ function preventCartDrawerOpening(templateName) {
window.location.reload();
}
-async function directAddToCart(event, productId) {
+async function directAddToCart(event, productId, inventory, isTrackingInventory) {
event.preventDefault();
+ await trackVariantQuantityOnCart(productId);
+ const variantQuantityInCart = parseInt(document.querySelector('#cartQuantity')?.value) || null;
+ const isTrackingInventoryAvailable = Boolean(isTrackingInventory) && Number.isFinite(inventory);
+ const newQuantity = 1;
+
+ if (isTrackingInventoryAvailable && ((variantQuantityInCart ?? 0) + newQuantity) > inventory) {
+ return notify(ADD_TO_CART_EXPECTED_ERRORS.max_quantity + inventory, 'warning');
+ }
+
try {
const response = await youcanjs.cart.addItem({
productVariantId: productId,
- quantity: 1
+ quantity: newQuantity
});
if (response.error) throw new Error(response.error);
diff --git a/themes/harmony/assets/cart.js b/themes/harmony/assets/cart.js
index 9095283a..f5e92d6b 100644
--- a/themes/harmony/assets/cart.js
+++ b/themes/harmony/assets/cart.js
@@ -90,23 +90,22 @@ async function removeCoupons(e) {
}
}
-function updateCart(item, quantity, totalPriceSelector, cartItemId, productVariantId) {
+function updateCart(item, quantity, totalPriceSelector, cartItemId, productVariantId, inventory) {
const inputHolder = document.getElementById(item);
const input = inputHolder.querySelector(`input[id="${productVariantId}"]`);
input.value = quantity;
const decrease = input.previousElementSibling;
const increase = input.nextElementSibling;
-
const productPrice = inputHolder.querySelector('.product-price');
const price = productPrice.innerText;
const totalPrice = inputHolder.querySelector(totalPriceSelector);
decrease
.querySelector('button')
- .setAttribute('onclick', `decreaseQuantity('${cartItemId}', '${productVariantId}', '${Number(quantity) - 1}')`);
+ .setAttribute('onclick', `decreaseQuantity('${cartItemId}', '${productVariantId}', '${Number(quantity) - 1}', ${inventory})`);
increase
.querySelector('button')
- .setAttribute('onclick', `increaseQuantity('${cartItemId}', '${productVariantId}', '${Number(quantity) + 1}')`);
+ .setAttribute('onclick', `increaseQuantity('${cartItemId}', '${productVariantId}', '${quantity}', ${inventory})`);
if (isNaN(quantity)) {
totalPrice.innerText = 0;
@@ -143,16 +142,16 @@ document.addEventListener('DOMContentLoaded', () => {
fetchCoupons();
});
-function updateDOM(cartItemId, productVariantId, quantity) {
- updateCart(cartItemId, quantity, '.total-price', cartItemId, productVariantId);
+function updateDOM(cartItemId, productVariantId, quantity, inventory) {
+ updateCart(cartItemId, quantity, '.total-price', cartItemId, productVariantId, inventory);
updateTotalPrice();
}
-function updatePrice(cartItemUniqueId, productVariantId, quantity) {
- updateCart(`cart-item-${cartItemUniqueId}`, quantity, '.item-price', cartItemUniqueId, productVariantId);
+function updatePrice(cartItemUniqueId, productVariantId, quantity, inventory) {
+ updateCart(`cart-item-${cartItemUniqueId}`, quantity, '.item-price', cartItemUniqueId, productVariantId, inventory);
}
-async function updateQuantity(cartItemId, productVariantId, quantity) {
+async function updateQuantity(cartItemId, productVariantId, quantity, inventory) {
load(`#loading__${cartItemId}`);
try {
await youcanjs.cart.updateItem({ cartItemId, productVariantId, quantity });
@@ -162,28 +161,32 @@ async function updateQuantity(cartItemId, productVariantId, quantity) {
stopLoad(`#loading__${cartItemId}`);
}
- updateDOM(cartItemId, productVariantId, quantity);
- updatePrice(cartItemId,productVariantId,quantity);
+ updateDOM(cartItemId, productVariantId, quantity, inventory);
+ updatePrice(cartItemId,productVariantId,quantity, inventory);
updateTotalPrice();
}
-async function updateOnchange(cartItemId, productVariantId) {
+async function updateOnchange(cartItemId, productVariantId, inventory) {
const inputHolder = document.getElementById(cartItemId);
const input = inputHolder.querySelector(`input[id="${productVariantId}"]`);
const quantity = input.value;
- await updateQuantity(cartItemId, productVariantId, quantity);
+ await updateQuantity(cartItemId, productVariantId, quantity, inventory);
}
-async function decreaseQuantity(cartItemId, productVariantId, quantity) {
- if (quantity < 1) {
+async function decreaseQuantity(cartItemId, productVariantId, quantity, inventory) {
+ if (Number(quantity) < 1) {
return;
}
- await updateQuantity(cartItemId, productVariantId, quantity);
+ await updateQuantity(cartItemId, productVariantId, quantity, inventory);
}
-async function increaseQuantity(cartItemId, productVariantId, quantity) {
- await updateQuantity(cartItemId, productVariantId, quantity);
+async function increaseQuantity(cartItemId, productVariantId, quantity, inventory) {
+ if (Number.isFinite(inventory) && (Number(quantity) >= inventory)) {
+ return notify(ADD_TO_CART_EXPECTED_ERRORS.max_quantity + inventory, 'warning');
+ }
+
+ await updateQuantity(cartItemId, productVariantId, (Number(quantity) + 1), inventory);
}
function updateCartItemCount(count) {
diff --git a/themes/harmony/assets/dropdown-menu.js b/themes/harmony/assets/dropdown-menu.js
index 63d7f9f7..81a3b7b1 100644
--- a/themes/harmony/assets/dropdown-menu.js
+++ b/themes/harmony/assets/dropdown-menu.js
@@ -1,8 +1,10 @@
+let isDropDownOpened = false;
/**
* @param {HTMLElement} element
*/
function showDropDownMenu(element) {
element.classList.toggle('show');
+ isDropDownOpened = true;
}
/**
@@ -12,6 +14,7 @@ function showDropDownMenu(element) {
function hideDropDownMenu(element, event) {
if (!event.target.matches('.dropbtn, .dropbtn *')) {
element?.classList.remove('show');
+ isDropDownOpened = false;
}
}
@@ -43,7 +46,11 @@ function dropdownMenu() {
const dropdownContent = dropDownInput.querySelector('.dropdown-content');
selectInput.addEventListener('click', () => showDropDownMenu(dropdownContent));
- window.addEventListener('click', (event) => hideDropDownMenu(dropdownContent, event));
+ document.addEventListener('click', (event) => {
+ if(isDropDownOpened) {
+ hideDropDownMenu(dropdownContent, event);
+ }
+ });
const selectOptions = dropdownContent.querySelectorAll('li');
@@ -62,5 +69,4 @@ function dropdownMenu() {
});
});
}
-
dropdownMenu();
diff --git a/themes/harmony/assets/express-checkout.css b/themes/harmony/assets/express-checkout.css
index 6ea68553..1a54f0eb 100644
--- a/themes/harmony/assets/express-checkout.css
+++ b/themes/harmony/assets/express-checkout.css
@@ -94,6 +94,10 @@
font-family:inherit;
font-weight:bold;
}
+.express-checkout-button:disabled{
+ opacity:0.5;
+ cursor:not-allowed;
+}
.express-checkout-button span{
font-family:inherit;
font-weight:inherit;
@@ -215,11 +219,6 @@
}
}
-.sticky-checkout-placeholder .express-checkout-button:disabled{
- opacity:0.5;
- cursor:not-allowed;
-}
-
.sticky-btn-placehodler{
opacity:0.5;
cursor:not-allowed;
diff --git a/themes/harmony/assets/express-checkout.js b/themes/harmony/assets/express-checkout.js
index df46fe8c..a2af7a7e 100644
--- a/themes/harmony/assets/express-checkout.js
+++ b/themes/harmony/assets/express-checkout.js
@@ -30,7 +30,6 @@ async function placeOrder() {
const formField = form.querySelector(`[name="${fieldName}"]`);
const errorEl = form.querySelector(`.validation-error[data-error="${fieldName}"]`);
if (formField) {
- console.log(formField);
formField.classList.add('error');
}
diff --git a/themes/harmony/assets/main.css b/themes/harmony/assets/main.css
index 7dd589e6..5ec99088 100644
--- a/themes/harmony/assets/main.css
+++ b/themes/harmony/assets/main.css
@@ -504,6 +504,9 @@ textarea{
.yc-alert.error{
background-color:var(--yc-error-color);
}
+.yc-alert.warning{
+ background-color:var(--yc-warning-color);
+}
.yc-alert.show{
opacity:1;
z-index:9999;
diff --git a/themes/harmony/assets/main.js b/themes/harmony/assets/main.js
index 2b4a6dfc..024d55a9 100644
--- a/themes/harmony/assets/main.js
+++ b/themes/harmony/assets/main.js
@@ -34,12 +34,22 @@ if (fixedNavbar && notice) {
/* ----- spinner-loader ----- */
/* -------------------------- */
function load(el) {
- const loader = document.querySelector(el);
- if (loader) {
- loader.classList.remove('hidden');
+ const element = document.querySelector(el);
+
+ if (!element) {
+ return;
+ }
+
+ if (element) {
+ element.classList.remove('hidden');
+ }
+
+ if (element.parentElement.hasAttribute('data-type')) {
+ element.parentElement.setAttribute('data-type', 'loading');
+ element.parentElement.disabled = true;
}
- const nextEl = loader ? loader.nextElementSibling : null;
+ const nextEl = element ? element.nextElementSibling : null;
if (nextEl) {
nextEl.classList.add('hidden');
@@ -47,12 +57,22 @@ function load(el) {
}
function stopLoad(el) {
- const loader = document.querySelector(el);
- if (loader) {
- loader.classList.add('hidden');
+ const element = document.querySelector(el);
+
+ if (!element) {
+ return;
+ }
+
+ if (element) {
+ element.classList.add('hidden');
+ }
+
+ if (element.parentElement.hasAttribute('data-type')) {
+ element.parentElement.setAttribute('data-type', '');
+ element.parentElement.disabled = false;
}
- const nextEl = loader ? loader.nextElementSibling : null;
+ const nextEl = element ? element.nextElementSibling : null;
if (nextEl) {
nextEl.classList.remove('hidden');
}
@@ -330,7 +350,7 @@ function formatCurrency(amount, currencySymbol, locale = 'en-US') {
const parts = determineSymbolPositionFormatter.formatToParts(1); // format with 1 USD just to determine the position of the currency symbol
const symbolIndex = parts.findIndex(part => part.type === 'currency');
-
+
return symbolIndex === 0
? `${currencySymbol} ${formattedValue}`
: `${formattedValue} ${currencySymbol}`;
@@ -346,3 +366,60 @@ function shouldUsePrecision(amount) {
return isMulticurrencyActive && usePrecision;
}
+
+/**
+ * Restrict the input value based on the inventory number
+ *
+ * @param {HTMLInputElement} inputElement - The input element.
+ * @param {number} maxInventoryValue - The maximum allowable inventory value.
+ */
+function restrictInputValue(inputElement, maxInventoryValue) {
+
+ if (maxInventoryValue === null) {
+ return;
+ }
+
+ let currentValue = parseInt(inputElement.value);
+
+ if (currentValue < 1) {
+ inputElement.value = 1;
+ }
+
+ if (currentValue > maxInventoryValue) {
+ inputElement.value = maxInventoryValue;
+ }
+}
+
+/**
+ * Tracks the quantity of a specific variant in the cart and set it in the hidden quantity input.
+ *
+ * @param {string} selectedVariantId - The ID of the selected product variant.
+ */
+async function trackVariantQuantityOnCart(selectedVariantId) {
+ try {
+ load('#loading__cart');
+ const cartQuantityInput = document.querySelector('#cartQuantity');
+ cartQuantityInput.value = 0;
+ const cart = await youcanjs.cart.fetch();
+
+ if (!cart) {
+ return;
+ }
+
+ if (cart.items.length === 0 || cart.items.data?.length === 0) {
+ return;
+ }
+
+ const cartItem = cart.items.find((item) => item.productVariant.id === selectedVariantId);
+
+ if (!cartItem || cartItem.productVariant.product.track_inventory === false) {
+ return;
+ }
+
+ cartQuantityInput.value = cartItem.quantity;
+ } catch(e) {
+ notify(e.message, 'error');
+ } finally {
+ stopLoad('#loading__cart');
+ }
+}
diff --git a/themes/harmony/assets/product-quantity-input.js b/themes/harmony/assets/product-quantity-input.js
index 552caf8c..8206c4f1 100644
--- a/themes/harmony/assets/product-quantity-input.js
+++ b/themes/harmony/assets/product-quantity-input.js
@@ -5,6 +5,7 @@ function manipulateQuantity() {
const decrementButton = $('.decrement-button');
const incrementButton = $('.increment-button');
const quantityInput = $('.quantity-input');
+ const inventoryInput = $('#_inventory');
/**
* Decreases quantity value by 1 when decrement button is clicked
@@ -21,8 +22,22 @@ function manipulateQuantity() {
*/
incrementButton?.addEventListener('click', () => {
const currentValue = parseInt(quantityInput.value);
+ const inventory = parseInt(inventoryInput.value);
+
+ if(Number.isFinite(inventory) && currentValue >= inventory) {
+ return notify(ADD_TO_CART_EXPECTED_ERRORS.max_quantity + inventory, 'warning');
+ }
+
quantityInput.value = currentValue + 1;
});
+
+ /**
+ * Check if the current value exceeds the max inventory
+ */
+ quantityInput?.addEventListener('input', () => {
+ const maxInventoryValue = parseInt(inventoryInput.value);
+ restrictInputValue(quantityInput, maxInventoryValue);
+ });
}
manipulateQuantity();
diff --git a/themes/harmony/assets/product.js b/themes/harmony/assets/product.js
index 2343e4f5..7cc5db3c 100644
--- a/themes/harmony/assets/product.js
+++ b/themes/harmony/assets/product.js
@@ -171,48 +171,70 @@ function setVariant(parentSection, id) {
}
/**
- * Sets inventory of product variant.
- * And disable add to cart button when inventory is not sufficient.
+ * Disable action buttons (express checkout button / add to cart button) if stock is out.
*
- * @param {HTMLElement} parentSection
- * @param {Number} inventory
+ * @param {HTMLElement} el
+ * @param {Boolean} isStockOut
*/
-function setInventory(parentSection, inventory) {
- const inventoryInput = parentSection.querySelector('#_inventory');
-
- inventoryInput.value = globalProduct.isTrackingInventory ? inventory : null;
-
- /** @type {HTMLButtonElement} addToCartButton */
- const addToCartButton = parentSection.querySelector('.yc-btn');
+function disableActionButtons(el, isStockOut) {
+ const elements = document.querySelectorAll(el);
- if (!addToCartButton) {
+ if(!elements.length) {
return;
}
- if (!addToCartButton.disabled && addToCartButton.getAttribute('data-text') === null) {
- addToCartButton.setAttribute('data-text', addToCartButton.innerHTML);
- }
+ elements.forEach((element) => {
+ if (!element.disabled && element.getAttribute('data-text') === null) {
+ element.setAttribute('data-text', element.innerHTML);
+ }
- const isAddToCartDisabled = globalProduct.isTrackingInventory && inventory === 0;
+ element.disabled = isStockOut;
- addToCartButton.disabled = isAddToCartDisabled;
+ if (isStockOut) {
+ element.innerHTML = TRANSLATED_TEXT.empty_inventory;
+ } else {
+ element.innerHTML = element.getAttribute('data-text');
+ }
+ });
+}
- if (isAddToCartDisabled) {
- addToCartButton.innerHTML = TRANSLATED_TEXT.empty_inventory;
- } else {
- addToCartButton.innerHTML = addToCartButton.getAttribute('data-text');
- }
+/**
+ * Force reset the quantity input if the variant is changed.
+ *
+ * @param {HTMLElement} parentSection
+ */
+function forceResetQuantityInput(parentSection) {
+ const quantityInput = parentSection.querySelector('.quantity-input');
+
+ quantityInput.value = 1;
+}
+
+/**
+ * Sets inventory of product variant and disable action buttons if the inventory is not sufficient (out of stock).
+ *
+ * @param {HTMLElement} parentSection
+ * @param {Number} inventory
+ */
+function setInventory(parentSection, inventory) {
+ const inventoryInput = parentSection.querySelector('#_inventory');
+ const isStockOut = globalProduct.isTrackingInventory && inventory === 0;
+
+ inventoryInput.value = globalProduct.isTrackingInventory ? inventory : null;
+ disableActionButtons('.add-to-cart-button', isStockOut);
+ disableActionButtons('.express-checkout-button', isStockOut);
+ forceResetQuantityInput(parentSection);
}
/**
* Sets default options for a product
* @param {HTMLElement} parentSection
*/
-function selectDefaultOptions(parentSection) {
+async function selectDefaultOptions(parentSection) {
const options = parentSection.querySelectorAll('.product-options > div');
if (!options || !options.length) {
setInventory(parentSection, defaultVariant?.inventory);
+ await trackVariantQuantityOnCart(defaultVariant?.id);
return setVariant(parentSection, defaultVariant?.id);
}
@@ -577,7 +599,7 @@ function goToCheckoutStep(close = false) {
function setup() {
const singleProductSections = document.querySelectorAll('.yc-single-product');
- if (!singleProductSections) return;
+ if (!singleProductSections || typeof defaultVariant === 'undefined' ) return;
singleProductSections.forEach((section) => {
const productDetails = section.querySelector('.product-options');
@@ -591,7 +613,7 @@ function setup() {
);
if (productDetails) {
- const observer = new MutationObserver(() => {
+ const observer = new MutationObserver(async () => {
const selectedVariant = getSelectedVariant(section);
const variantIdInput = section.querySelector('#variantId');
variantIdInput.value = selectedVariant.id;
@@ -604,6 +626,7 @@ function setup() {
);
setInventory(section, selectedVariant.inventory);
+ await trackVariantQuantityOnCart(selectedVariant.id);
});
observer.observe(productDetails, {
diff --git a/themes/harmony/locales/ar.default.json b/themes/harmony/locales/ar.default.json
index a2013f4c..9d21fa31 100644
--- a/themes/harmony/locales/ar.default.json
+++ b/themes/harmony/locales/ar.default.json
@@ -153,7 +153,8 @@
"quantity_smaller_than_zero": "يجب أن تكون الكمية أكبر من 0",
"upload_image": "الرجاء تحميل صورة",
"product_added": "تمت إضافة المنتج بنجاح",
- "empty_inventory": "المُنتج غير متوفر حاليًا في المخزون"
+ "empty_inventory": "المُنتج غير متوفر حاليًا في المخزون",
+ "max_quantity": "الكمية المتاحة لهذا العرض هي: "
},
"page": {
"contact": {
diff --git a/themes/harmony/locales/en.json b/themes/harmony/locales/en.json
index 37e933c2..3fba71eb 100644
--- a/themes/harmony/locales/en.json
+++ b/themes/harmony/locales/en.json
@@ -153,7 +153,8 @@
"quantity_smaller_than_zero": "Quantity must be greater than 0",
"upload_image": "Please upload an image",
"product_added": "Product has been added successfully",
- "empty_inventory": "Product out of stock"
+ "empty_inventory": "Product out of stock",
+ "max_quantity": "The available quantity for this variant is: "
},
"page": {
"contact": {
diff --git a/themes/harmony/locales/fr.json b/themes/harmony/locales/fr.json
index b1e3e400..dd9862aa 100644
--- a/themes/harmony/locales/fr.json
+++ b/themes/harmony/locales/fr.json
@@ -153,7 +153,8 @@
"quantity_smaller_than_zero": "La quantité doit être supérieure à 0",
"upload_image": "Veuillez télécharger une image",
"product_added": "Produit ajouté avec succès",
- "empty_inventory": "Produit en rupture de stock"
+ "empty_inventory": "Produit en rupture de stock",
+ "max_quantity": "La quantité disponible pour cette variante est : "
},
"page": {
"contact": {
diff --git a/themes/harmony/snippets/add-to-cart.liquid b/themes/harmony/snippets/add-to-cart.liquid
index 8831487f..221922eb 100644
--- a/themes/harmony/snippets/add-to-cart.liquid
+++ b/themes/harmony/snippets/add-to-cart.liquid
@@ -21,6 +21,8 @@
diff --git a/themes/harmony/snippets/cart-drawer.liquid b/themes/harmony/snippets/cart-drawer.liquid
index 74abd571..0c7aefac 100644
--- a/themes/harmony/snippets/cart-drawer.liquid
+++ b/themes/harmony/snippets/cart-drawer.liquid
@@ -1,5 +1,6 @@
{{ 'cart-drawer.css' | asset_url | stylesheet_tag }}
+
{% if settings.direct_add_to_cart and item.variants.size <= 1 %}
-