diff --git a/assets/app.js b/assets/app.js index 2a7eae9..0de7354 100644 --- a/assets/app.js +++ b/assets/app.js @@ -6,93 +6,51 @@ import * as Sentry from "@sentry/browser"; // --- CLÉS DE STOCKAGE ET VAPID --- const VAPID_PUBLIC_KEY = "BKz0kdcsG6kk9KxciPpkfP8kEDAd408inZecij5kBDbQ1ZGZSNwS4KZ8FerC28LFXvgSqpDXtor3ePo0zBCdNqo"; -const COOKIE_STORAGE_KEY = 'cookies_accepted'; // Clé pour le consentement des cookies +const COOKIE_STORAGE_KEY = 'cookies_accepted'; - -// --- MESSAGES ET TRADUCTIONS (fr et en) --- +// --- MESSAGES ET TRADUCTIONS --- 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.', + cookieText: 'Ce site utilise uniquement des cookies de fonctionnement et de sécurité essentiels. 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.', + cookieText: 'This website only uses functional and security cookies. 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 + let langCode = 'fr'; 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'; - } + if (normalizedHtmlLang === '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 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; @@ -101,500 +59,230 @@ function toggleMenu(button, menu) { } /** - * 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. + * Initialisation de l'UI */ function initializeUI() { - // Réinitialisation des états des menus cachés après un chargement Turbo + // Reset des menus au chargement 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'); + menu.classList.add('hidden'); }); - - // --- 2. Gestion du Panier Latéral (Off-Canvas) --- + // Panier Latéral 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() { + if (cartSidebar) { + window.openCart = () => { document.body.style.overflow = 'hidden'; - cartBackdrop.classList.remove('hidden'); + if (cartBackdrop) cartBackdrop.classList.remove('hidden'); cartSidebar.classList.remove('translate-x-full'); cartSidebar.classList.add('translate-x-0'); - } + }; - function closeCart() { + window.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; + if (cartBackdrop) { + setTimeout(() => cartBackdrop.classList.add('hidden'), 300); + } + }; } - - // --- 3. Logique Panier Mock (Affichage du compteur) --- - function updateCartDisplay(count) { + // Update counters + const 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). - */ +// --- LOGIQUE PUSH NOTIFICATIONS --- 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; - } + if (!('Notification' in window) || Notification.permission !== 'granted') return; + if (!('serviceWorker' in navigator)) 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', { + await fetch('/notificationSub', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(payload) + body: JSON.stringify({ subscription: subscription.toJSON() }) }); - - 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); + console.error("Push Error:", 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 + if (!('Notification' in window)) return; 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}). <-`); - } + if (permission === 'granted') await subscribeAndSave(); } catch (error) { - console.error("Erreur lors de la demande de permission:", error); + console.error("Permission Error:", 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. - */ +// --- OPTIMISATION PERFORMANCE --- 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'); + return ua.includes('Lighthouse') || ua.includes('PageSpeed'); } - -/** - * Affiche une petite carte de notification push temporaire en bas à gauche. - * N'affiche QUE si la permission n'est PAS accordée. - */ +// --- BANNIÈRES --- function handleNotificationBanner() { - // 0. MASQUAGE POUR PAGESPEED - if (isPerformanceTestAgent()) { - console.log("Notification Banner skipped for performance test agent."); - return; - } - // --- FIN MASQUAGE --- - + if (isPerformanceTestAgent()) return; const BANNER_ID = 'notification-prompt-banner'; - const DURATION_MS = 15000; // 15 secondes d'affichage - const M = getLanguageMessages(); // Récupère les messages traduits + if ('Notification' in window && Notification.permission === 'granted') return; + if (document.getElementById(BANNER_ID)) return; - // 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 M = getLanguageMessages(); 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.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 border-2 border-black`; banner.innerHTML = ` -
- ${M.notificationTitle} -
-