Implement custom Shopify cart with drawer UI
Replace Shopify Buy Button iframe with custom implementation using Storefront API 2026-01. Create interactive cart drawer with full product management capabilities (add, remove, update quantities) and seamless checkout flow. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
c08662caf8
commit
28501fec7c
13 changed files with 1158 additions and 81 deletions
0
assets/css/components/_shopify-buy-button.scss
Normal file
0
assets/css/components/_shopify-buy-button.scss
Normal file
233
assets/css/components/_shopify-cart-drawer.scss
Normal file
233
assets/css/components/_shopify-cart-drawer.scss
Normal file
|
|
@ -0,0 +1,233 @@
|
|||
/* Cart Drawer Styles */
|
||||
.cart-drawer {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 9999;
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.cart-drawer.is-open {
|
||||
pointer-events: auto;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.cart-drawer__overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.cart-drawer__panel {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
max-width: 420px;
|
||||
background-color: #ffffff;
|
||||
box-shadow: -2px 0 8px rgba(0, 0, 0, 0.15);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
transform: translateX(100%);
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.cart-drawer.is-open .cart-drawer__panel {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
.cart-drawer__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 1.5rem;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.cart-drawer__header h3 {
|
||||
margin: 0;
|
||||
font-size: 1.5rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.cart-drawer__close {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
padding: 0.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
.cart-drawer__close:hover {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.cart-drawer__content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.cart-drawer__empty {
|
||||
text-align: center;
|
||||
padding: 3rem 1rem;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.cart-drawer__empty.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.cart-drawer__items {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.cart-drawer__items.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.cart-item {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
padding: 1rem;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.cart-item__image {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
object-fit: cover;
|
||||
border-radius: 4px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.cart-item__details {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.cart-item__title {
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.cart-item__variant {
|
||||
font-size: 0.875rem;
|
||||
color: #666;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.cart-item__price {
|
||||
font-weight: bold;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.cart-item__quantity {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
.cart-item__qty-btn {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border: 1px solid #ddd;
|
||||
background: #fff;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1rem;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.cart-item__qty-btn:hover:not(:disabled) {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.cart-item__qty-btn:disabled {
|
||||
opacity: 0.4;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.cart-item__qty-value {
|
||||
min-width: 30px;
|
||||
text-align: center;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.cart-item__remove {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #ff3333;
|
||||
cursor: pointer;
|
||||
padding: 0.25rem 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
text-decoration: underline;
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
.cart-item__remove:hover {
|
||||
color: #cc0000;
|
||||
}
|
||||
|
||||
.cart-drawer__footer {
|
||||
border-top: 1px solid #e0e0e0;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.cart-drawer__checkout-btn {
|
||||
width: 100%;
|
||||
font-family: "Open Sans", sans-serif;
|
||||
font-weight: bold;
|
||||
font-size: 1rem;
|
||||
color: #000000;
|
||||
background-color: #00ff00;
|
||||
border: none;
|
||||
border-radius: 40px;
|
||||
padding: 14px 34px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
.cart-drawer__checkout-btn:hover:not(:disabled) {
|
||||
background-color: #00e600;
|
||||
}
|
||||
|
||||
.cart-drawer__checkout-btn:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* Mobile responsiveness */
|
||||
@media (max-width: 768px) {
|
||||
.cart-drawer__panel {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
/* Loading state */
|
||||
.cart-drawer__content.is-loading {
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
|
@ -1258,8 +1258,238 @@ body.is-fullscreen {
|
|||
margin-top: calc(var(--spacing) * 4);
|
||||
}
|
||||
|
||||
.snipcart-modal__container {
|
||||
z-index: 1000;
|
||||
/* Cart Drawer Styles */
|
||||
.cart-drawer {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 9999;
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.cart-drawer.is-open {
|
||||
pointer-events: auto;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.cart-drawer__overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.cart-drawer__panel {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
max-width: 420px;
|
||||
background-color: #ffffff;
|
||||
box-shadow: -2px 0 8px rgba(0, 0, 0, 0.15);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
transform: translateX(100%);
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.cart-drawer.is-open .cart-drawer__panel {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
.cart-drawer__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 1.5rem;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.cart-drawer__header h3 {
|
||||
margin: 0;
|
||||
font-size: 1.5rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.cart-drawer__close {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
padding: 0.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
.cart-drawer__close:hover {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.cart-drawer__content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.cart-drawer__empty {
|
||||
text-align: center;
|
||||
padding: 3rem 1rem;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.cart-drawer__empty.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.cart-drawer__items {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.cart-drawer__items.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.cart-item {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
padding: 1rem;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.cart-item__image {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
-o-object-fit: cover;
|
||||
object-fit: cover;
|
||||
border-radius: 4px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.cart-item__details {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.cart-item__title {
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.cart-item__variant {
|
||||
font-size: 0.875rem;
|
||||
color: #666;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.cart-item__price {
|
||||
font-weight: bold;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.cart-item__quantity {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
.cart-item__qty-btn {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border: 1px solid #ddd;
|
||||
background: #fff;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1rem;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.cart-item__qty-btn:hover:not(:disabled) {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.cart-item__qty-btn:disabled {
|
||||
opacity: 0.4;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.cart-item__qty-value {
|
||||
min-width: 30px;
|
||||
text-align: center;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.cart-item__remove {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #ff3333;
|
||||
cursor: pointer;
|
||||
padding: 0.25rem 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
text-decoration: underline;
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
.cart-item__remove:hover {
|
||||
color: #cc0000;
|
||||
}
|
||||
|
||||
.cart-drawer__footer {
|
||||
border-top: 1px solid #e0e0e0;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.cart-drawer__checkout-btn {
|
||||
width: 100%;
|
||||
font-family: "Open Sans", sans-serif;
|
||||
font-weight: bold;
|
||||
font-size: 1rem;
|
||||
color: #000000;
|
||||
background-color: #00ff00;
|
||||
border: none;
|
||||
border-radius: 40px;
|
||||
padding: 14px 34px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
.cart-drawer__checkout-btn:hover:not(:disabled) {
|
||||
background-color: #00e600;
|
||||
}
|
||||
|
||||
.cart-drawer__checkout-btn:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* Mobile responsiveness */
|
||||
@media (max-width: 768px) {
|
||||
.cart-drawer__panel {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
/* Loading state */
|
||||
.cart-drawer__content.is-loading {
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
[data-template=subscription-newsletter] main {
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -22,6 +22,7 @@
|
|||
@import "template/shop/layout";
|
||||
@import "template/shop/section--product";
|
||||
@import "template/shop/thanks";
|
||||
// @import "template/shop/snipcart"; // Snipcart désactivé - voir assets/snipcart-archive/README.md
|
||||
@import "components/shopify-buy-button.scss";
|
||||
@import "components/shopify-cart-drawer.scss";
|
||||
|
||||
@import "template/subscription-newsletter/layout";
|
||||
|
|
|
|||
|
|
@ -1,3 +0,0 @@
|
|||
.snipcart-modal__container {
|
||||
z-index: 1000;
|
||||
}
|
||||
214
assets/js/cart-drawer.js
Normal file
214
assets/js/cart-drawer.js
Normal file
|
|
@ -0,0 +1,214 @@
|
|||
/**
|
||||
* Cart Drawer Component
|
||||
* Manages the cart sidebar with add/remove/update functionality
|
||||
*/
|
||||
(function() {
|
||||
const drawer = document.getElementById('cart-drawer');
|
||||
const emptyState = document.querySelector('[data-cart-empty]');
|
||||
const itemsContainer = document.querySelector('[data-cart-items]');
|
||||
const checkoutBtn = document.querySelector('[data-cart-checkout]');
|
||||
const closeButtons = document.querySelectorAll('[data-cart-close]');
|
||||
|
||||
let currentCart = null;
|
||||
let cartInstance = null;
|
||||
|
||||
// Wait for ShopifyCart to be available
|
||||
function initCartDrawer() {
|
||||
if (typeof ShopifyCart === 'undefined') {
|
||||
setTimeout(initCartDrawer, 100);
|
||||
return;
|
||||
}
|
||||
|
||||
cartInstance = new ShopifyCart({
|
||||
domain: 'nv7cqv-bu.myshopify.com',
|
||||
storefrontAccessToken: 'dec3d35a2554384d149c72927d1cfd1b'
|
||||
});
|
||||
|
||||
// Initialize event listeners
|
||||
setupEventListeners();
|
||||
}
|
||||
|
||||
function setupEventListeners() {
|
||||
// Close drawer
|
||||
closeButtons.forEach(btn => {
|
||||
btn.addEventListener('click', closeDrawer);
|
||||
});
|
||||
|
||||
// Checkout button
|
||||
checkoutBtn.addEventListener('click', () => {
|
||||
if (currentCart?.checkoutUrl) {
|
||||
window.location.href = currentCart.checkoutUrl;
|
||||
}
|
||||
});
|
||||
|
||||
// Listen for custom cart update events
|
||||
document.addEventListener('cart:updated', (e) => {
|
||||
currentCart = e.detail.cart;
|
||||
renderCart();
|
||||
openDrawer();
|
||||
});
|
||||
}
|
||||
|
||||
function openDrawer() {
|
||||
drawer.classList.add('is-open');
|
||||
document.body.style.overflow = 'hidden';
|
||||
}
|
||||
|
||||
function closeDrawer() {
|
||||
drawer.classList.remove('is-open');
|
||||
document.body.style.overflow = '';
|
||||
}
|
||||
|
||||
function renderCart() {
|
||||
if (!currentCart || !currentCart.lines || currentCart.lines.edges.length === 0) {
|
||||
emptyState.classList.remove('hidden');
|
||||
itemsContainer.classList.add('hidden');
|
||||
checkoutBtn.disabled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
emptyState.classList.add('hidden');
|
||||
itemsContainer.classList.remove('hidden');
|
||||
checkoutBtn.disabled = false;
|
||||
|
||||
// Render cart items
|
||||
itemsContainer.innerHTML = currentCart.lines.edges.map(edge => {
|
||||
const item = edge.node;
|
||||
const merchandise = item.merchandise;
|
||||
|
||||
return `
|
||||
<div class="cart-item" data-line-id="${item.id}">
|
||||
<div class="cart-item__details">
|
||||
<h4 class="cart-item__title">${merchandise.product.title}</h4>
|
||||
${merchandise.title !== 'Default Title' ? `<p class="cart-item__variant">${merchandise.title}</p>` : ''}
|
||||
<p class="cart-item__price">${merchandise.price.amount} ${merchandise.price.currencyCode}</p>
|
||||
|
||||
<div class="cart-item__quantity">
|
||||
<button class="cart-item__qty-btn" data-action="decrease" data-line-id="${item.id}">−</button>
|
||||
<span class="cart-item__qty-value">${item.quantity}</span>
|
||||
<button class="cart-item__qty-btn" data-action="increase" data-line-id="${item.id}">+</button>
|
||||
</div>
|
||||
|
||||
<button class="cart-item__remove" data-action="remove" data-line-id="${item.id}">
|
||||
Retirer
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
// Attach event listeners to quantity buttons
|
||||
attachQuantityListeners();
|
||||
}
|
||||
|
||||
function attachQuantityListeners() {
|
||||
const buttons = itemsContainer.querySelectorAll('[data-action]');
|
||||
|
||||
buttons.forEach(btn => {
|
||||
btn.addEventListener('click', async (e) => {
|
||||
const action = e.target.dataset.action;
|
||||
const lineId = e.target.dataset.lineId;
|
||||
|
||||
await handleQuantityChange(action, lineId);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function handleQuantityChange(action, lineId) {
|
||||
if (!cartInstance || !currentCart) return;
|
||||
|
||||
// Find the line item
|
||||
const line = currentCart.lines.edges.find(edge => edge.node.id === lineId);
|
||||
if (!line) return;
|
||||
|
||||
const currentQty = line.node.quantity;
|
||||
let newQty = currentQty;
|
||||
|
||||
if (action === 'increase') {
|
||||
newQty = currentQty + 1;
|
||||
} else if (action === 'decrease') {
|
||||
newQty = Math.max(0, currentQty - 1);
|
||||
} else if (action === 'remove') {
|
||||
newQty = 0;
|
||||
}
|
||||
|
||||
// Update cart via API
|
||||
try {
|
||||
itemsContainer.classList.add('is-loading');
|
||||
|
||||
const query = `
|
||||
mutation cartLinesUpdate($cartId: ID!, $lines: [CartLineUpdateInput!]!) {
|
||||
cartLinesUpdate(cartId: $cartId, lines: $lines) {
|
||||
cart {
|
||||
id
|
||||
checkoutUrl
|
||||
lines(first: 10) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
quantity
|
||||
merchandise {
|
||||
... on ProductVariant {
|
||||
id
|
||||
title
|
||||
price {
|
||||
amount
|
||||
currencyCode
|
||||
}
|
||||
product {
|
||||
title
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
userErrors {
|
||||
field
|
||||
message
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const data = await cartInstance.query(query, {
|
||||
cartId: currentCart.id,
|
||||
lines: [{
|
||||
id: lineId,
|
||||
quantity: newQty
|
||||
}]
|
||||
});
|
||||
|
||||
if (data.cartLinesUpdate.userErrors.length > 0) {
|
||||
throw new Error(data.cartLinesUpdate.userErrors[0].message);
|
||||
}
|
||||
|
||||
currentCart = data.cartLinesUpdate.cart;
|
||||
renderCart();
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error updating cart:', error);
|
||||
alert('Erreur lors de la mise à jour du panier');
|
||||
} finally {
|
||||
itemsContainer.classList.remove('is-loading');
|
||||
}
|
||||
}
|
||||
|
||||
// Public API
|
||||
window.CartDrawer = {
|
||||
open: openDrawer,
|
||||
close: closeDrawer,
|
||||
updateCart: (cart) => {
|
||||
currentCart = cart;
|
||||
renderCart();
|
||||
}
|
||||
};
|
||||
|
||||
// Initialize when DOM is ready
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initCartDrawer);
|
||||
} else {
|
||||
initCartDrawer();
|
||||
}
|
||||
})();
|
||||
121
assets/js/product-add-to-cart.js
Normal file
121
assets/js/product-add-to-cart.js
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
/**
|
||||
* 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]');
|
||||
|
||||
// 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 = 'Rupture de stock';
|
||||
addToCartBtn.classList.add('out-of-stock');
|
||||
if (stockDisplay) {
|
||||
stockDisplay.textContent = 'Rupture de stock';
|
||||
stockDisplay.classList.add('out-of-stock');
|
||||
}
|
||||
} else {
|
||||
// Show in stock
|
||||
if (stockDisplay) {
|
||||
stockDisplay.textContent = 'En stock';
|
||||
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
|
||||
addToCartBtn.disabled = true;
|
||||
const originalText = addToCartBtn.textContent;
|
||||
addToCartBtn.textContent = 'Ajout en cours...';
|
||||
|
||||
try {
|
||||
const variantId = this.dataset.variantId;
|
||||
const cartResult = await cart.addToCart(variantId, 1);
|
||||
|
||||
// Show success feedback
|
||||
addToCartBtn.textContent = 'Ajouté ! ✓';
|
||||
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;
|
||||
addToCartBtn.classList.remove('success');
|
||||
}, 1500);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error adding to cart:', error);
|
||||
|
||||
// Show error feedback
|
||||
addToCartBtn.textContent = 'Erreur - Réessayer';
|
||||
addToCartBtn.classList.add('error');
|
||||
|
||||
// Re-enable button after delay
|
||||
setTimeout(() => {
|
||||
addToCartBtn.disabled = false;
|
||||
addToCartBtn.textContent = originalText;
|
||||
addToCartBtn.classList.remove('error');
|
||||
}, 2000);
|
||||
}
|
||||
});
|
||||
|
||||
// Load product data on page load
|
||||
loadProductData();
|
||||
})();
|
||||
232
assets/js/shopify-cart.js
Normal file
232
assets/js/shopify-cart.js
Normal file
|
|
@ -0,0 +1,232 @@
|
|||
/**
|
||||
* Shopify Storefront API Client
|
||||
* Custom implementation using Cart API (2026-01)
|
||||
*/
|
||||
class ShopifyCart {
|
||||
constructor(config) {
|
||||
this.domain = config.domain;
|
||||
this.storefrontAccessToken = config.storefrontAccessToken;
|
||||
this.apiVersion = '2026-01';
|
||||
this.endpoint = `https://${this.domain}/api/${this.apiVersion}/graphql.json`;
|
||||
this.cartId = null;
|
||||
this.cartItems = [];
|
||||
|
||||
// Load existing cart from localStorage
|
||||
this.loadCart();
|
||||
}
|
||||
|
||||
/**
|
||||
* Make GraphQL request to Shopify Storefront API
|
||||
*/
|
||||
async query(query, variables = {}) {
|
||||
const response = await fetch(this.endpoint, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Shopify-Storefront-Access-Token': this.storefrontAccessToken,
|
||||
},
|
||||
body: JSON.stringify({ query, variables }),
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.errors) {
|
||||
console.error('Shopify API Error:', result.errors);
|
||||
throw new Error(result.errors[0].message);
|
||||
}
|
||||
|
||||
return result.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get product information by ID
|
||||
*/
|
||||
async getProduct(productId) {
|
||||
const query = `
|
||||
query getProduct($id: ID!) {
|
||||
product(id: $id) {
|
||||
id
|
||||
title
|
||||
description
|
||||
availableForSale
|
||||
variants(first: 10) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
title
|
||||
price {
|
||||
amount
|
||||
currencyCode
|
||||
}
|
||||
availableForSale
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const data = await this.query(query, {
|
||||
id: `gid://shopify/Product/${productId}`
|
||||
});
|
||||
|
||||
return data.product;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new cart
|
||||
*/
|
||||
async createCart(lines = []) {
|
||||
const query = `
|
||||
mutation cartCreate($input: CartInput!) {
|
||||
cartCreate(input: $input) {
|
||||
cart {
|
||||
id
|
||||
checkoutUrl
|
||||
lines(first: 10) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
quantity
|
||||
merchandise {
|
||||
... on ProductVariant {
|
||||
id
|
||||
title
|
||||
price {
|
||||
amount
|
||||
currencyCode
|
||||
}
|
||||
product {
|
||||
title
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
userErrors {
|
||||
field
|
||||
message
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const data = await this.query(query, {
|
||||
input: { lines }
|
||||
});
|
||||
|
||||
if (data.cartCreate.userErrors.length > 0) {
|
||||
throw new Error(data.cartCreate.userErrors[0].message);
|
||||
}
|
||||
|
||||
this.cartId = data.cartCreate.cart.id;
|
||||
this.saveCart();
|
||||
|
||||
return data.cartCreate.cart;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add item to cart
|
||||
*/
|
||||
async addToCart(variantId, quantity = 1) {
|
||||
const lines = [{
|
||||
merchandiseId: `gid://shopify/ProductVariant/${variantId}`,
|
||||
quantity: quantity
|
||||
}];
|
||||
|
||||
let cart;
|
||||
|
||||
if (this.cartId) {
|
||||
// Add to existing cart
|
||||
const query = `
|
||||
mutation cartLinesAdd($cartId: ID!, $lines: [CartLineInput!]!) {
|
||||
cartLinesAdd(cartId: $cartId, lines: $lines) {
|
||||
cart {
|
||||
id
|
||||
checkoutUrl
|
||||
lines(first: 10) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
quantity
|
||||
merchandise {
|
||||
... on ProductVariant {
|
||||
id
|
||||
title
|
||||
price {
|
||||
amount
|
||||
currencyCode
|
||||
}
|
||||
product {
|
||||
title
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
userErrors {
|
||||
field
|
||||
message
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const data = await this.query(query, {
|
||||
cartId: this.cartId,
|
||||
lines
|
||||
});
|
||||
|
||||
if (data.cartLinesAdd.userErrors.length > 0) {
|
||||
throw new Error(data.cartLinesAdd.userErrors[0].message);
|
||||
}
|
||||
|
||||
cart = data.cartLinesAdd.cart;
|
||||
} else {
|
||||
// Create new cart
|
||||
cart = await this.createCart(lines);
|
||||
}
|
||||
|
||||
this.cartItems = cart.lines.edges;
|
||||
return cart;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get checkout URL to redirect user
|
||||
*/
|
||||
getCheckoutUrl(cart) {
|
||||
return cart?.checkoutUrl || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save cart ID to localStorage
|
||||
*/
|
||||
saveCart() {
|
||||
if (this.cartId) {
|
||||
localStorage.setItem('shopify_cart_id', this.cartId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load cart ID from localStorage
|
||||
*/
|
||||
loadCart() {
|
||||
this.cartId = localStorage.getItem('shopify_cart_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear cart
|
||||
*/
|
||||
clearCart() {
|
||||
this.cartId = null;
|
||||
this.cartItems = [];
|
||||
localStorage.removeItem('shopify_cart_id');
|
||||
}
|
||||
}
|
||||
|
||||
// Export for use in other scripts
|
||||
window.ShopifyCart = ShopifyCart;
|
||||
85
site/snippets/buy-button--t-shirt.php
Normal file
85
site/snippets/buy-button--t-shirt.php
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
<div class="product-purchase">
|
||||
<div class="product-stock-info">
|
||||
<p data-product-stock class="stock-status"></p>
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="btn-add-to-cart"
|
||||
data-shopify-add-to-cart
|
||||
data-product-id="15689076179317"
|
||||
data-variant-id=""
|
||||
>
|
||||
Ajouter au panier
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.product-purchase {
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.product-stock-info {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.stock-status {
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.stock-status.in-stock {
|
||||
color: #00cc00;
|
||||
}
|
||||
|
||||
.stock-status.low-stock {
|
||||
color: #ff9900;
|
||||
}
|
||||
|
||||
.stock-status.out-of-stock {
|
||||
color: #ff3333;
|
||||
}
|
||||
|
||||
.btn-add-to-cart {
|
||||
font-family: "Open Sans", sans-serif;
|
||||
font-weight: bold;
|
||||
font-size: 1rem;
|
||||
color: #000000;
|
||||
background-color: #00ff00;
|
||||
border: none;
|
||||
border-radius: 40px;
|
||||
padding: 12px 34px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s ease;
|
||||
width: 100%;
|
||||
max-width: 300px;
|
||||
}
|
||||
|
||||
.btn-add-to-cart:hover:not(:disabled) {
|
||||
background-color: #00e600;
|
||||
}
|
||||
|
||||
.btn-add-to-cart:focus {
|
||||
outline: 2px solid #00e600;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.btn-add-to-cart:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.btn-add-to-cart.success {
|
||||
background-color: #00cc00;
|
||||
}
|
||||
|
||||
.btn-add-to-cart.error {
|
||||
background-color: #ff3333;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.btn-add-to-cart.out-of-stock {
|
||||
background-color: #cccccc;
|
||||
color: #666666;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,3 +1,33 @@
|
|||
<!-- Cart Drawer -->
|
||||
<div id="cart-drawer" class="cart-drawer">
|
||||
<div class="cart-drawer__overlay" data-cart-close></div>
|
||||
<div class="cart-drawer__panel">
|
||||
<div class="cart-drawer__header">
|
||||
<h3>Panier</h3>
|
||||
<button class="cart-drawer__close" data-cart-close aria-label="Fermer le panier">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<line x1="18" y1="6" x2="6" y2="18"></line>
|
||||
<line x1="6" y1="6" x2="18" y2="18"></line>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="cart-drawer__content">
|
||||
<div class="cart-drawer__empty" data-cart-empty>
|
||||
<p>Votre panier est vide</p>
|
||||
</div>
|
||||
|
||||
<div class="cart-drawer__items" data-cart-items></div>
|
||||
</div>
|
||||
|
||||
<div class="cart-drawer__footer">
|
||||
<button class="cart-drawer__checkout-btn" data-cart-checkout>
|
||||
Passer commande
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer id="site-footer">
|
||||
<div class="site-footer__container">
|
||||
<div class="footer__mentions">
|
||||
|
|
@ -10,6 +40,8 @@
|
|||
</footer>
|
||||
|
||||
<script src="<?= url('assets/js/onload.js') ?>"></script>
|
||||
<script src="<?= url('assets/js/shopify-cart.js') ?>"></script>
|
||||
<script src="<?= url('assets/js/cart-drawer.js') ?>"></script>
|
||||
<?php if(isset($scripts) && is_array($scripts)): ?>
|
||||
<?php foreach($scripts as $script): ?>
|
||||
<script src="<?= url($script) ?>"></script>
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@
|
|||
|
||||
<link rel="stylesheet" type="text/css" href="<?= url('assets/fonts/stylesheet.css') ?>?version-cache-prevent<?= rand(0, 1000)?>" />
|
||||
<link rel="stylesheet" type="text/css" href="<?= url('assets/css/style.css') ?>" />
|
||||
<link rel="stylesheet" type="text/css" href="<?= url('assets/css/cart-drawer.css') ?>" />
|
||||
</head>
|
||||
<body data-template="<?= $page->template() ?>">
|
||||
<header id="site-header">
|
||||
|
|
|
|||
|
|
@ -18,75 +18,7 @@
|
|||
<?php endif ?>
|
||||
</div>
|
||||
|
||||
<?php if($page->hasOptions()->toBool() && $page->optionValues()->isNotEmpty()): ?>
|
||||
<div class="product-options">
|
||||
<ul class="product-options__list">
|
||||
<?php
|
||||
$values = $page->optionValues()->split(',');
|
||||
$optionSlug = $page->optionLabel()->slug();
|
||||
foreach($values as $index => $value):
|
||||
$value = trim($value);
|
||||
$uniqueId = $optionSlug . '-' . Str::slug(strtolower($value));
|
||||
?>
|
||||
<li>
|
||||
<input type="radio" id="<?= $uniqueId ?>" name="<?= $optionSlug ?>" value="<?= $value ?>" />
|
||||
<label for="<?= $uniqueId ?>"><?= $value ?></label>
|
||||
</li>
|
||||
<?php endforeach ?>
|
||||
</ul>
|
||||
</div>
|
||||
<?php endif ?>
|
||||
|
||||
<div class="add-to-cart">
|
||||
<!-- Snipcart désactivé - voir assets/snipcart-archive/README.md pour restauration -->
|
||||
<button
|
||||
class="btn__default"
|
||||
disabled
|
||||
<?php
|
||||
/*
|
||||
// SNIPCART - Décommenter pour réactiver
|
||||
class="btn__default snipcart-add-item"
|
||||
data-item-id="<?= $page->slug() ?>"
|
||||
data-item-price="<?= $page->price() ?>"
|
||||
data-item-description="<?= $page->description()->excerpt(100) ?>"
|
||||
data-item-image="<?= $page->images()->first() ? $page->images()->first()->url() : '' ?>"
|
||||
data-item-name="<?= $page->title()->html() ?>"
|
||||
data-item-shippable="true"
|
||||
data-item-weight="<?= $page->weight()->or(0) ?>"
|
||||
data-item-length="<?= $page->length()->or(0) ?>"
|
||||
data-item-width="<?= $page->width()->or(0) ?>"
|
||||
data-item-height="<?= $page->height()->or(0) ?>"
|
||||
*/
|
||||
?>
|
||||
<?php
|
||||
/*
|
||||
// SNIPCART OPTIONS - Décommenter pour réactiver
|
||||
if($page->hasOptions()->toBool() && $page->optionValues()->isNotEmpty()):
|
||||
$values = $page->optionValues()->split(',');
|
||||
$trimmedValues = array_map('trim', $values);
|
||||
$snipcartOptions = implode('|', $trimmedValues);
|
||||
?>
|
||||
data-item-custom1-name="<?= $page->optionLabel()->html() ?>"
|
||||
data-item-custom1-options="<?= $snipcartOptions ?>"
|
||||
data-item-custom1-required="true"
|
||||
disabled
|
||||
<?php endif; */
|
||||
?>
|
||||
>
|
||||
<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-.530c-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-default-text="<?= t('addToCart', 'Ajouter au panier') ?>">
|
||||
<?php if($page->hasOptions()->toBool() && $page->optionValues()->isNotEmpty()): ?>
|
||||
<?= t('chooseOption', 'Choisissez une option') ?>
|
||||
<?php else: ?>
|
||||
<?= t('addToCart', 'Ajouter au panier') ?>
|
||||
<?php endif ?>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<?php snippet('buy-button--t-shirt') ?>
|
||||
</div>
|
||||
|
||||
<div class="product-gallery swiper">
|
||||
|
|
@ -122,8 +54,7 @@
|
|||
</section>
|
||||
</main>
|
||||
|
||||
<?php
|
||||
// Snipcart désactivé - voir assets/snipcart-archive/README.md
|
||||
// Pour réactiver: ajouter 'assets/js/product-size.js', 'assets/js/snipcart.js' dans le tableau
|
||||
snippet('footer', ['scripts' => ['assets/js/product-gallery.js']])
|
||||
?>
|
||||
<?php snippet('footer', ['scripts' => [
|
||||
'assets/js/product-add-to-cart.js',
|
||||
'assets/js/product-gallery.js'
|
||||
]]) ?>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue