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);
|
renderPrice(product);
|
||||||
renderDetails(product, isEnglish);
|
renderDetails(product, isEnglish);
|
||||||
renderImages(product, isEnglish);
|
renderImages(product, isEnglish);
|
||||||
renderVariants(product);
|
renderOptions(product);
|
||||||
setupAddToCart(product);
|
setupAddToCart(product);
|
||||||
renderStock(product);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderTitle(product, isEnglish) {
|
function renderTitle(product, isEnglish) {
|
||||||
|
|
@ -113,42 +112,78 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderVariants(product) {
|
function renderOptions(product) {
|
||||||
if (product.variants.edges.length <= 1) return;
|
if (product.variants.edges.length <= 1) return;
|
||||||
|
|
||||||
const variantsContainer = document.querySelector("[data-product-variants]");
|
const firstVariant = product.variants.edges[0].node;
|
||||||
const variantSelector = document.querySelector("[data-variant-selector]");
|
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";
|
product.variants.edges.forEach(edge => {
|
||||||
|
const variant = edge.node;
|
||||||
variantSelector.innerHTML = product.variants.edges
|
if (variant.selectedOptions && variant.selectedOptions[0]) {
|
||||||
.map((edge) => {
|
optionValues.add(variant.selectedOptions[0].value);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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) {
|
function setupAddToCart(product) {
|
||||||
|
|
@ -158,31 +193,35 @@
|
||||||
const productId = product.id.replace("gid://shopify/Product/", "");
|
const productId = product.id.replace("gid://shopify/Product/", "");
|
||||||
addToCartBtn.dataset.productId = productId;
|
addToCartBtn.dataset.productId = productId;
|
||||||
|
|
||||||
const firstAvailableVariant = product.variants.edges.find(
|
const hasMultipleVariants = product.variants.edges.length > 1;
|
||||||
(e) => e.node.availableForSale
|
const firstVariant = product.variants.edges[0]?.node;
|
||||||
);
|
const hasOptions = firstVariant?.selectedOptions && firstVariant.selectedOptions.length > 0;
|
||||||
if (firstAvailableVariant) {
|
|
||||||
const variantId = firstAvailableVariant.node.id.replace(
|
|
||||||
"gid://shopify/ProductVariant/",
|
|
||||||
""
|
|
||||||
);
|
|
||||||
addToCartBtn.dataset.variantId = variantId;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderStock(product) {
|
const uniqueOptions = new Set();
|
||||||
const stockEl = document.querySelector("[data-product-stock]");
|
product.variants.edges.forEach(edge => {
|
||||||
if (!stockEl) return;
|
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 (hasMultipleVariants && hasOptions && hasMultipleOptions) {
|
||||||
|
addToCartBtn.setAttribute('disabled', 'disabled');
|
||||||
if (product.availableForSale) {
|
const buttonText = addToCartBtn.querySelector('[data-button-text]');
|
||||||
stockEl.textContent = addToCartBtn?.dataset.textInStock || "En stock";
|
if (buttonText) {
|
||||||
stockEl.classList.add("in-stock");
|
buttonText.textContent = addToCartBtn.dataset.textChooseOption || 'Choisissez une option';
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
stockEl.textContent =
|
const firstAvailableVariant = product.variants.edges.find(
|
||||||
addToCartBtn?.dataset.textOutOfStock || "Rupture de stock";
|
(e) => e.node.availableForSale
|
||||||
stockEl.classList.add("out-of-stock");
|
);
|
||||||
|
if (firstAvailableVariant) {
|
||||||
|
const variantId = firstAvailableVariant.node.id.replace(
|
||||||
|
"gid://shopify/ProductVariant/",
|
||||||
|
""
|
||||||
|
);
|
||||||
|
addToCartBtn.dataset.variantId = variantId;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@ return [
|
||||||
'loading' => 'Loading...',
|
'loading' => 'Loading...',
|
||||||
'productNotFound' => 'Product not found',
|
'productNotFound' => 'Product not found',
|
||||||
'selectVariant' => 'Select',
|
'selectVariant' => 'Select',
|
||||||
|
'chooseOption' => 'Choose an option',
|
||||||
|
|
||||||
// Blueprints - Home
|
// Blueprints - Home
|
||||||
'home.title' => 'Home',
|
'home.title' => 'Home',
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@ return [
|
||||||
'loading' => 'Chargement...',
|
'loading' => 'Chargement...',
|
||||||
'productNotFound' => 'Produit non trouvé',
|
'productNotFound' => 'Produit non trouvé',
|
||||||
'selectVariant' => 'Choisir',
|
'selectVariant' => 'Choisir',
|
||||||
|
'chooseOption' => 'Choisissez une option',
|
||||||
|
|
||||||
// Blueprints - Home
|
// Blueprints - Home
|
||||||
'home.title' => 'Accueil',
|
'home.title' => 'Accueil',
|
||||||
|
|
|
||||||
|
|
@ -1,25 +1,20 @@
|
||||||
<div class="product-purchase">
|
<div class="add-to-cart">
|
||||||
<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>
|
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="btn-add-to-cart"
|
class="btn__default"
|
||||||
data-shopify-add-to-cart
|
data-shopify-add-to-cart
|
||||||
data-product-id=""
|
data-product-id=""
|
||||||
data-variant-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-adding="<?= t('addingToCart') ?>"
|
||||||
data-text-added="<?= t('addedToCart') ?>"
|
data-text-added="<?= t('addedToCart') ?>"
|
||||||
data-text-error="<?= t('errorAddToCart') ?>"
|
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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,10 @@ snippet('header', ['title' => $page->title(), 'template' => 'shop']);
|
||||||
|
|
||||||
<div class="details" data-product-details></div>
|
<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') ?>
|
<?php snippet('buy-button') ?>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue