From 51c1aa2f6f7307f9e7955ef7aaa182bfec025c99 Mon Sep 17 00:00:00 2001 From: Serreau Jovann Date: Thu, 15 Jan 2026 18:23:53 +0100 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat(assets/app.js):=20Supprime=20l?= =?UTF-8?q?e=20code=20relatif=20aux=20notifications=20push=20et=20aux=20co?= =?UTF-8?q?okies.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- assets/app.js | 584 +------------------------------------------------- 1 file changed, 1 insertion(+), 583 deletions(-) diff --git a/assets/app.js b/assets/app.js index cdd8ed7..a5910fb 100644 --- a/assets/app.js +++ b/assets/app.js @@ -2,593 +2,11 @@ import './app.scss' import * as Turbo from "@hotwired/turbo" -// --- CLÉS DE STOCKAGE ET VAPID --- -const VAPID_PUBLIC_KEY = "BKz0kdcsG6kk9KxciPpkfP8kEDAd408inZecij5kBDbQ1ZGZSNwS4KZ8FerC28LFXvgSqpDXtor3ePo0zBCdNqo"; -const COOKIE_STORAGE_KEY = 'cookies_accepted'; // Clé pour le consentement des cookies - - -// --- MESSAGES ET TRADUCTIONS (fr et en) --- -const MESSAGES = { - fr: { - // Notifications - notificationTitle: "🔔 Activer les notifications", - notificationText: "Recevez les nouvelles, les promotions et les événements de l'association.", - notificationButton: "Activer", - notificationClose: "Fermer la notification", - - // Cookies - cookieText: 'Ce site utilise uniquement des cookies de fonctionnement et de sécurité (e.g. Cloudflare) essentiels pour son bon fonctionnement. Aucune donnée personnelle n\'est collectée.', - cookieLink: "Politique de cookies", - cookieButton: "Accepter", - }, - en: { - // Notifications - notificationTitle: "🔔 Enable Notifications", - notificationText: "Receive news, promotions, and association events.", - notificationButton: "Enable", - notificationClose: "Close notification", - - // Cookies - cookieText: 'This website only uses functional and security cookies (e.g. Cloudflare) essential for its proper operation. No personal data is collected.', - cookieLink: "Cookie Policy", - cookieButton: "Accept", - } -}; - -/** - * Détermine la langue à utiliser, en privilégiant l'attribut 'lang' du document HTML, - * puis la langue du navigateur. Retourne l'ensemble de messages correspondant. - * @returns {object} L'objet de messages pour la langue sélectionnée. - */ -function getLanguageMessages() { - let langCode = 'fr'; // Français par défaut - - // 1. Tente de lire la langue depuis l'attribut 'lang' du tag - const htmlLang = document.documentElement.lang; - if (htmlLang) { - // On prend les deux premiers caractères (ex: 'en-US' devient 'en') - const normalizedHtmlLang = htmlLang.toLowerCase().substring(0, 2); - if (normalizedHtmlLang === 'en') { - langCode = 'en'; - } - } else { - // 2. Si l'attribut 'lang' est manquant, utilise la langue du navigateur - const userLang = navigator.language || navigator.userLanguage; - if (userLang && userLang.toLowerCase().startsWith('en')) { - langCode = 'en'; - } - } - - return MESSAGES[langCode]; -} -// --- FIN MESSAGES ET TRADUCTIONS --- - - -/** - * Convertit une chaîne Base64 URL Safe en Uint8Array. - * Nécessaire pour passer la clé VAPID publique à pushManager.subscribe(). - * @param {string} base64String - * @returns {Uint8Array} - */ -function urlBase64ToUint8Array(base64String) { - const padding = '='.repeat((4 - base64String.length % 4) % 4); - const base64 = (base64String + padding) - .replace(/\-/g, '+') - .replace(/_/g, '/'); - - const rawData = window.atob(base64); - const outputArray = new Uint8Array(rawData.length); - - for (let i = 0; i < rawData.length; ++i) { - outputArray[i] = rawData.charCodeAt(i); - } - return outputArray; -} - - -/** - * Fonction générique pour basculer la visibilité d'un menu déroulant. - * @param {HTMLElement} button - Le bouton qui déclenche l'action. - * @param {HTMLElement} menu - Le menu à afficher/masquer. - */ -function toggleMenu(button, menu) { - if (!button || !menu) return; - const isExpanded = button.getAttribute('aria-expanded') === 'true' || false; - button.setAttribute('aria-expanded', !isExpanded); - menu.classList.toggle('hidden'); -} - -/** - * Fonction d'initialisation pour les composants qui DOIVENT être réinitialisés - * après un chargement Turbo (comme les compteurs d'articles, les états initiaux). - * Le menu mobile et le panier sont gérés par délégation d'événements. - */ -function initializeUI() { - // Réinitialisation des états des menus cachés après un chargement Turbo - document.querySelectorAll('#mobile-menu, #userMenuDesktop, #userMenuMobile').forEach(menu => { - if (!menu.classList.contains('hidden')) { - menu.classList.add('hidden'); - } - }); - document.querySelectorAll('#mobileMenuButton, #userMenuButtonDesktop, #userMenuButtonMobile').forEach(button => { - button.setAttribute('aria-expanded', 'false'); - }); - - - // --- 2. Gestion du Panier Latéral (Off-Canvas) --- - const cartSidebar = document.getElementById('cartSidebar'); - const cartBackdrop = document.getElementById('cartBackdrop'); - const closeCartButton = document.getElementById('closeCartButton'); - - // Mettez les fonctions ici pour qu'elles soient toujours définies si les éléments existent - if (cartSidebar && cartBackdrop && closeCartButton) { - function openCart() { - document.body.style.overflow = 'hidden'; - cartBackdrop.classList.remove('hidden'); - cartSidebar.classList.remove('translate-x-full'); - cartSidebar.classList.add('translate-x-0'); - } - - function closeCart() { - document.body.style.overflow = ''; - cartSidebar.classList.remove('translate-x-0'); - cartSidebar.classList.add('translate-x-full'); - setTimeout(() => { - cartBackdrop.classList.add('hidden'); - }, 300); - } - - // Stocker les fonctions dans une variable globale accessible par l'écouteur du document - window.openCart = openCart; - window.closeCart = closeCart; - } else { - // Sécurité si les éléments du panier n'existent pas - window.openCart = null; - window.closeCart = null; - } - - - // --- 3. Logique Panier Mock (Affichage du compteur) --- - function updateCartDisplay(count) { - const desktopCounter = document.getElementById('cartCountDesktop'); - const mobileCounter = document.getElementById('cartCountMobile'); - - if (desktopCounter) desktopCounter.textContent = count; - if (mobileCounter) mobileCounter.textContent = count; - } - - // Simuler un panier non-vide au chargement (Mettre 0 pour un panier vide réel) - updateCartDisplay(0); - - // --- 4. Vérification de l'abonnement push au chargement --- - if ('Notification' in window && Notification.permission === 'granted') { - subscribeAndSave(); - } -} - -/** - * Tente d'abonner l'utilisateur aux notifications push via le Service Worker - * et envoie l'objet d'abonnement au backend (/notificationSub). - */ -async function subscribeAndSave() { - if (!('Notification' in window) || Notification.permission !== 'granted') { - console.log("Les notifications ne sont pas supportées ou la permission n'est pas accordée."); - return; - } - - if (!('serviceWorker' in navigator) || !('PushManager' in window)) { - console.error("Le Service Worker ou PushManager n'est pas disponible pour l'abonnement."); - return; - } - - try { - const registration = await navigator.serviceWorker.ready; - let subscription = await registration.pushManager.getSubscription(); - - // 1. Si aucun abonnement n'existe, on essaie d'en créer un. - if (!subscription) { - console.log("Aucun abonnement existant trouvé. Tentative de nouvel abonnement..."); - - const applicationServerKey = urlBase64ToUint8Array(VAPID_PUBLIC_KEY); - - subscription = await registration.pushManager.subscribe({ - userVisibleOnly: true, - applicationServerKey: applicationServerKey - }); - - if (!subscription) { - console.error("Échec de la création de l'abonnement. Le Service Worker est-il correctement enregistré et la clé VAPID valide ?"); - return; - } - } else { - console.log("Abonnement push existant trouvé. Vérification/Mise à jour auprès du serveur."); - } - - // 2. Envoi (ou mise à jour) de l'abonnement au backend - const payload = { subscription: subscription.toJSON() }; - - const response = await fetch('/notificationSub', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(payload) - }); - - if (response.ok) { - console.log("-> Abonnement aux notifications sauvegardé (ou vérifié) avec succès sur le serveur. <-"); - } else { - console.error("-> Erreur lors de la sauvegarde de l'abonnement:", response.status, response.statusText); - } - - } catch (error) { - console.error("Erreur lors de l'obtention de l'abonnement ou de l'enregistrement:", error); - } -} - -/** - * Tente de demander la permission si nécessaire et d'appeler l'abonnement. - * (Utilisée par le clic du bandeau) - */ -async function promptForPermissionAndSubscribe() { - if (!('Notification' in window)) { - console.error("Les Notifications ne sont pas supportées par ce navigateur."); - return; - } - - // Demander la permission - try { - const permission = await Notification.requestPermission(); - - if (permission === 'granted') { - console.log("-> Permission de notification accordée. Lancement de l'abonnement. <-"); - await subscribeAndSave(); - } else { - console.log(`-> Permission de notification refusée ou ignorée (${permission}). <-`); - } - } catch (error) { - console.error("Erreur lors de la demande de permission:", error); - } -} - -// ==================================================================== -// --- NOUVELLE FONCTION DE DÉTECTION POUR PAGESPEED/LIGHTHOUSE --- -// ==================================================================== - -/** - * Vérifie si le script est exécuté par un User Agent de test (e.g., Lighthouse/PageSpeed) - * @returns {boolean} True si c'est un User Agent de test. - */ -function isPerformanceTestAgent() { - const ua = navigator.userAgent; - // Vérifie les User Agents connus des outils de performance - return ua.includes('Lighthouse') || ua.includes('Chrome-Lighthouse') || ua.includes('PageSpeed') || ua.includes('moto g power') || ua.includes('Intel Mac OS X 10_15_7'); -} - - -/** - * Affiche une petite carte de notification push temporaire en bas à gauche. - * N'affiche QUE si la permission n'est PAS accordée. - */ -function handleNotificationBanner() { - // 0. MASQUAGE POUR PAGESPEED - if (isPerformanceTestAgent()) { - console.log("Notification Banner skipped for performance test agent."); - return; - } - // --- FIN MASQUAGE --- - - const BANNER_ID = 'notification-prompt-banner'; - const DURATION_MS = 15000; // 15 secondes d'affichage - const M = getLanguageMessages(); // Récupère les messages traduits - - // 1. NE PAS AFFICHER si la permission est déjà accordée - if ('Notification' in window && Notification.permission === 'granted') { - return; - } - - // 2. Si le bandeau existe déjà, on quitte. - if (document.getElementById(BANNER_ID)) { - return; - } - - // 3. Créer le conteneur du message - const banner = document.createElement('div'); - banner.id = BANNER_ID; - banner.className = `fixed bottom-4 left-4 z-50 p-4 max-w-xs - bg-indigo-600 text-white rounded-xl shadow-2xl - transition-all duration-500 transform - opacity-0 translate-y-full - md:left-8 md:bottom-8`; // Style initial (masqué) - - banner.innerHTML = ` -
-

- ${M.notificationTitle} -

- -
-

- ${M.notificationText} -

- - `; - - document.body.appendChild(banner); - - // 4. Fonctions d'animation et de gestion - const hideBanner = () => { - // Déclenche l'animation de disparition - banner.classList.remove('opacity-100', 'translate-y-0'); - banner.classList.add('opacity-0', 'translate-y-full'); - // Supprime après l'animation pour nettoyer le DOM - setTimeout(() => { - if (document.body.contains(banner)) { - document.body.removeChild(banner); - } - }, 600); - }; - - // Clic sur le bouton de fermeture - document.getElementById('closeNotificationBanner').addEventListener('click', () => { - hideBanner(); - }); - - // Clic sur le bouton d'activation -> Logique Push - document.getElementById('activateNotifications').addEventListener('click', async () => { - await promptForPermissionAndSubscribe(); - // Fermer le bandeau après l'interaction (que ce soit accordé ou refusé) - hideBanner(); - }); - - // 5. Affichage et Timer - - // Montre le bandeau (déclencher l'animation) - setTimeout(() => { - banner.classList.remove('opacity-0', 'translate-y-full'); - banner.classList.add('opacity-100', 'translate-y-0'); - }, 100); - - // Cache le bandeau automatiquement après la durée définie - setTimeout(hideBanner, DURATION_MS); -} - - -/** - * Affiche le bandeau de consentement aux cookies en bas à droite s'il n'a jamais été accepté. - */ -function handleCookieBanner() { - // 0. MASQUAGE POUR PAGESPEED - if (isPerformanceTestAgent()) { - console.log("Cookie Banner skipped for performance test agent."); - return; - } - // --- FIN MASQUAGE --- - - const BANNER_ID = 'cookie-banner'; - const M = getLanguageMessages(); // Récupère les messages traduits - - // 1. NE PAS AFFICHER si le consentement est déjà enregistré - if (localStorage.getItem(COOKIE_STORAGE_KEY) === 'true') { - return; - } - - // 2. Si le bandeau existe déjà, on quitte. - if (document.getElementById(BANNER_ID)) { - return; - } - - // 3. Créer le conteneur du message (position bottom-right) - const banner = document.createElement('div'); - banner.id = BANNER_ID; - banner.className = `fixed bottom-4 right-4 z-50 p-4 max-w-sm - bg-white text-gray-800 rounded-xl shadow-2xl - transition-all duration-500 transform - opacity-0 translate-y-full - border border-gray-100 - md:right-8 md:bottom-8`; // Style initial (masqué) - - // Utilisation de M.cookieText, M.cookieLink et M.cookieButton - // MODIFICATION APPORTÉE ICI : Ajout de py-2 (padding vertical) au lien "En savoir plus" - banner.innerHTML = ` -

- ${M.cookieText} -

-
- - ${M.cookieLink} - - -
- `; - - document.body.appendChild(banner); - - // 4. Fonctions d'animation et de gestion - const hideBanner = () => { - // Déclenche l'animation de disparition - banner.classList.remove('opacity-100', 'translate-y-0'); - banner.classList.add('opacity-0', 'translate-y-full'); - // Supprime après l'animation - setTimeout(() => { - if (document.body.contains(banner)) { - document.body.removeChild(banner); - } - }, 600); - }; - - // Clic sur le bouton d'acceptation - document.getElementById('acceptCookies').addEventListener('click', () => { - localStorage.setItem(COOKIE_STORAGE_KEY, 'true'); - hideBanner(); - }); - - // 5. Affichage - // Montre le bandeau (déclencher l'animation) - setTimeout(() => { - banner.classList.remove('opacity-0', 'translate-y-full'); - banner.classList.add('opacity-100', 'translate-y-0'); - }, 100); -} - - // --- INITIALISATION DES COMPOSANTS APRÈS TURBO/CHARGEMENT --- document.addEventListener('DOMContentLoaded', ()=>{ - // initializeUI appelle subscribeAndSave si la permission est accordée - initializeUI() - if (!isPerformanceTestAgent()) { - // Gère le bandeau de cookies (bottom-right) - handleCookieBanner() - } - /*if (!isPerformanceTestAgent()) { - var BASE_URL_WOOT = "https://app.chatwoot.com"; - let script = document.createElement('script'); - script.setAttribute('src', BASE_URL_WOOT + "/packs/js/sdk.js") - script.setAttribute('sync', true) - document.head.append(script) - script.onload = function () { - window.chatwootSDK.run({ - websiteToken: '8SXvcdoWJVA77hug4mT5JhAP', - baseUrl: BASE_URL_WOOT - }) - } - } - - const env = document.querySelector('meta[name="env"]') - if(env.getAttribute('content') == "prod") { - if (typeof navigator.serviceWorker !== 'undefined') { - // Assurez-vous que le Service Worker est bien enregistré en mode prod - navigator.serviceWorker.register('sw.js') - } - }*/ }); + document.addEventListener('turbo:load', () => { - initializeUI(); - handleNotificationBanner(); - handleCookieBanner(); // Doit être appelé à chaque chargement Turbo -}); - - -// ==================================================================== -// --- DÉLÉGATION D'ÉVÉNEMENTS (Gestion des clics une seule fois) --- -// ==================================================================== - -document.addEventListener('click', (event) => { - const target = event.target; - - // --- 1. GESTION DU MENU MOBILE (Burger) --- - const mobileMenuButton = document.getElementById('mobileMenuButton'); - const mobileMenu = document.getElementById('mobile-menu'); - - // On vérifie si la cible cliquée est le bouton ou un de ses enfants - if (mobileMenuButton && mobileMenu && (target === mobileMenuButton || mobileMenuButton.contains(target))) { - event.preventDefault(); - toggleMenu(mobileMenuButton, mobileMenu); - return; - } - - // --- 2. GESTION DU MENU UTILISATEUR (Dropdown) --- - const userMenuButtonDesktop = document.getElementById('userMenuButtonDesktop'); - const userMenuDesktop = document.getElementById('userMenuDesktop'); - const userMenuButtonMobile = document.getElementById('userMenuButtonMobile'); - const userMenuMobile = document.getElementById('userMenuMobile'); - - // Ouverture/Fermeture du menu utilisateur Desktop - if (userMenuButtonDesktop && userMenuDesktop && (target === userMenuButtonDesktop || userMenuButtonDesktop.contains(target))) { - event.preventDefault(); - // S'assurer que les autres menus sont fermés - userMenuMobile.classList.add('hidden'); - toggleMenu(userMenuButtonDesktop, userMenuDesktop); - return; - } - - // Ouverture/Fermeture du menu utilisateur Mobile - if (userMenuButtonMobile && userMenuMobile && (target === userMenuButtonMobile || userMenuButtonMobile.contains(target))) { - event.preventDefault(); - // S'assurer que les autres menus sont fermés - userMenuDesktop.classList.add('hidden'); - toggleMenu(userMenuButtonMobile, userMenuMobile); - return; - } - - // Fermeture des menus s'il y a un clic en dehors - const isClickInsideDesktopMenu = userMenuDesktop && (userMenuDesktop.contains(target) || userMenuButtonDesktop.contains(target)); - const isClickInsideMobileMenu = userMenuMobile && (userMenuMobile.contains(target) || userMenuButtonMobile.contains(target)); - - if (userMenuDesktop && userMenuButtonDesktop && !isClickInsideDesktopMenu) { - userMenuDesktop.classList.add('hidden'); - userMenuButtonDesktop.setAttribute('aria-expanded', 'false'); - } - if (userMenuMobile && userMenuButtonMobile && !isClickInsideMobileMenu) { - userMenuMobile.classList.add('hidden'); - userMenuButtonMobile.setAttribute('aria-expanded', 'false'); - } - - // --- 3. GESTION DE L'OUVERTURE ET FERMETURE DU PANIER --- - const openCartDesktop = document.getElementById('openCartDesktop'); - const openCartMobile = document.getElementById('openCartMobile'); - const closeCartButton = document.getElementById('closeCartButton'); - const cartBackdrop = document.getElementById('cartBackdrop'); - - // Ouverture (Desktop ou Mobile) - if (window.openCart && ( - (openCartDesktop && (target === openCartDesktop || openCartDesktop.contains(target))) || - (openCartMobile && (target === openCartMobile || openCartMobile.contains(target))) - )) { - event.preventDefault(); - window.openCart(); - return; - } - - // Fermeture (Bouton interne) - if (window.closeCart && closeCartButton && (target === closeCartButton || closeCartButton.contains(target))) { - event.preventDefault(); - window.closeCart(); - return; - } - - // Fermeture (Cliquer sur le fond/backdrop) - if (window.closeCart && target === cartBackdrop) { - window.closeCart(); - return; - } }); - -// --- GESTION GLOBALE DE LA TOUCHE ESC (Une seule fois) --- -document.addEventListener('keydown', (event) => { - const cartSidebar = document.getElementById('cartSidebar'); - // Fermer le panier - if (cartSidebar && window.closeCart && event.key === 'Escape' && !cartSidebar.classList.contains('translate-x-full')) { - window.closeCart(); - return; - } - - // Fermer les menus utilisateur - const userMenuDesktop = document.getElementById('userMenuDesktop'); - const userMenuButtonDesktop = document.getElementById('userMenuButtonDesktop'); - const userMenuMobile = document.getElementById('userMenuMobile'); - const userMenuButtonMobile = document.getElementById('userMenuButtonMobile'); - - if (event.key === 'Escape') { - if (userMenuDesktop && !userMenuDesktop.classList.contains('hidden')) { - toggleMenu(userMenuButtonDesktop, userMenuDesktop); - return; - } - if (userMenuMobile && !userMenuMobile.classList.contains('hidden')) { - toggleMenu(userMenuButtonMobile, userMenuMobile); - return; - } - } -});