Add Shopify variant options selector with circular radio buttons
All checks were successful
Deploy / Deploy to Production (push) Successful in 7s
All checks were successful
Deploy / Deploy to Production (push) Successful in 7s
- Implement dynamic option rendering from Shopify variant data - Generate circular radio buttons for product variants (sizes, colors, etc.) - Disable add-to-cart button until option is selected - Display "Choisissez une option" text when option required - Update button text and enable on option selection - Add is-selected class to chosen option - Handle disabled state for out-of-stock variants - Restore btn__default button style with icon and text structure - Add chooseOption translation key in FR/EN Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
0c8cc5000c
commit
4987c4830f
5 changed files with 109 additions and 69 deletions
|
|
@ -55,9 +55,8 @@
|
|||
renderPrice(product);
|
||||
renderDetails(product, isEnglish);
|
||||
renderImages(product, isEnglish);
|
||||
renderVariants(product);
|
||||
renderOptions(product);
|
||||
setupAddToCart(product);
|
||||
renderStock(product);
|
||||
}
|
||||
|
||||
function renderTitle(product, isEnglish) {
|
||||
|
|
@ -113,42 +112,78 @@
|
|||
}
|
||||
}
|
||||
|
||||
function renderVariants(product) {
|
||||
function renderOptions(product) {
|
||||
if (product.variants.edges.length <= 1) return;
|
||||
|
||||
const variantsContainer = document.querySelector("[data-product-variants]");
|
||||
const variantSelector = document.querySelector("[data-variant-selector]");
|
||||
const firstVariant = product.variants.edges[0].node;
|
||||
if (!firstVariant.selectedOptions || firstVariant.selectedOptions.length === 0) return;
|
||||
|
||||
if (!variantsContainer || !variantSelector) return;
|
||||
const mainOption = firstVariant.selectedOptions[0];
|
||||
const optionValues = new Set();
|
||||
|
||||
variantsContainer.style.display = "block";
|
||||
|
||||
variantSelector.innerHTML = product.variants.edges
|
||||
.map((edge) => {
|
||||
const variant = edge.node;
|
||||
const variantId = variant.id.replace(
|
||||
"gid://shopify/ProductVariant/",
|
||||
""
|
||||
);
|
||||
const price = parseFloat(variant.price.amount).toFixed(2) + "€";
|
||||
const availability = variant.availableForSale
|
||||
? ""
|
||||
: " (Rupture de stock)";
|
||||
|
||||
return `<option value="${variantId}" ${
|
||||
!variant.availableForSale ? "disabled" : ""
|
||||
}>
|
||||
${variant.title} - ${price}${availability}
|
||||
</option>`;
|
||||
})
|
||||
.join("");
|
||||
|
||||
variantSelector.addEventListener("change", (e) => {
|
||||
const addToCartBtn = document.querySelector("[data-shopify-add-to-cart]");
|
||||
if (addToCartBtn) {
|
||||
addToCartBtn.dataset.variantId = e.target.value;
|
||||
product.variants.edges.forEach(edge => {
|
||||
const variant = edge.node;
|
||||
if (variant.selectedOptions && variant.selectedOptions[0]) {
|
||||
optionValues.add(variant.selectedOptions[0].value);
|
||||
}
|
||||
});
|
||||
|
||||
if (optionValues.size <= 1) return;
|
||||
|
||||
const optionsContainer = document.querySelector("[data-product-options]");
|
||||
const optionsList = document.querySelector("[data-product-options-list]");
|
||||
|
||||
if (!optionsContainer || !optionsList) return;
|
||||
|
||||
const optionName = mainOption.name;
|
||||
const optionSlug = optionName.toLowerCase().replace(/\s+/g, '-');
|
||||
|
||||
optionsList.innerHTML = Array.from(optionValues).map((value) => {
|
||||
const uniqueId = `${optionSlug}-${value.toLowerCase().replace(/\s+/g, '-')}`;
|
||||
const variant = product.variants.edges.find(e =>
|
||||
e.node.selectedOptions && e.node.selectedOptions[0]?.value === value
|
||||
)?.node;
|
||||
const isAvailable = variant?.availableForSale || false;
|
||||
|
||||
return `
|
||||
<li>
|
||||
<input
|
||||
type="radio"
|
||||
id="${uniqueId}"
|
||||
name="${optionSlug}"
|
||||
value="${value}"
|
||||
data-variant-id="${variant ? variant.id.replace('gid://shopify/ProductVariant/', '') : ''}"
|
||||
${!isAvailable ? 'disabled' : ''}
|
||||
/>
|
||||
<label for="${uniqueId}">${value}</label>
|
||||
</li>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
optionsContainer.style.display = 'block';
|
||||
|
||||
const radios = optionsList.querySelectorAll('input[type="radio"]');
|
||||
const addToCartBtn = document.querySelector("[data-shopify-add-to-cart]");
|
||||
const buttonText = addToCartBtn?.querySelector('[data-button-text]');
|
||||
|
||||
radios.forEach(radio => {
|
||||
radio.addEventListener('change', function() {
|
||||
const variantId = this.dataset.variantId;
|
||||
|
||||
if (addToCartBtn) {
|
||||
addToCartBtn.dataset.variantId = variantId;
|
||||
addToCartBtn.removeAttribute('disabled');
|
||||
}
|
||||
|
||||
if (buttonText) {
|
||||
buttonText.textContent = addToCartBtn.dataset.defaultText || 'Ajouter au panier';
|
||||
}
|
||||
|
||||
const allLi = optionsList.querySelectorAll('li');
|
||||
allLi.forEach(li => li.classList.remove('is-selected'));
|
||||
this.closest('li').classList.add('is-selected');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function setupAddToCart(product) {
|
||||
|
|
@ -158,31 +193,35 @@
|
|||
const productId = product.id.replace("gid://shopify/Product/", "");
|
||||
addToCartBtn.dataset.productId = productId;
|
||||
|
||||
const firstAvailableVariant = product.variants.edges.find(
|
||||
(e) => e.node.availableForSale
|
||||
);
|
||||
if (firstAvailableVariant) {
|
||||
const variantId = firstAvailableVariant.node.id.replace(
|
||||
"gid://shopify/ProductVariant/",
|
||||
""
|
||||
);
|
||||
addToCartBtn.dataset.variantId = variantId;
|
||||
}
|
||||
}
|
||||
const hasMultipleVariants = product.variants.edges.length > 1;
|
||||
const firstVariant = product.variants.edges[0]?.node;
|
||||
const hasOptions = firstVariant?.selectedOptions && firstVariant.selectedOptions.length > 0;
|
||||
|
||||
function renderStock(product) {
|
||||
const stockEl = document.querySelector("[data-product-stock]");
|
||||
if (!stockEl) return;
|
||||
const uniqueOptions = new Set();
|
||||
product.variants.edges.forEach(edge => {
|
||||
if (edge.node.selectedOptions && edge.node.selectedOptions[0]) {
|
||||
uniqueOptions.add(edge.node.selectedOptions[0].value);
|
||||
}
|
||||
});
|
||||
const hasMultipleOptions = uniqueOptions.size > 1;
|
||||
|
||||
const addToCartBtn = document.querySelector("[data-shopify-add-to-cart]");
|
||||
|
||||
if (product.availableForSale) {
|
||||
stockEl.textContent = addToCartBtn?.dataset.textInStock || "En stock";
|
||||
stockEl.classList.add("in-stock");
|
||||
if (hasMultipleVariants && hasOptions && hasMultipleOptions) {
|
||||
addToCartBtn.setAttribute('disabled', 'disabled');
|
||||
const buttonText = addToCartBtn.querySelector('[data-button-text]');
|
||||
if (buttonText) {
|
||||
buttonText.textContent = addToCartBtn.dataset.textChooseOption || 'Choisissez une option';
|
||||
}
|
||||
} else {
|
||||
stockEl.textContent =
|
||||
addToCartBtn?.dataset.textOutOfStock || "Rupture de stock";
|
||||
stockEl.classList.add("out-of-stock");
|
||||
const firstAvailableVariant = product.variants.edges.find(
|
||||
(e) => e.node.availableForSale
|
||||
);
|
||||
if (firstAvailableVariant) {
|
||||
const variantId = firstAvailableVariant.node.id.replace(
|
||||
"gid://shopify/ProductVariant/",
|
||||
""
|
||||
);
|
||||
addToCartBtn.dataset.variantId = variantId;
|
||||
}
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ return [
|
|||
'loading' => 'Loading...',
|
||||
'productNotFound' => 'Product not found',
|
||||
'selectVariant' => 'Select',
|
||||
'chooseOption' => 'Choose an option',
|
||||
|
||||
// Blueprints - Home
|
||||
'home.title' => 'Home',
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ return [
|
|||
'loading' => 'Chargement...',
|
||||
'productNotFound' => 'Produit non trouvé',
|
||||
'selectVariant' => 'Choisir',
|
||||
'chooseOption' => 'Choisissez une option',
|
||||
|
||||
// Blueprints - Home
|
||||
'home.title' => 'Accueil',
|
||||
|
|
|
|||
|
|
@ -1,25 +1,20 @@
|
|||
<div class="product-purchase">
|
||||
<div class="product-stock-info">
|
||||
<p data-product-stock class="stock-status"></p>
|
||||
</div>
|
||||
|
||||
<div class="product-variants" data-product-variants style="display: none;">
|
||||
<label for="variant-select"><?= t('selectVariant') ?></label>
|
||||
<select id="variant-select" data-variant-selector></select>
|
||||
</div>
|
||||
|
||||
<div class="add-to-cart">
|
||||
<button
|
||||
class="btn-add-to-cart"
|
||||
class="btn__default"
|
||||
data-shopify-add-to-cart
|
||||
data-product-id=""
|
||||
data-variant-id=""
|
||||
data-text-add="<?= t('addToCart') ?>"
|
||||
data-default-text="<?= t('addToCart') ?>"
|
||||
data-text-choose-option="<?= t('chooseOption') ?>"
|
||||
data-text-adding="<?= t('addingToCart') ?>"
|
||||
data-text-added="<?= t('addedToCart') ?>"
|
||||
data-text-error="<?= t('errorAddToCart') ?>"
|
||||
data-text-out-of-stock="<?= t('outOfStock') ?>"
|
||||
data-text-in-stock="<?= t('inStock') ?>"
|
||||
>
|
||||
<?= t('addToCart') ?>
|
||||
<span class="icon">
|
||||
<svg clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="2" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="m14.523 18.787s4.501-4.505 6.255-6.26c.146-.146.219-.338.219-.53s-.073-.383-.219-.53c-1.753-1.754-6.255-6.258-6.255-6.258-.144-.145-.334-.217-.524-.217-.193 0-.385.074-.532.221-.293.292-.295.766-.004 1.056l4.978 4.978h-14.692c-.414 0-.75.336-.75.75s.336.75.75.75h14.692l-4.979 4.979c-.289.289-.286.762.006 1.054.148.148.341.222.533.222.19 0 .378-.072.522-.215z" fill-rule="nonzero" />
|
||||
</svg>
|
||||
</span>
|
||||
<div class="txt" data-button-text><?= t('addToCart') ?></div>
|
||||
</button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -27,6 +27,10 @@ snippet('header', ['title' => $page->title(), 'template' => 'shop']);
|
|||
|
||||
<div class="details" data-product-details></div>
|
||||
|
||||
<div class="product-options" data-product-options style="display: none;">
|
||||
<ul class="product-options__list" data-product-options-list></ul>
|
||||
</div>
|
||||
|
||||
<?php snippet('buy-button') ?>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue