diff --git a/assets/css/style.scss b/assets/css/style.scss index 9687a73..2e517e4 100644 --- a/assets/css/style.scss +++ b/assets/css/style.scss @@ -22,6 +22,6 @@ @import "template/shop/layout"; @import "template/shop/section--product"; @import "template/shop/thanks"; -@import "template/shop/snipcart"; +// @import "template/shop/snipcart"; // Snipcart désactivé - voir assets/snipcart-archive/README.md @import "template/subscription-newsletter/layout"; diff --git a/assets/snipcart-archive/README.md b/assets/snipcart-archive/README.md new file mode 100644 index 0000000..3b1c388 --- /dev/null +++ b/assets/snipcart-archive/README.md @@ -0,0 +1,164 @@ +# Archive Snipcart + +Cette archive contient tous les fichiers et le code nécessaires pour réactiver l'intégration Snipcart si besoin. + +## Date d'archivage +13 janvier 2026 + +## Raison de l'archivage +Remplacement par l'intégration Shopify Buy Button + +--- + +## Fichiers archivés + +### Fichiers JavaScript +- `snipcart.js` - Loader Snipcart avec configuration de la clé API +- `product-size.js` - Gestion des options produit (tailles, etc.) pour Snipcart + +### Fichiers SCSS +- `_snipcart.scss` - Styles pour le modal Snipcart + +--- + +## Comment restaurer l'intégration Snipcart + +### 1. Restaurer les fichiers JavaScript + +#### Fichier: `assets/js/snipcart.js` +Copier le fichier depuis l'archive: +```bash +cp assets/snipcart-archive/snipcart.js assets/js/ +``` + +#### Fichier: `assets/js/product-size.js` +Copier le fichier depuis l'archive: +```bash +cp assets/snipcart-archive/product-size.js assets/js/ +``` + +### 2. Restaurer les styles SCSS + +#### Fichier: `assets/css/template/shop/_snipcart.scss` +Copier le fichier depuis l'archive: +```bash +cp assets/snipcart-archive/_snipcart.scss assets/css/template/shop/ +``` + +Puis décommenter l'import dans `assets/css/style.scss`: +```scss +// Décommenter cette ligne: +@import 'template/shop/snipcart'; +``` + +### 3. Restaurer le template produit + +#### Fichier: `site/templates/product.php` + +1. **Restaurer le bouton "Ajouter au panier" avec les attributs Snipcart** (ligne ~40-78): + - Décommenter tous les attributs `data-item-*` du bouton + - Ajouter la classe `snipcart-add-item` au bouton + +2. **Restaurer les scripts dans le footer** (ligne 114): + ```php + ['assets/js/product-size.js', 'assets/js/snipcart.js', 'assets/js/product-gallery.js']]) ?> + ``` + +### 4. Restaurer les routes et webhooks + +#### Fichier: `site/config/config.php` + +Décommenter les routes Snipcart (lignes 39-147): + +1. **Route de validation produit** (`validate.json`): + - Permet à Snipcart de valider les prix et stock + - Route: `(:any)/validate.json` + +2. **Webhook Snipcart**: + - Gère les événements de commande (décrémente le stock) + - Route: `snipcart-webhook` + +### 5. Configuration Snipcart + +#### Clé API publique +La clé API publique Snipcart est dans `snipcart.js`: +```javascript +publicApiKey: 'NGU4ODQ3MjAtY2MzMC00MWEyLWI2YTMtNjBmNGYzMTBlOTZkNjM4OTY1NDY4OTE5MTQyMTI3' +``` + +#### Configuration du webhook +Pour que le webhook fonctionne, il faut: +1. Configurer l'URL du webhook dans le dashboard Snipcart +2. URL: `https://votre-domaine.com/snipcart-webhook` +3. Événements à écouter: `order.completed` + +### 6. Vérifications après restauration + +- [ ] Les fichiers JS sont présents dans `assets/js/` +- [ ] Le fichier SCSS est présent et importé +- [ ] Les boutons "Ajouter au panier" ont la classe `snipcart-add-item` +- [ ] Les attributs `data-item-*` sont présents sur les boutons +- [ ] Les routes sont décommentées dans `config.php` +- [ ] Le CSS de Snipcart est compilé +- [ ] Le webhook est configuré dans le dashboard Snipcart +- [ ] Les traductions sont restaurées dans `site/languages/en.php` et `fr.php` + +--- + +## Fonctionnalités Snipcart implémentées + +### Gestion des produits +- Affichage du prix +- Options de produit (tailles, couleurs, etc.) +- Validation des options obligatoires +- Images produit + +### Gestion du panier +- Ajout au panier +- Gestion du stock +- Calcul des frais de port (basé sur poids/dimensions) +- Validation des prix côté serveur + +### Gestion des commandes +- Webhook pour décrémenter le stock automatiquement +- Redirection vers page de remerciement après paiement +- Token de commande dans l'URL + +### Multi-langue +- Support FR/EN +- Redirection post-paiement avec détection de la langue + +--- + +## Dépendances externes + +### CDN Snipcart +Snipcart est chargé depuis le CDN officiel: +- Version: 3.0 +- JS: `https://cdn.snipcart.com/themes/v3.0/default/snipcart.js` +- CSS: `https://cdn.snipcart.com/themes/v3.0/default/snipcart.css` + +### Stratégie de chargement +- `loadStrategy: 'on-user-interaction'` +- Chargement différé pour optimiser les performances +- Timeout: 2750ms + +--- + +## Notes importantes + +1. **Sécurité**: Le webhook devrait valider la signature Snipcart en production (voir commentaire dans `config.php`) + +2. **Stock**: Le système décrémente automatiquement le stock via le webhook `order.completed` + +3. **Validation**: Chaque produit expose une route `validate.json` pour que Snipcart puisse vérifier les prix + +4. **Multi-langue**: La redirection post-paiement détecte automatiquement la langue depuis l'URL + +--- + +## Support + +Pour toute question sur Snipcart: +- Documentation: https://docs.snipcart.com/ +- Support: https://snipcart.com/support diff --git a/assets/snipcart-archive/_snipcart.scss b/assets/snipcart-archive/_snipcart.scss new file mode 100644 index 0000000..aab74c7 --- /dev/null +++ b/assets/snipcart-archive/_snipcart.scss @@ -0,0 +1,3 @@ +.snipcart-modal__container { + z-index: 1000; +} diff --git a/assets/snipcart-archive/product-size.js b/assets/snipcart-archive/product-size.js new file mode 100644 index 0000000..399ffce --- /dev/null +++ b/assets/snipcart-archive/product-size.js @@ -0,0 +1,66 @@ +/** + * Gestion de la sélection des options produit + * Met à jour les attributs Snipcart et gère les classes CSS + */ + +(function() { + 'use strict'; + + /** + * Initialise la gestion des options + */ + function initOptionSelector() { + const optionsContainer = document.querySelector('.product-options'); + const addToCartButton = document.querySelector('.snipcart-add-item'); + + if (!addToCartButton) { + return; + } + + // Si pas d'options, le bouton est déjà actif + if (!optionsContainer) { + return; + } + + const radios = optionsContainer.querySelectorAll('input[type="radio"]'); + + // Réinitialiser toutes les options (important pour le cache navigateur) + radios.forEach(radio => { + radio.checked = false; + }); + + // Retirer la classe is-selected de tous les li + const allLi = optionsContainer.querySelectorAll('li'); + allLi.forEach(li => li.classList.remove('is-selected')); + + // S'assurer que le bouton est désactivé au départ + addToCartButton.setAttribute('disabled', 'disabled'); + + // Écouter les changements de sélection + radios.forEach(radio => { + radio.addEventListener('change', function() { + // Mettre à jour l'attribut Snipcart + addToCartButton.setAttribute('data-item-custom1-value', this.value); + + // Activer le bouton + addToCartButton.removeAttribute('disabled'); + + // Changer le texte du bouton + const buttonText = addToCartButton.querySelector('.txt'); + if (buttonText) { + buttonText.textContent = buttonText.getAttribute('data-default-text') || 'Ajouter au panier'; + } + + // Gérer la classe is-selected sur les li parents + allLi.forEach(li => li.classList.remove('is-selected')); + this.closest('li').classList.add('is-selected'); + }); + }); + } + + /** + * Initialisation au chargement de la page + */ + document.addEventListener('DOMContentLoaded', initOptionSelector); + +})(); diff --git a/assets/snipcart-archive/snipcart.js b/assets/snipcart-archive/snipcart.js new file mode 100644 index 0000000..13eef65 --- /dev/null +++ b/assets/snipcart-archive/snipcart.js @@ -0,0 +1,94 @@ +window.SnipcartSettings = { + publicApiKey: + 'NGU4ODQ3MjAtY2MzMC00MWEyLWI2YTMtNjBmNGYzMTBlOTZkNjM4OTY1NDY4OTE5MTQyMTI3', + loadStrategy: 'on-user-interaction', +}; + +// Redirection après paiement réussi +document.addEventListener('snipcart.ready', function() { + Snipcart.events.on('cart.confirmed', function(cartState) { + // Détecter la langue actuelle depuis l'URL + const currentPath = window.location.pathname; + const langMatch = currentPath.match(/^\/([a-z]{2})(\/|$)/); + const langPrefix = langMatch ? '/' + langMatch[1] : ''; + + window.location.href = langPrefix + '/thanks?order=' + cartState.token; + }); +}); + +(() => { + var c, d; + (d = (c = window.SnipcartSettings).version) != null || (c.version = '3.0'); + var s, S; + (S = (s = window.SnipcartSettings).timeoutDuration) != null || + (s.timeoutDuration = 2750); + var l, p; + (p = (l = window.SnipcartSettings).domain) != null || + (l.domain = 'cdn.snipcart.com'); + var w, u; + (u = (w = window.SnipcartSettings).protocol) != null || + (w.protocol = 'https'); + var f = + window.SnipcartSettings.version.includes('v3.0.0-ci') || + (window.SnipcartSettings.version != '3.0' && + window.SnipcartSettings.version.localeCompare('3.4.0', void 0, { + numeric: !0, + sensitivity: 'base', + }) === -1), + m = ['focus', 'mouseover', 'touchmove', 'scroll', 'keydown']; + window.LoadSnipcart = o; + document.readyState === 'loading' + ? document.addEventListener('DOMContentLoaded', r) + : r(); + function r() { + window.SnipcartSettings.loadStrategy + ? window.SnipcartSettings.loadStrategy === 'on-user-interaction' && + (m.forEach((t) => document.addEventListener(t, o)), + setTimeout(o, window.SnipcartSettings.timeoutDuration)) + : o(); + } + var a = !1; + function o() { + if (a) return; + a = !0; + let t = document.getElementsByTagName('head')[0], + e = document.querySelector('#snipcart'), + i = document.querySelector( + `src[src^="${window.SnipcartSettings.protocol}://${window.SnipcartSettings.domain}"][src$="snipcart.js"]` + ), + n = document.querySelector( + `link[href^="${window.SnipcartSettings.protocol}://${window.SnipcartSettings.domain}"][href$="snipcart.css"]` + ); + e || + ((e = document.createElement('div')), + (e.id = 'snipcart'), + e.setAttribute('hidden', 'true'), + document.body.appendChild(e)), + v(e), + i || + ((i = document.createElement('script')), + (i.src = `${window.SnipcartSettings.protocol}://${window.SnipcartSettings.domain}/themes/v${window.SnipcartSettings.version}/default/snipcart.js`), + (i.async = !0), + t.appendChild(i)), + n || + ((n = document.createElement('link')), + (n.rel = 'stylesheet'), + (n.type = 'text/css'), + (n.href = `${window.SnipcartSettings.protocol}://${window.SnipcartSettings.domain}/themes/v${window.SnipcartSettings.version}/default/snipcart.css`), + t.prepend(n)), + m.forEach((g) => document.removeEventListener(g, o)); + } + function v(t) { + !f || + ((t.dataset.apiKey = window.SnipcartSettings.publicApiKey), + window.SnipcartSettings.addProductBehavior && + (t.dataset.configAddProductBehavior = + window.SnipcartSettings.addProductBehavior), + window.SnipcartSettings.modalStyle && + (t.dataset.configModalStyle = window.SnipcartSettings.modalStyle), + window.SnipcartSettings.currency && + (t.dataset.currency = window.SnipcartSettings.currency), + window.SnipcartSettings.templatesUrl && + (t.dataset.templatesUrl = window.SnipcartSettings.templatesUrl)); + } +})(); diff --git a/site/config/config.php b/site/config/config.php index 55d96fb..427755b 100644 --- a/site/config/config.php +++ b/site/config/config.php @@ -37,6 +37,8 @@ return [ ], 'routes' => [ + // SNIPCART ROUTES - Désactivées, voir assets/snipcart-archive/README.md pour restauration + /* [ 'pattern' => '(:any)/validate.json', 'method' => 'GET', @@ -144,5 +146,6 @@ return [ return Response::json(['status' => 'success'], 200); } ] + */ ] ]; diff --git a/site/templates/product.php b/site/templates/product.php index d836f63..0928abf 100644 --- a/site/templates/product.php +++ b/site/templates/product.php @@ -38,7 +38,13 @@
+