Migrate product data from Kirby to Shopify Storefront API
- Add product loaders (product-loader.js, products-list-loader.js) to fetch data from Shopify - Extend Shopify API client with getProductByHandle() and getAllProducts() methods - Integrate Shopify metafields for multilingual support (custom.title_en, custom.description_en) - Refactor product.php and home.php templates to load content dynamically - Simplify product blueprint to minimal routing configuration - Create generic buy-button.php snippet with variant selection - Update footer.php with conditional script loading - Refactor _section--product.scss for better Sass structure - Add translations for loading states and product errors - Clean up old Kirby product content files Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
957cf79e45
commit
ad699f0365
22 changed files with 649 additions and 579 deletions
|
|
@ -1,12 +1,3 @@
|
|||
.section__product,
|
||||
.store__nav{
|
||||
max-width: 1200px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.section__product,
|
||||
.store__nav {
|
||||
max-width: 1200px;
|
||||
|
|
@ -14,10 +5,6 @@
|
|||
margin-right: auto;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
.store__nav {
|
||||
padding-top: calc(var(--spacing) * 1);
|
||||
padding-bottom: calc(var(--spacing) * 0.5);
|
||||
|
|
@ -27,174 +14,35 @@
|
|||
a {
|
||||
text-decoration: none;
|
||||
|
||||
&::before {
|
||||
content: "← ";
|
||||
}
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
a::before {
|
||||
content: "← ";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
.section__product .details {
|
||||
// margin-bottom: calc(var(--spacing) * 2);
|
||||
|
||||
ul{
|
||||
margin-left: 2ch;
|
||||
li{
|
||||
padding-bottom: 0.2em;
|
||||
@media #{$small} {
|
||||
a {
|
||||
padding-top: 0;
|
||||
font-size: var(--fs-small);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.section__product {
|
||||
.details {
|
||||
ul {
|
||||
margin-left: 2ch;
|
||||
|
||||
.product-options__list {
|
||||
list-style: none;
|
||||
display: flex;
|
||||
gap: 2ch;
|
||||
|
||||
li {
|
||||
position: relative;
|
||||
|
||||
input[type="radio"] {
|
||||
position: fixed;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
label {
|
||||
font-family: var(--title);
|
||||
font-size: var(--fs-normal);
|
||||
height: 4ch;
|
||||
width: 4ch;
|
||||
border-radius: 50%;
|
||||
border: var(--border);
|
||||
border-color: transparent;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding-top: 0px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
input[type="radio"]:checked + label {
|
||||
border-color: var(--color-txt);
|
||||
}
|
||||
|
||||
input[type="radio"]:not(:checked) + label:hover {
|
||||
border-color: var(--grey-600);
|
||||
background-color: var(--grey-800);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
.product-gallery {
|
||||
position: relative;
|
||||
aspect-ratio: 4 / 3;
|
||||
|
||||
.swiper-slide {
|
||||
width: 100%;
|
||||
|
||||
figure {
|
||||
|
||||
aspect-ratio: 4 / 3;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
padding-bottom: 0.2em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Swiper navigation arrows
|
||||
.swiper-button-prev,
|
||||
.swiper-button-next {
|
||||
color: var(--color-txt);
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
|
||||
&:after {
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
|
||||
// Swiper pagination dots
|
||||
.swiper-pagination {
|
||||
position: relative;
|
||||
margin-top: calc(var(--spacing) * 0.5);
|
||||
bottom: 0;
|
||||
|
||||
.swiper-pagination-bullet {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background: var(--grey-600);
|
||||
opacity: 0.5;
|
||||
transition: opacity 0.3s;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
|
||||
.swiper-pagination-bullet-active {
|
||||
background: var(--color-txt);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.hero {
|
||||
margin-bottom: calc(var(--spacing) * 1);
|
||||
padding: calc(var(--spacing) * 0.5) 0;
|
||||
border-top: var(--border-light);
|
||||
border-bottom: var(--border-light);
|
||||
|
||||
.p__baseline-big {
|
||||
margin: 0;
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
|
||||
.add-to-cart {
|
||||
margin: 0;
|
||||
border-bottom: var(--border-light);
|
||||
padding: calc(var(--spacing) * 0.5) 0;
|
||||
}
|
||||
|
||||
.product-options {
|
||||
border-bottom: var(--border-light);
|
||||
padding: calc(var(--spacing) * 0.25) 0;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@media #{$small} {
|
||||
.store__nav a {
|
||||
padding-top: 0;
|
||||
font-size: var(--fs-small);
|
||||
}
|
||||
|
||||
.section__product {
|
||||
@media #{$small} {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-bottom: 10vh;
|
||||
|
|
@ -207,6 +55,7 @@
|
|||
margin-top: calc(var(--spacing) * 0.5);
|
||||
order: 1;
|
||||
}
|
||||
|
||||
figure {
|
||||
order: 2;
|
||||
margin-bottom: calc(var(--spacing) * 1);
|
||||
|
|
@ -226,25 +75,25 @@
|
|||
order: 5;
|
||||
}
|
||||
|
||||
.product-gallery{
|
||||
.product-gallery {
|
||||
width: 100vw;
|
||||
position: relative;
|
||||
left: calc(var(--padding-body)*-1);
|
||||
left: calc(var(--padding-body) * -1);
|
||||
|
||||
.swiper-button-prev,
|
||||
.swiper-button-next{ display: none; }
|
||||
.swiper-button-next {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media #{$small-up} {
|
||||
|
||||
|
||||
.section__product{
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: calc(var(--padding-body)*2);
|
||||
margin-bottom: calc(var(--spacing)*3);
|
||||
@media #{$small-up} {
|
||||
.product-content {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: calc(var(--padding-body) * 2);
|
||||
margin-bottom: calc(var(--spacing) * 3);
|
||||
}
|
||||
|
||||
.details {
|
||||
margin-bottom: calc(var(--spacing) * 2);
|
||||
|
|
@ -255,17 +104,140 @@
|
|||
border-top: var(--border-light);
|
||||
}
|
||||
|
||||
.col-left{
|
||||
.col-left {
|
||||
min-height: 100%;
|
||||
padding-bottom: 40px; //dots
|
||||
|
||||
padding-bottom: 40px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.product-gallery .swiper-slide figure{
|
||||
width: calc(100% - 60px);
|
||||
}
|
||||
}
|
||||
|
||||
.product-options {
|
||||
border-bottom: var(--border-light);
|
||||
padding: calc(var(--spacing) * 0.25) 0;
|
||||
}
|
||||
|
||||
.product-options__list {
|
||||
list-style: none;
|
||||
display: flex;
|
||||
gap: 2ch;
|
||||
|
||||
li {
|
||||
position: relative;
|
||||
|
||||
input[type="radio"] {
|
||||
position: fixed;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
|
||||
&:checked + label {
|
||||
border-color: var(--color-txt);
|
||||
}
|
||||
|
||||
&:not(:checked) + label:hover {
|
||||
border-color: var(--grey-600);
|
||||
background-color: var(--grey-800);
|
||||
}
|
||||
}
|
||||
|
||||
label {
|
||||
font-family: var(--title);
|
||||
font-size: var(--fs-normal);
|
||||
height: 4ch;
|
||||
width: 4ch;
|
||||
border-radius: 50%;
|
||||
border: var(--border);
|
||||
border-color: transparent;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding-top: 0px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.product-gallery {
|
||||
position: relative;
|
||||
aspect-ratio: 4 / 3;
|
||||
|
||||
.swiper-slide {
|
||||
width: 100%;
|
||||
|
||||
figure {
|
||||
aspect-ratio: 4 / 3;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
}
|
||||
|
||||
@media #{$small-up} {
|
||||
figure {
|
||||
width: calc(100% - 60px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.swiper-button-prev,
|
||||
.swiper-button-next {
|
||||
color: var(--color-txt);
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
|
||||
&:after {
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
|
||||
.swiper-pagination {
|
||||
position: relative;
|
||||
margin-top: calc(var(--spacing) * 0.5);
|
||||
bottom: 0;
|
||||
|
||||
.swiper-pagination-bullet {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background: var(--grey-600);
|
||||
opacity: 0.5;
|
||||
transition: opacity 0.3s;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
&.swiper-pagination-bullet-active {
|
||||
background: var(--color-txt);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.hero {
|
||||
margin-bottom: calc(var(--spacing) * 1);
|
||||
padding: calc(var(--spacing) * 0.5) 0;
|
||||
border-top: var(--border-light);
|
||||
border-bottom: var(--border-light);
|
||||
|
||||
.p__baseline-big {
|
||||
margin: 0;
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
|
||||
.add-to-cart {
|
||||
margin: 0;
|
||||
border-bottom: var(--border-light);
|
||||
padding: calc(var(--spacing) * 0.5) 0;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,109 +1,46 @@
|
|||
/**
|
||||
* Product Add to Cart functionality
|
||||
* Handles the add to cart button interaction with Shopify
|
||||
*/
|
||||
(function() {
|
||||
// Initialize Shopify Cart
|
||||
const cart = new ShopifyCart({
|
||||
domain: 'nv7cqv-bu.myshopify.com',
|
||||
storefrontAccessToken: 'dec3d35a2554384d149c72927d1cfd1b'
|
||||
});
|
||||
|
||||
// Get product ID from data attribute
|
||||
const addToCartBtn = document.querySelector('[data-shopify-add-to-cart]');
|
||||
|
||||
if (!addToCartBtn) {
|
||||
console.warn('Add to cart button not found');
|
||||
return;
|
||||
}
|
||||
|
||||
const productId = addToCartBtn.dataset.productId;
|
||||
const variantId = addToCartBtn.dataset.variantId;
|
||||
const stockDisplay = document.querySelector('[data-product-stock]');
|
||||
|
||||
// Get translated texts
|
||||
const texts = {
|
||||
add: addToCartBtn.dataset.textAdd || 'Add to cart',
|
||||
adding: addToCartBtn.dataset.textAdding || 'Adding...',
|
||||
added: addToCartBtn.dataset.textAdded || 'Added! ✓',
|
||||
error: addToCartBtn.dataset.textError || 'Error - Try again',
|
||||
outOfStock: addToCartBtn.dataset.textOutOfStock || 'Out of stock',
|
||||
inStock: addToCartBtn.dataset.textInStock || 'In stock'
|
||||
error: addToCartBtn.dataset.textError || 'Error - Try again'
|
||||
};
|
||||
|
||||
// Load product data to check availability
|
||||
async function loadProductData() {
|
||||
try {
|
||||
const product = await cart.getProduct(productId);
|
||||
|
||||
if (!product) {
|
||||
console.error('Product not found');
|
||||
return;
|
||||
}
|
||||
|
||||
// Find the specific variant or use the first one
|
||||
let variant;
|
||||
if (variantId) {
|
||||
variant = product.variants.edges.find(
|
||||
edge => edge.node.id === `gid://shopify/ProductVariant/${variantId}`
|
||||
)?.node;
|
||||
} else {
|
||||
variant = product.variants.edges[0]?.node;
|
||||
}
|
||||
|
||||
if (!variant) {
|
||||
console.error('Variant not found');
|
||||
return;
|
||||
}
|
||||
|
||||
// Update button based on availability
|
||||
if (!variant.availableForSale) {
|
||||
addToCartBtn.disabled = true;
|
||||
addToCartBtn.textContent = texts.outOfStock;
|
||||
addToCartBtn.classList.add('out-of-stock');
|
||||
if (stockDisplay) {
|
||||
stockDisplay.textContent = texts.outOfStock;
|
||||
stockDisplay.classList.add('out-of-stock');
|
||||
}
|
||||
} else {
|
||||
// Show in stock
|
||||
if (stockDisplay) {
|
||||
stockDisplay.textContent = texts.inStock;
|
||||
stockDisplay.classList.add('in-stock');
|
||||
}
|
||||
}
|
||||
|
||||
// Store variant ID for later use
|
||||
addToCartBtn.dataset.variantId = variant.id.replace('gid://shopify/ProductVariant/', '');
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error loading product:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle add to cart click
|
||||
addToCartBtn.addEventListener('click', async function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
// Disable button during request
|
||||
const variantId = this.dataset.variantId;
|
||||
|
||||
if (!variantId) {
|
||||
console.error('No variant ID found');
|
||||
return;
|
||||
}
|
||||
|
||||
addToCartBtn.disabled = true;
|
||||
const originalText = addToCartBtn.textContent;
|
||||
addToCartBtn.textContent = texts.adding;
|
||||
|
||||
try {
|
||||
const variantId = this.dataset.variantId;
|
||||
const cartResult = await cart.addToCart(variantId, 1);
|
||||
|
||||
// Show success feedback
|
||||
addToCartBtn.textContent = texts.added;
|
||||
addToCartBtn.classList.add('success');
|
||||
|
||||
// Dispatch event to open cart drawer
|
||||
document.dispatchEvent(new CustomEvent('cart:updated', {
|
||||
detail: { cart: cartResult }
|
||||
}));
|
||||
|
||||
// Reset button after short delay
|
||||
setTimeout(() => {
|
||||
addToCartBtn.disabled = false;
|
||||
addToCartBtn.textContent = originalText;
|
||||
|
|
@ -113,11 +50,9 @@
|
|||
} catch (error) {
|
||||
console.error('Error adding to cart:', error);
|
||||
|
||||
// Show error feedback
|
||||
addToCartBtn.textContent = texts.error;
|
||||
addToCartBtn.classList.add('error');
|
||||
|
||||
// Re-enable button after delay
|
||||
setTimeout(() => {
|
||||
addToCartBtn.disabled = false;
|
||||
addToCartBtn.textContent = originalText;
|
||||
|
|
@ -125,7 +60,4 @@
|
|||
}, 2000);
|
||||
}
|
||||
});
|
||||
|
||||
// Load product data on page load
|
||||
loadProductData();
|
||||
})();
|
||||
|
|
|
|||
188
assets/js/product-loader.js
Normal file
188
assets/js/product-loader.js
Normal file
|
|
@ -0,0 +1,188 @@
|
|||
(async function () {
|
||||
const container = document.querySelector("[data-product-loader]");
|
||||
if (!container) return;
|
||||
|
||||
const handle = container.dataset.shopifyHandle;
|
||||
const language = container.dataset.language || 'fr';
|
||||
const isEnglish = language === 'en';
|
||||
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);
|
||||
|
||||
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);
|
||||
renderVariants(product);
|
||||
setupAddToCart(product);
|
||||
renderStock(product);
|
||||
}
|
||||
|
||||
function renderTitle(product, isEnglish) {
|
||||
const titleEl = document.querySelector("[data-product-title]");
|
||||
if (titleEl) {
|
||||
const title = isEnglish && product.titleEn?.value
|
||||
? product.titleEn.value
|
||||
: product.title;
|
||||
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) {
|
||||
const description = isEnglish && product.descriptionEn?.value
|
||||
? product.descriptionEn.value
|
||||
: product.descriptionHtml || "";
|
||||
detailsEl.innerHTML = description;
|
||||
}
|
||||
}
|
||||
|
||||
function renderImages(product, isEnglish) {
|
||||
const imagesContainer = document.querySelector("[data-product-images]");
|
||||
|
||||
if (imagesContainer && product.images.edges.length > 0) {
|
||||
const productTitle = isEnglish && product.titleEn?.value
|
||||
? product.titleEn.value
|
||||
: product.title;
|
||||
|
||||
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("");
|
||||
}
|
||||
}
|
||||
|
||||
function renderVariants(product) {
|
||||
if (product.variants.edges.length <= 1) return;
|
||||
|
||||
const variantsContainer = document.querySelector("[data-product-variants]");
|
||||
const variantSelector = document.querySelector("[data-variant-selector]");
|
||||
|
||||
if (!variantsContainer || !variantSelector) return;
|
||||
|
||||
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;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
function renderStock(product) {
|
||||
const stockEl = document.querySelector("[data-product-stock]");
|
||||
if (!stockEl) return;
|
||||
|
||||
const addToCartBtn = document.querySelector("[data-shopify-add-to-cart]");
|
||||
|
||||
if (product.availableForSale) {
|
||||
stockEl.textContent = addToCartBtn?.dataset.textInStock || "En stock";
|
||||
stockEl.classList.add("in-stock");
|
||||
} else {
|
||||
stockEl.textContent =
|
||||
addToCartBtn?.dataset.textOutOfStock || "Rupture de stock";
|
||||
stockEl.classList.add("out-of-stock");
|
||||
}
|
||||
}
|
||||
})();
|
||||
47
assets/js/products-list-loader.js
Normal file
47
assets/js/products-list-loader.js
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
(async function() {
|
||||
const container = document.querySelector('[data-products-loader]');
|
||||
if (!container) return;
|
||||
|
||||
const language = (container.dataset.language || 'FR').toLowerCase();
|
||||
const isEnglish = language === 'en';
|
||||
|
||||
try {
|
||||
const cart = new ShopifyCart({
|
||||
domain: 'nv7cqv-bu.myshopify.com',
|
||||
storefrontAccessToken: 'dec3d35a2554384d149c72927d1cfd1b'
|
||||
});
|
||||
|
||||
const productsData = await cart.getAllProducts();
|
||||
const products = productsData.edges;
|
||||
|
||||
const productsHtml = products.map(edge => {
|
||||
const product = edge.node;
|
||||
const image = product.images.edges[0]?.node;
|
||||
const price = parseFloat(product.priceRange.minVariantPrice.amount).toFixed(2);
|
||||
const slug = product.handle;
|
||||
const productUrl = isEnglish ? `/en/${slug}` : `/${slug}`;
|
||||
const productTitle = isEnglish && product.titleEn?.value
|
||||
? product.titleEn.value
|
||||
: product.title;
|
||||
|
||||
return `
|
||||
<article class="store__product">
|
||||
<figure>
|
||||
${image ? `<img src="${image.url}" alt="${image.altText || productTitle}" loading="lazy" />` : ''}
|
||||
</figure>
|
||||
<p class="line-1">
|
||||
<a href="${productUrl}">${productTitle}</a>
|
||||
</p>
|
||||
<p class="price">${price}€</p>
|
||||
<a href="${productUrl}" class="link-block" aria-hidden="true"></a>
|
||||
</article>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
container.innerHTML = productsHtml;
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error loading products:', error);
|
||||
container.innerHTML = '<p>Erreur lors du chargement des produits</p>';
|
||||
}
|
||||
})();
|
||||
|
|
@ -73,6 +73,117 @@ class ShopifyCart {
|
|||
return data.product;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get product by handle
|
||||
* @param {string} handle - Product handle (slug)
|
||||
*/
|
||||
async getProductByHandle(handle) {
|
||||
const query = `
|
||||
query getProductByHandle($handle: String!) {
|
||||
product(handle: $handle) {
|
||||
id
|
||||
handle
|
||||
title
|
||||
description
|
||||
descriptionHtml
|
||||
availableForSale
|
||||
tags
|
||||
titleEn: metafield(namespace: "custom", key: "title_en") {
|
||||
value
|
||||
}
|
||||
descriptionEn: metafield(namespace: "custom", key: "description_en") {
|
||||
value
|
||||
}
|
||||
priceRange {
|
||||
minVariantPrice {
|
||||
amount
|
||||
currencyCode
|
||||
}
|
||||
}
|
||||
images(first: 10) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
url
|
||||
altText
|
||||
width
|
||||
height
|
||||
}
|
||||
}
|
||||
}
|
||||
variants(first: 20) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
title
|
||||
sku
|
||||
availableForSale
|
||||
price {
|
||||
amount
|
||||
currencyCode
|
||||
}
|
||||
selectedOptions {
|
||||
name
|
||||
value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const data = await this.query(query, { handle });
|
||||
return data.product || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all products for listing page
|
||||
* @param {number} first - Number of products to fetch
|
||||
*/
|
||||
async getAllProducts(first = 20) {
|
||||
const query = `
|
||||
query getAllProducts($first: Int!) {
|
||||
products(first: $first, sortKey: TITLE) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
handle
|
||||
title
|
||||
description
|
||||
availableForSale
|
||||
titleEn: metafield(namespace: "custom", key: "title_en") {
|
||||
value
|
||||
}
|
||||
priceRange {
|
||||
minVariantPrice {
|
||||
amount
|
||||
currencyCode
|
||||
}
|
||||
}
|
||||
images(first: 1) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
url
|
||||
altText
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
pageInfo {
|
||||
hasNextPage
|
||||
endCursor
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const data = await this.query(query, { first });
|
||||
return data.products;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new cart
|
||||
*/
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue