2026-01-16 12:03:20 +01:00
|
|
|
(async function () {
|
|
|
|
|
const container = document.querySelector("[data-product-loader]");
|
|
|
|
|
if (!container) return;
|
|
|
|
|
|
|
|
|
|
const handle = container.dataset.shopifyHandle;
|
2026-01-21 11:11:18 +01:00
|
|
|
const language = container.dataset.language || "fr";
|
|
|
|
|
const isEnglish = language === "en";
|
2026-01-16 12:03:20 +01:00
|
|
|
const loadingState = container.querySelector(".product-loading");
|
|
|
|
|
const contentState = container.querySelector(".product-content");
|
|
|
|
|
const errorState = container.querySelector(".product-error");
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const cart = new ShopifyCart({
|
|
|
|
|
domain: "nv7cqv-bu.myshopify.com",
|
|
|
|
|
storefrontAccessToken: "dec3d35a2554384d149c72927d1cfd1b",
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const product = await cart.getProductByHandle(handle);
|
|
|
|
|
|
|
|
|
|
if (!product) {
|
|
|
|
|
throw new Error("Product not found");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
renderProduct(product, isEnglish);
|
2026-01-16 17:02:27 +01:00
|
|
|
updateMetaTags(product, isEnglish);
|
2026-01-16 12:03:20 +01:00
|
|
|
|
|
|
|
|
loadingState.style.display = "none";
|
|
|
|
|
contentState.removeAttribute("style");
|
|
|
|
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
if (typeof Swiper !== "undefined" && product.images.edges.length > 0) {
|
|
|
|
|
new Swiper(".product-gallery", {
|
|
|
|
|
loop: product.images.edges.length > 1,
|
|
|
|
|
navigation: {
|
|
|
|
|
nextEl: ".swiper-button-next",
|
|
|
|
|
prevEl: ".swiper-button-prev",
|
|
|
|
|
},
|
|
|
|
|
pagination: {
|
|
|
|
|
el: ".swiper-pagination",
|
|
|
|
|
clickable: true,
|
|
|
|
|
},
|
|
|
|
|
keyboard: {
|
|
|
|
|
enabled: true,
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}, 100);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error("Error loading product:", error);
|
|
|
|
|
loadingState.style.display = "none";
|
|
|
|
|
errorState.style.display = "block";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function renderProduct(product, isEnglish) {
|
|
|
|
|
renderTitle(product, isEnglish);
|
|
|
|
|
renderPrice(product);
|
|
|
|
|
renderDetails(product, isEnglish);
|
|
|
|
|
renderImages(product, isEnglish);
|
2026-01-16 12:15:28 +01:00
|
|
|
renderOptions(product);
|
2026-01-16 12:03:20 +01:00
|
|
|
setupAddToCart(product);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function renderTitle(product, isEnglish) {
|
|
|
|
|
const titleEl = document.querySelector("[data-product-title]");
|
|
|
|
|
if (titleEl) {
|
2026-01-21 11:11:18 +01:00
|
|
|
const title =
|
|
|
|
|
isEnglish && product.titleEn?.value
|
|
|
|
|
? product.titleEn.value
|
|
|
|
|
: product.title;
|
2026-01-16 12:03:20 +01:00
|
|
|
titleEl.textContent = title;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function renderPrice(product) {
|
|
|
|
|
const priceEl = document.querySelector("[data-product-price]");
|
|
|
|
|
if (priceEl) {
|
|
|
|
|
const price = parseFloat(product.priceRange.minVariantPrice.amount);
|
|
|
|
|
priceEl.textContent = price.toFixed(2) + "€";
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function renderDetails(product, isEnglish) {
|
|
|
|
|
const detailsEl = document.querySelector("[data-product-details]");
|
|
|
|
|
if (detailsEl) {
|
2026-01-21 11:11:18 +01:00
|
|
|
const description =
|
|
|
|
|
isEnglish && product.descriptionEn?.value
|
2026-01-22 13:19:26 +01:00
|
|
|
? product.descriptionEn.value.replaceAll("\n", "<br>")
|
2026-01-21 11:11:18 +01:00
|
|
|
: product.descriptionHtml || "";
|
2026-01-16 12:03:20 +01:00
|
|
|
detailsEl.innerHTML = description;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function renderImages(product, isEnglish) {
|
|
|
|
|
const imagesContainer = document.querySelector("[data-product-images]");
|
|
|
|
|
|
|
|
|
|
if (imagesContainer && product.images.edges.length > 0) {
|
2026-01-21 11:11:18 +01:00
|
|
|
const productTitle =
|
|
|
|
|
isEnglish && product.titleEn?.value
|
|
|
|
|
? product.titleEn.value
|
|
|
|
|
: product.title;
|
2026-01-16 12:03:20 +01:00
|
|
|
|
|
|
|
|
imagesContainer.innerHTML = product.images.edges
|
|
|
|
|
.map((edge) => {
|
|
|
|
|
const img = edge.node;
|
|
|
|
|
return `
|
|
|
|
|
<div class="swiper-slide">
|
|
|
|
|
<figure>
|
|
|
|
|
<img src="${img.url}"
|
|
|
|
|
alt="${img.altText || productTitle}"
|
|
|
|
|
loading="lazy" />
|
|
|
|
|
</figure>
|
|
|
|
|
</div>
|
|
|
|
|
`;
|
|
|
|
|
})
|
|
|
|
|
.join("");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-16 12:15:28 +01:00
|
|
|
function renderOptions(product) {
|
2026-01-16 12:03:20 +01:00
|
|
|
if (product.variants.edges.length <= 1) return;
|
|
|
|
|
|
2026-01-16 12:15:28 +01:00
|
|
|
const firstVariant = product.variants.edges[0].node;
|
2026-01-21 11:11:18 +01:00
|
|
|
if (
|
|
|
|
|
!firstVariant.selectedOptions ||
|
|
|
|
|
firstVariant.selectedOptions.length === 0
|
|
|
|
|
)
|
|
|
|
|
return;
|
2026-01-16 12:03:20 +01:00
|
|
|
|
2026-01-16 12:15:28 +01:00
|
|
|
const mainOption = firstVariant.selectedOptions[0];
|
|
|
|
|
const optionValues = new Set();
|
2026-01-16 12:03:20 +01:00
|
|
|
|
2026-01-21 11:11:18 +01:00
|
|
|
product.variants.edges.forEach((edge) => {
|
2026-01-16 12:15:28 +01:00
|
|
|
const variant = edge.node;
|
|
|
|
|
if (variant.selectedOptions && variant.selectedOptions[0]) {
|
|
|
|
|
optionValues.add(variant.selectedOptions[0].value);
|
2026-01-16 12:03:20 +01:00
|
|
|
}
|
|
|
|
|
});
|
2026-01-16 12:15:28 +01:00
|
|
|
|
|
|
|
|
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;
|
2026-01-21 11:11:18 +01:00
|
|
|
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 `
|
2026-01-16 12:15:28 +01:00
|
|
|
<li>
|
|
|
|
|
<input
|
|
|
|
|
type="radio"
|
|
|
|
|
id="${uniqueId}"
|
|
|
|
|
name="${optionSlug}"
|
|
|
|
|
value="${value}"
|
2026-01-21 11:11:18 +01:00
|
|
|
data-variant-id="${
|
|
|
|
|
variant
|
|
|
|
|
? variant.id.replace("gid://shopify/ProductVariant/", "")
|
|
|
|
|
: ""
|
|
|
|
|
}"
|
|
|
|
|
${!isAvailable ? "disabled" : ""}
|
2026-01-16 12:15:28 +01:00
|
|
|
/>
|
|
|
|
|
<label for="${uniqueId}">${value}</label>
|
|
|
|
|
</li>
|
|
|
|
|
`;
|
2026-01-21 11:11:18 +01:00
|
|
|
})
|
|
|
|
|
.join("");
|
2026-01-16 12:15:28 +01:00
|
|
|
|
2026-01-21 11:11:18 +01:00
|
|
|
optionsContainer.style.display = "block";
|
2026-01-16 12:15:28 +01:00
|
|
|
|
|
|
|
|
const radios = optionsList.querySelectorAll('input[type="radio"]');
|
|
|
|
|
const addToCartBtn = document.querySelector("[data-shopify-add-to-cart]");
|
2026-01-21 11:11:18 +01:00
|
|
|
const buttonText = addToCartBtn?.querySelector("[data-button-text]");
|
2026-01-16 12:15:28 +01:00
|
|
|
|
2026-01-21 11:11:18 +01:00
|
|
|
radios.forEach((radio) => {
|
|
|
|
|
radio.addEventListener("change", function () {
|
2026-01-16 12:15:28 +01:00
|
|
|
const variantId = this.dataset.variantId;
|
|
|
|
|
|
|
|
|
|
if (addToCartBtn) {
|
|
|
|
|
addToCartBtn.dataset.variantId = variantId;
|
2026-01-21 11:11:18 +01:00
|
|
|
addToCartBtn.removeAttribute("disabled");
|
2026-01-16 12:15:28 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (buttonText) {
|
2026-01-21 11:11:18 +01:00
|
|
|
buttonText.textContent =
|
|
|
|
|
addToCartBtn.dataset.defaultText || "Ajouter au panier";
|
2026-01-16 12:15:28 +01:00
|
|
|
}
|
|
|
|
|
|
2026-01-21 11:11:18 +01:00
|
|
|
const allLi = optionsList.querySelectorAll("li");
|
|
|
|
|
allLi.forEach((li) => li.classList.remove("is-selected"));
|
|
|
|
|
this.closest("li").classList.add("is-selected");
|
2026-01-16 12:15:28 +01:00
|
|
|
});
|
|
|
|
|
});
|
2026-01-16 12:03:20 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function setupAddToCart(product) {
|
|
|
|
|
const addToCartBtn = document.querySelector("[data-shopify-add-to-cart]");
|
|
|
|
|
if (!addToCartBtn) return;
|
|
|
|
|
|
|
|
|
|
const productId = product.id.replace("gid://shopify/Product/", "");
|
|
|
|
|
addToCartBtn.dataset.productId = productId;
|
|
|
|
|
|
2026-01-16 12:15:28 +01:00
|
|
|
const hasMultipleVariants = product.variants.edges.length > 1;
|
|
|
|
|
const firstVariant = product.variants.edges[0]?.node;
|
2026-01-21 11:11:18 +01:00
|
|
|
const hasOptions =
|
|
|
|
|
firstVariant?.selectedOptions && firstVariant.selectedOptions.length > 0;
|
2026-01-16 12:03:20 +01:00
|
|
|
|
2026-01-16 12:15:28 +01:00
|
|
|
const uniqueOptions = new Set();
|
2026-01-21 11:11:18 +01:00
|
|
|
product.variants.edges.forEach((edge) => {
|
2026-01-16 12:15:28 +01:00
|
|
|
if (edge.node.selectedOptions && edge.node.selectedOptions[0]) {
|
|
|
|
|
uniqueOptions.add(edge.node.selectedOptions[0].value);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
const hasMultipleOptions = uniqueOptions.size > 1;
|
2026-01-16 12:03:20 +01:00
|
|
|
|
2026-01-16 12:15:28 +01:00
|
|
|
if (hasMultipleVariants && hasOptions && hasMultipleOptions) {
|
2026-01-21 11:11:18 +01:00
|
|
|
addToCartBtn.setAttribute("disabled", "disabled");
|
|
|
|
|
const buttonText = addToCartBtn.querySelector("[data-button-text]");
|
2026-01-16 12:15:28 +01:00
|
|
|
if (buttonText) {
|
2026-01-21 11:11:18 +01:00
|
|
|
buttonText.textContent =
|
|
|
|
|
addToCartBtn.dataset.textChooseOption || "Choisissez une option";
|
2026-01-16 12:15:28 +01:00
|
|
|
}
|
2026-01-16 12:03:20 +01:00
|
|
|
} else {
|
2026-01-16 12:15:28 +01:00
|
|
|
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;
|
|
|
|
|
}
|
2026-01-16 12:03:20 +01:00
|
|
|
}
|
|
|
|
|
}
|
2026-01-16 17:02:27 +01:00
|
|
|
|
|
|
|
|
function updateMetaTags(product, isEnglish) {
|
|
|
|
|
// Update title and description
|
2026-01-21 11:11:18 +01:00
|
|
|
const title =
|
|
|
|
|
isEnglish && product.titleEn?.value
|
|
|
|
|
? product.titleEn.value
|
|
|
|
|
: product.title;
|
|
|
|
|
const description =
|
|
|
|
|
isEnglish && product.descriptionEn?.value
|
|
|
|
|
? product.descriptionEn.value
|
|
|
|
|
: product.description;
|
2026-01-16 17:02:27 +01:00
|
|
|
|
|
|
|
|
// Update Open Graph title
|
2026-01-21 11:11:18 +01:00
|
|
|
const ogTitle = document.getElementById("og-title");
|
2026-01-16 17:02:27 +01:00
|
|
|
if (ogTitle) {
|
2026-01-21 11:11:18 +01:00
|
|
|
ogTitle.setAttribute("content", title);
|
2026-01-16 17:02:27 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Update Open Graph description
|
2026-01-21 11:11:18 +01:00
|
|
|
const ogDescription = document.getElementById("og-description");
|
2026-01-16 17:02:27 +01:00
|
|
|
if (ogDescription && description) {
|
|
|
|
|
const excerpt = description.substring(0, 160);
|
2026-01-21 11:11:18 +01:00
|
|
|
ogDescription.setAttribute("content", excerpt);
|
2026-01-16 17:02:27 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Update Open Graph image
|
2026-01-21 11:11:18 +01:00
|
|
|
const ogImage = document.getElementById("og-image");
|
2026-01-16 17:02:27 +01:00
|
|
|
if (ogImage && product.images.edges.length > 0) {
|
2026-01-21 11:11:18 +01:00
|
|
|
ogImage.setAttribute("content", product.images.edges[0].node.url);
|
2026-01-16 17:02:27 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Update product price
|
2026-01-21 11:11:18 +01:00
|
|
|
const ogPrice = document.getElementById("og-price");
|
2026-01-16 17:02:27 +01:00
|
|
|
if (ogPrice) {
|
2026-01-21 11:11:18 +01:00
|
|
|
const price = parseFloat(
|
|
|
|
|
product.priceRange.minVariantPrice.amount
|
|
|
|
|
).toFixed(2);
|
|
|
|
|
ogPrice.setAttribute("content", price);
|
2026-01-16 17:02:27 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Update availability
|
2026-01-21 11:11:18 +01:00
|
|
|
const ogAvailability = document.getElementById("og-availability");
|
2026-01-16 17:02:27 +01:00
|
|
|
if (ogAvailability) {
|
2026-01-21 11:11:18 +01:00
|
|
|
const availability = product.availableForSale
|
|
|
|
|
? "in stock"
|
|
|
|
|
: "out of stock";
|
|
|
|
|
ogAvailability.setAttribute("content", availability);
|
2026-01-16 17:02:27 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Update page title
|
|
|
|
|
document.title = `${title} | Index.ngo`;
|
|
|
|
|
|
|
|
|
|
// Update meta description
|
|
|
|
|
let metaDescription = document.querySelector('meta[name="description"]');
|
|
|
|
|
if (metaDescription && description) {
|
|
|
|
|
const excerpt = description.substring(0, 160);
|
2026-01-21 11:11:18 +01:00
|
|
|
metaDescription.setAttribute("content", excerpt);
|
2026-01-16 17:02:27 +01:00
|
|
|
}
|
|
|
|
|
}
|
2026-01-16 12:03:20 +01:00
|
|
|
})();
|