Compare commits

...

3 commits

Author SHA1 Message Date
isUnknown
9b4bd4b731 product > english > description : nl2br
All checks were successful
Deploy / Deploy to Production (push) Successful in 6s
2026-01-21 11:11:18 +01:00
isUnknown
aa873e117f Fix cart initialization and ShopifyCart loading
- Wait for ShopifyCart to be available before initializing structured data
- Add getCart() method to retrieve existing cart from Shopify API
- Load cart state on page load to display correct initial cart contents

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-21 11:06:28 +01:00
9396ae4e02 Fix Shopify API connection on local Windows dev server
Add SSL_VERIFYPEER option to cURL to fix HTTP 0 errors when running
with PHP built-in server on Windows.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-20 12:03:06 +01:00
5 changed files with 166 additions and 63 deletions

View file

@ -32,6 +32,9 @@
// Initialize event listeners // Initialize event listeners
setupEventListeners(); setupEventListeners();
// Load initial cart state
loadCart();
} }
function setupEventListeners() { function setupEventListeners() {
@ -60,6 +63,18 @@
}); });
} }
async function loadCart() {
if (!cartInstance) return;
try {
const cart = await cartInstance.getCart();
currentCart = cart;
renderCart();
} catch (error) {
console.error('Error loading cart:', error);
}
}
function openDrawer() { function openDrawer() {
drawer.classList.add('is-open'); drawer.classList.add('is-open');
document.body.style.overflow = 'hidden'; document.body.style.overflow = 'hidden';

View file

@ -3,8 +3,8 @@
if (!container) return; if (!container) return;
const handle = container.dataset.shopifyHandle; const handle = container.dataset.shopifyHandle;
const language = container.dataset.language || 'fr'; const language = container.dataset.language || "fr";
const isEnglish = language === 'en'; const isEnglish = language === "en";
const loadingState = container.querySelector(".product-loading"); const loadingState = container.querySelector(".product-loading");
const contentState = container.querySelector(".product-content"); const contentState = container.querySelector(".product-content");
const errorState = container.querySelector(".product-error"); const errorState = container.querySelector(".product-error");
@ -63,9 +63,10 @@
function renderTitle(product, isEnglish) { function renderTitle(product, isEnglish) {
const titleEl = document.querySelector("[data-product-title]"); const titleEl = document.querySelector("[data-product-title]");
if (titleEl) { if (titleEl) {
const title = isEnglish && product.titleEn?.value const title =
? product.titleEn.value isEnglish && product.titleEn?.value
: product.title; ? product.titleEn.value
: product.title;
titleEl.textContent = title; titleEl.textContent = title;
} }
} }
@ -81,9 +82,10 @@
function renderDetails(product, isEnglish) { function renderDetails(product, isEnglish) {
const detailsEl = document.querySelector("[data-product-details]"); const detailsEl = document.querySelector("[data-product-details]");
if (detailsEl) { if (detailsEl) {
const description = isEnglish && product.descriptionEn?.value const description =
? product.descriptionEn.value isEnglish && product.descriptionEn?.value
: product.descriptionHtml || ""; ? product.descriptionEn.value.replace("\n", "<br><br>")
: product.descriptionHtml || "";
detailsEl.innerHTML = description; detailsEl.innerHTML = description;
} }
} }
@ -92,9 +94,10 @@
const imagesContainer = document.querySelector("[data-product-images]"); const imagesContainer = document.querySelector("[data-product-images]");
if (imagesContainer && product.images.edges.length > 0) { if (imagesContainer && product.images.edges.length > 0) {
const productTitle = isEnglish && product.titleEn?.value const productTitle =
? product.titleEn.value isEnglish && product.titleEn?.value
: product.title; ? product.titleEn.value
: product.title;
imagesContainer.innerHTML = product.images.edges imagesContainer.innerHTML = product.images.edges
.map((edge) => { .map((edge) => {
@ -117,12 +120,16 @@
if (product.variants.edges.length <= 1) return; if (product.variants.edges.length <= 1) return;
const firstVariant = product.variants.edges[0].node; const firstVariant = product.variants.edges[0].node;
if (!firstVariant.selectedOptions || firstVariant.selectedOptions.length === 0) return; if (
!firstVariant.selectedOptions ||
firstVariant.selectedOptions.length === 0
)
return;
const mainOption = firstVariant.selectedOptions[0]; const mainOption = firstVariant.selectedOptions[0];
const optionValues = new Set(); const optionValues = new Set();
product.variants.edges.forEach(edge => { product.variants.edges.forEach((edge) => {
const variant = edge.node; const variant = edge.node;
if (variant.selectedOptions && variant.selectedOptions[0]) { if (variant.selectedOptions && variant.selectedOptions[0]) {
optionValues.add(variant.selectedOptions[0].value); optionValues.add(variant.selectedOptions[0].value);
@ -137,52 +144,62 @@
if (!optionsContainer || !optionsList) return; if (!optionsContainer || !optionsList) return;
const optionName = mainOption.name; const optionName = mainOption.name;
const optionSlug = optionName.toLowerCase().replace(/\s+/g, '-'); const optionSlug = optionName.toLowerCase().replace(/\s+/g, "-");
optionsList.innerHTML = Array.from(optionValues).map((value) => { optionsList.innerHTML = Array.from(optionValues)
const uniqueId = `${optionSlug}-${value.toLowerCase().replace(/\s+/g, '-')}`; .map((value) => {
const variant = product.variants.edges.find(e => const uniqueId = `${optionSlug}-${value
e.node.selectedOptions && e.node.selectedOptions[0]?.value === value .toLowerCase()
)?.node; .replace(/\s+/g, "-")}`;
const isAvailable = variant?.availableForSale || false; const variant = product.variants.edges.find(
(e) =>
e.node.selectedOptions && e.node.selectedOptions[0]?.value === value
)?.node;
const isAvailable = variant?.availableForSale || false;
return ` return `
<li> <li>
<input <input
type="radio" type="radio"
id="${uniqueId}" id="${uniqueId}"
name="${optionSlug}" name="${optionSlug}"
value="${value}" value="${value}"
data-variant-id="${variant ? variant.id.replace('gid://shopify/ProductVariant/', '') : ''}" data-variant-id="${
${!isAvailable ? 'disabled' : ''} variant
? variant.id.replace("gid://shopify/ProductVariant/", "")
: ""
}"
${!isAvailable ? "disabled" : ""}
/> />
<label for="${uniqueId}">${value}</label> <label for="${uniqueId}">${value}</label>
</li> </li>
`; `;
}).join(''); })
.join("");
optionsContainer.style.display = 'block'; optionsContainer.style.display = "block";
const radios = optionsList.querySelectorAll('input[type="radio"]'); const radios = optionsList.querySelectorAll('input[type="radio"]');
const addToCartBtn = document.querySelector("[data-shopify-add-to-cart]"); const addToCartBtn = document.querySelector("[data-shopify-add-to-cart]");
const buttonText = addToCartBtn?.querySelector('[data-button-text]'); const buttonText = addToCartBtn?.querySelector("[data-button-text]");
radios.forEach(radio => { radios.forEach((radio) => {
radio.addEventListener('change', function() { radio.addEventListener("change", function () {
const variantId = this.dataset.variantId; const variantId = this.dataset.variantId;
if (addToCartBtn) { if (addToCartBtn) {
addToCartBtn.dataset.variantId = variantId; addToCartBtn.dataset.variantId = variantId;
addToCartBtn.removeAttribute('disabled'); addToCartBtn.removeAttribute("disabled");
} }
if (buttonText) { if (buttonText) {
buttonText.textContent = addToCartBtn.dataset.defaultText || 'Ajouter au panier'; buttonText.textContent =
addToCartBtn.dataset.defaultText || "Ajouter au panier";
} }
const allLi = optionsList.querySelectorAll('li'); const allLi = optionsList.querySelectorAll("li");
allLi.forEach(li => li.classList.remove('is-selected')); allLi.forEach((li) => li.classList.remove("is-selected"));
this.closest('li').classList.add('is-selected'); this.closest("li").classList.add("is-selected");
}); });
}); });
} }
@ -196,10 +213,11 @@
const hasMultipleVariants = product.variants.edges.length > 1; const hasMultipleVariants = product.variants.edges.length > 1;
const firstVariant = product.variants.edges[0]?.node; const firstVariant = product.variants.edges[0]?.node;
const hasOptions = firstVariant?.selectedOptions && firstVariant.selectedOptions.length > 0; const hasOptions =
firstVariant?.selectedOptions && firstVariant.selectedOptions.length > 0;
const uniqueOptions = new Set(); const uniqueOptions = new Set();
product.variants.edges.forEach(edge => { product.variants.edges.forEach((edge) => {
if (edge.node.selectedOptions && edge.node.selectedOptions[0]) { if (edge.node.selectedOptions && edge.node.selectedOptions[0]) {
uniqueOptions.add(edge.node.selectedOptions[0].value); uniqueOptions.add(edge.node.selectedOptions[0].value);
} }
@ -207,10 +225,11 @@
const hasMultipleOptions = uniqueOptions.size > 1; const hasMultipleOptions = uniqueOptions.size > 1;
if (hasMultipleVariants && hasOptions && hasMultipleOptions) { if (hasMultipleVariants && hasOptions && hasMultipleOptions) {
addToCartBtn.setAttribute('disabled', 'disabled'); addToCartBtn.setAttribute("disabled", "disabled");
const buttonText = addToCartBtn.querySelector('[data-button-text]'); const buttonText = addToCartBtn.querySelector("[data-button-text]");
if (buttonText) { if (buttonText) {
buttonText.textContent = addToCartBtn.dataset.textChooseOption || 'Choisissez une option'; buttonText.textContent =
addToCartBtn.dataset.textChooseOption || "Choisissez une option";
} }
} else { } else {
const firstAvailableVariant = product.variants.edges.find( const firstAvailableVariant = product.variants.edges.find(
@ -228,44 +247,50 @@
function updateMetaTags(product, isEnglish) { function updateMetaTags(product, isEnglish) {
// Update title and description // Update title and description
const title = isEnglish && product.titleEn?.value const title =
? product.titleEn.value isEnglish && product.titleEn?.value
: product.title; ? product.titleEn.value
const description = isEnglish && product.descriptionEn?.value : product.title;
? product.descriptionEn.value const description =
: product.description; isEnglish && product.descriptionEn?.value
? product.descriptionEn.value
: product.description;
// Update Open Graph title // Update Open Graph title
const ogTitle = document.getElementById('og-title'); const ogTitle = document.getElementById("og-title");
if (ogTitle) { if (ogTitle) {
ogTitle.setAttribute('content', title); ogTitle.setAttribute("content", title);
} }
// Update Open Graph description // Update Open Graph description
const ogDescription = document.getElementById('og-description'); const ogDescription = document.getElementById("og-description");
if (ogDescription && description) { if (ogDescription && description) {
const excerpt = description.substring(0, 160); const excerpt = description.substring(0, 160);
ogDescription.setAttribute('content', excerpt); ogDescription.setAttribute("content", excerpt);
} }
// Update Open Graph image // Update Open Graph image
const ogImage = document.getElementById('og-image'); const ogImage = document.getElementById("og-image");
if (ogImage && product.images.edges.length > 0) { if (ogImage && product.images.edges.length > 0) {
ogImage.setAttribute('content', product.images.edges[0].node.url); ogImage.setAttribute("content", product.images.edges[0].node.url);
} }
// Update product price // Update product price
const ogPrice = document.getElementById('og-price'); const ogPrice = document.getElementById("og-price");
if (ogPrice) { if (ogPrice) {
const price = parseFloat(product.priceRange.minVariantPrice.amount).toFixed(2); const price = parseFloat(
ogPrice.setAttribute('content', price); product.priceRange.minVariantPrice.amount
).toFixed(2);
ogPrice.setAttribute("content", price);
} }
// Update availability // Update availability
const ogAvailability = document.getElementById('og-availability'); const ogAvailability = document.getElementById("og-availability");
if (ogAvailability) { if (ogAvailability) {
const availability = product.availableForSale ? 'in stock' : 'out of stock'; const availability = product.availableForSale
ogAvailability.setAttribute('content', availability); ? "in stock"
: "out of stock";
ogAvailability.setAttribute("content", availability);
} }
// Update page title // Update page title
@ -275,7 +300,7 @@
let metaDescription = document.querySelector('meta[name="description"]'); let metaDescription = document.querySelector('meta[name="description"]');
if (metaDescription && description) { if (metaDescription && description) {
const excerpt = description.substring(0, 160); const excerpt = description.substring(0, 160);
metaDescription.setAttribute('content', excerpt); metaDescription.setAttribute("content", excerpt);
} }
} }
})(); })();

View file

@ -306,6 +306,58 @@ class ShopifyCart {
return cart; return cart;
} }
/**
* Get existing cart by ID
*/
async getCart() {
if (!this.cartId) {
return null;
}
const query = `
query getCart($cartId: ID!) {
cart(id: $cartId) {
id
checkoutUrl
lines(first: 10) {
edges {
node {
id
quantity
merchandise {
... on ProductVariant {
id
title
price {
amount
currencyCode
}
product {
title
}
}
}
}
}
}
}
}
`;
try {
const data = await this.query(query, {
cartId: this.cartId
});
return data.cart;
} catch (error) {
// Cart might be expired or invalid
console.error('Error fetching cart:', error);
this.clearCart();
return null;
}
}
/** /**
* Get checkout URL to redirect user * Get checkout URL to redirect user
*/ */

View file

@ -43,6 +43,7 @@ function fetchShopifyProducts(): array
$ch = curl_init($endpoint); $ch = curl_init($endpoint);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); // Nécessaire pour dev local sur Windows
curl_setopt($ch, CURLOPT_HTTPHEADER, [ curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Content-Type: application/json', 'Content-Type: application/json',
'X-Shopify-Storefront-Access-Token: ' . $token 'X-Shopify-Storefront-Access-Token: ' . $token

View file

@ -38,12 +38,18 @@
const language = container.dataset.language || 'fr'; const language = container.dataset.language || 'fr';
const isEnglish = language === 'en'; const isEnglish = language === 'en';
const cart = new ShopifyCart({ function initStructuredData() {
domain: 'nv7cqv-bu.myshopify.com', if (typeof ShopifyCart === 'undefined') {
storefrontAccessToken: 'dec3d35a2554384d149c72927d1cfd1b' setTimeout(initStructuredData, 100);
}); return;
}
cart.getProductByHandle(handle).then(product => { const cart = new ShopifyCart({
domain: 'nv7cqv-bu.myshopify.com',
storefrontAccessToken: 'dec3d35a2554384d149c72927d1cfd1b'
});
cart.getProductByHandle(handle).then(product => {
if (!product) return; if (!product) return;
const title = isEnglish && product.titleEn?.value ? product.titleEn.value : product.title; const title = isEnglish && product.titleEn?.value ? product.titleEn.value : product.title;
@ -79,6 +85,10 @@
if (schemaScript) { if (schemaScript) {
schemaScript.textContent = JSON.stringify(schema, null, 2); schemaScript.textContent = JSON.stringify(schema, null, 2);
} }
}); });
}
// Initialize when ShopifyCart is available
initStructuredData();
})(); })();
</script> </script>