UNPKG

@tengerly/cookie-consent

Version:

A lightweight, customizable cookie consent solution for Next.js and modern web frameworks with comprehensive multi-language support

773 lines (676 loc) 28.3 kB
/** * Custom Cookie Consent Library * A lightweight, customizable cookie consent solution * Compatible with Next.js and modern web frameworks * * @version 1.0.0 * @author Tengerly */ class CookieConsent { constructor(options = {}) { this.options = { // Default configuration cookieName: 'portfolio_cookie_consent', cookieExpiryDays: 365, delay: 3000, autoShow: true, position: 'bottom-right', theme: 'default', // Translations translations: { en: { title: 'Cookie Settings', message: 'This website uses cookies to provide you with the best possible experience. We respect your privacy and give you full control over your data.', acceptAll: 'Accept All', acceptSome: 'Customize', reject: 'Essential Only', continue: 'Continue without consent', save: 'Save Settings', close: 'Close', functionalTitle: 'Functional Cookies', functionalDesc: 'Necessary for the proper functioning of the site (language, theme). Always enabled.', analyticsTitle: 'Analytics Cookies', analyticsDesc: 'Help us understand how visitors interact with our website.' }, fr: { title: 'Paramètres des cookies', message: 'Ce site utilise des cookies pour vous offrir la meilleure expérience possible. Nous respectons votre vie privée et vous donnons le contrôle total sur vos données.', acceptAll: 'Tout accepter', acceptSome: 'Personnaliser', reject: 'Essentiels uniquement', continue: 'Continuer sans consentement', save: 'Sauvegarder', close: 'Fermer', functionalTitle: 'Cookies Fonctionnels', functionalDesc: 'Nécessaires pour le bon fonctionnement du site (langue, thème). Toujours activés.', analyticsTitle: 'Cookies Analytiques', analyticsDesc: 'Nous aident à comprendre comment les visiteurs interagissent avec notre site web.' }, de: { title: 'Cookie-Einstellungen', message: 'Diese Website verwendet Cookies, um Ihnen die bestmögliche Erfahrung zu bieten. Wir respektieren Ihre Privatsphäre und geben Ihnen die volle Kontrolle über Ihre Daten.', acceptAll: 'Alle akzeptieren', acceptSome: 'Anpassen', reject: 'Nur notwendige', continue: 'Ohne Zustimmung fortfahren', save: 'Speichern', close: 'Schließen', functionalTitle: 'Funktionale Cookies', functionalDesc: 'Notwendig für das ordnungsgemäße Funktionieren der Website (Sprache, Theme). Immer aktiviert.', analyticsTitle: 'Analytische Cookies', analyticsDesc: 'Helfen uns zu verstehen, wie Besucher mit unserer Website interagieren.' }, es: { title: 'Configuración de Cookies', message: 'Este sitio web utiliza cookies para brindarle la mejor experiencia posible. Respetamos su privacidad y le damos control total sobre sus datos.', acceptAll: 'Aceptar todo', acceptSome: 'Personalizar', reject: 'Solo esenciales', continue: 'Continuar sin consentimiento', save: 'Guardar configuración', close: 'Cerrar', functionalTitle: 'Cookies Funcionales', functionalDesc: 'Necesarias para el funcionamiento adecuado del sitio (idioma, tema). Siempre habilitadas.', analyticsTitle: 'Cookies Analíticas', analyticsDesc: 'Nos ayudan a entender cómo los visitantes interactúan con nuestro sitio web.' }, it: { title: 'Impostazioni Cookie', message: 'Questo sito web utilizza i cookie per fornirvi la migliore esperienza possibile. Rispettiamo la vostra privacy e vi diamo il pieno controllo sui vostri dati.', acceptAll: 'Accetta tutto', acceptSome: 'Personalizza', reject: 'Solo essenziali', continue: 'Continua senza consenso', save: 'Salva impostazioni', close: 'Chiudi', functionalTitle: 'Cookie Funzionali', functionalDesc: 'Necessari per il corretto funzionamento del sito (lingua, tema). Sempre abilitati.', analyticsTitle: 'Cookie Analitici', analyticsDesc: 'Ci aiutano a capire come i visitatori interagiscono con il nostro sito web.' }, nl: { title: 'Cookie-instellingen', message: 'Deze website gebruikt cookies om u de best mogelijke ervaring te bieden. We respecteren uw privacy en geven u volledige controle over uw gegevens.', acceptAll: 'Alles accepteren', acceptSome: 'Aanpassen', reject: 'Alleen essentieel', continue: 'Doorgaan zonder toestemming', save: 'Instellingen opslaan', close: 'Sluiten', functionalTitle: 'Functionele Cookies', functionalDesc: 'Noodzakelijk voor de goede werking van de site (taal, thema). Altijd ingeschakeld.', analyticsTitle: 'Analytische Cookies', analyticsDesc: 'Helpen ons begrijpen hoe bezoekers omgaan met onze website.' }, pt: { title: 'Configurações de Cookies', message: 'Este site utiliza cookies para lhe proporcionar a melhor experiência possível. Respeitamos a sua privacidade e damos-lhe controlo total sobre os seus dados.', acceptAll: 'Aceitar tudo', acceptSome: 'Personalizar', reject: 'Apenas essenciais', continue: 'Continuar sem consentimento', save: 'Guardar configurações', close: 'Fechar', functionalTitle: 'Cookies Funcionais', functionalDesc: 'Necessários para o funcionamento adequado do site (idioma, tema). Sempre ativados.', analyticsTitle: 'Cookies Analíticos', analyticsDesc: 'Ajudam-nos a perceber como os visitantes interagem com o nosso site.' } }, // Callbacks onAcceptAll: null, onAcceptEssential: null, onAcceptCustom: null, onReject: null, // Override options ...options }; this.currentLanguage = this.options.language || this.detectLanguage(); this.isVisible = false; this.widget = null; this.modal = null; // Bind methods this.show = this.show.bind(this); this.hide = this.hide.bind(this); this.acceptAll = this.acceptAll.bind(this); this.acceptEssential = this.acceptEssential.bind(this); this.showPreferences = this.showPreferences.bind(this); this.hidePreferences = this.hidePreferences.bind(this); this.saveCustomPreferences = this.saveCustomPreferences.bind(this); this.init(); } detectLanguage() { // Get browser language const browserLang = (navigator.language || navigator.languages?.[0] || 'en').toLowerCase(); // Extract language code (e.g., 'en-US' -> 'en') const langCode = browserLang.split('-')[0]; // Check if we have translations for this language if (this.options.translations[langCode]) { return langCode; } // Fallback to English return 'en'; } init() { // Add CSS styles this.injectStyles(); // Auto-show after delay if no consent exists if (this.options.autoShow) { setTimeout(() => { if (!this.getConsent()) { this.show(); } }, this.options.delay); } } injectStyles() { if (document.getElementById('cookie-consent-styles')) return; const styles = ` <style id="cookie-consent-styles"> .cookie-consent { position: fixed; bottom: 20px; right: 20px; max-width: 400px; background: white; border-radius: 12px; box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2); z-index: 10000; transform: translateY(100px); opacity: 0; transition: all 0.3s ease; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; } .cookie-consent.show { transform: translateY(0); opacity: 1; } .cookie-header { padding: 20px 20px 10px; display: flex; align-items: flex-start; gap: 12px; } .cookie-icon { font-size: 24px; line-height: 1; } .cookie-content { flex: 1; } .cookie-title { font-size: 16px; font-weight: 600; margin: 0 0 8px 0; color: #1a1a1a; } .cookie-message { font-size: 14px; line-height: 1.4; margin: 0; color: #666; } .cookie-buttons { padding: 10px 20px 20px; display: flex; flex-direction: column; gap: 8px; } .cookie-btn { padding: 10px 16px; border: none; border-radius: 8px; font-size: 13px; font-weight: 500; cursor: pointer; transition: all 0.2s ease; background: #f5f5f5; color: #333; width: 100%; text-align: center; min-height: 40px; display: flex; align-items: center; justify-content: center; } .cookie-btn:hover { transform: translateY(-1px); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); } .cookie-btn-accept { background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%); color: white; order: 1; } .cookie-btn-choose { background: #e5e7eb; color: #374151; order: 2; } .cookie-btn-reject { background: #f3f4f6; color: #6b7280; order: 3; } .cookie-btn-continue { background: #f9fafb; color: #9ca3af; order: 4; font-size: 12px; } .cookie-preferences-modal { position: fixed; top: 0; left: 0; width: 100%; height: 100%; z-index: 10001; opacity: 0; visibility: hidden; transition: all 0.3s ease; } .cookie-preferences-modal.show { opacity: 1; visibility: visible; } .cookie-preferences-backdrop { position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.5); cursor: pointer; } .cookie-preferences-content { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%) scale(0.9); background: white; border-radius: 12px; box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3); max-width: 500px; width: 90%; max-height: 80vh; overflow-y: auto; transition: transform 0.3s ease; } .cookie-preferences-modal.show .cookie-preferences-content { transform: translate(-50%, -50%) scale(1); } .cookie-preferences-header { padding: 20px; border-bottom: 1px solid #e5e7eb; display: flex; align-items: flex-start; gap: 12px; } .cookie-close { background: none; border: none; font-size: 20px; cursor: pointer; color: #666; padding: 4px; margin-left: auto; } .cookie-preferences-body { padding: 20px; } .cookie-category { margin-bottom: 20px; padding-bottom: 20px; border-bottom: 1px solid #f3f4f6; } .cookie-category:last-child { border-bottom: none; margin-bottom: 0; } .cookie-category-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; } .cookie-category h4 { margin: 0; font-size: 16px; font-weight: 600; color: #1a1a1a; } .cookie-toggle { position: relative; } .cookie-toggle input { opacity: 0; position: absolute; } .toggle-label { display: block; width: 44px; height: 24px; background: #d1d5db; border-radius: 12px; cursor: pointer; transition: background 0.3s; position: relative; } .toggle-switch { position: absolute; top: 2px; left: 2px; width: 20px; height: 20px; background: white; border-radius: 50%; transition: transform 0.3s; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2); } .cookie-toggle input:checked + .toggle-label { background: #6366f1; } .cookie-toggle input:checked + .toggle-label .toggle-switch { transform: translateX(20px); } .cookie-toggle input:disabled + .toggle-label { opacity: 0.5; cursor: not-allowed; } .cookie-category-desc { font-size: 14px; color: #666; margin: 0; line-height: 1.4; } .cookie-preferences-footer { padding: 20px; border-top: 1px solid #e5e7eb; display: flex; flex-direction: column; gap: 8px; } .cookie-preferences-footer .cookie-btn { width: 100%; min-height: 44px; } @media (max-width: 768px) { .cookie-consent { bottom: 10px; right: 10px; left: 10px; max-width: none; } .cookie-buttons { flex-direction: column; gap: 6px; } .cookie-btn { width: 100%; text-align: center; min-height: 44px; padding: 12px 16px; } .cookie-preferences-footer { flex-direction: column; gap: 6px; } .cookie-preferences-footer .cookie-btn { min-height: 48px; } } </style> `; document.head.insertAdjacentHTML('beforeend', styles); } getTranslations() { return this.options.translations[this.currentLanguage] || this.options.translations['en']; } getAvailableLanguages() { return Object.keys(this.options.translations); } addCustomTranslation(languageCode, translations) { this.options.translations[languageCode] = translations; } show() { if (this.isVisible) return; this.remove(); // Remove any existing widgets const t = this.getTranslations(); this.widget = document.createElement('div'); this.widget.className = 'cookie-consent custom-cookie-widget'; this.widget.innerHTML = ` <div class="cookie-header"> <div class="cookie-icon">🍪</div> <div class="cookie-content"> <h3 class="cookie-title">${t.title}</h3> <p class="cookie-message">${t.message}</p> </div> </div> <div class="cookie-buttons"> <button class="cookie-btn cookie-btn-continue" data-action="essential">${t.continue}</button> <button class="cookie-btn cookie-btn-reject" data-action="essential">${t.reject}</button> <button class="cookie-btn cookie-btn-choose" data-action="preferences">${t.acceptSome}</button> <button class="cookie-btn cookie-btn-accept" data-action="all">${t.acceptAll}</button> </div> `; document.body.appendChild(this.widget); this.addEventListeners(this.widget); // Show with animation setTimeout(() => { this.widget.classList.add('show'); this.isVisible = true; }, 100); } hide() { if (!this.widget || !this.isVisible) return; this.widget.classList.remove('show'); setTimeout(() => { if (this.widget && this.widget.parentNode) { this.widget.remove(); this.widget = null; this.isVisible = false; } }, 300); } showPreferences() { this.hide(); // Hide main widget const t = this.getTranslations(); this.modal = document.createElement('div'); this.modal.className = 'cookie-preferences-modal'; this.modal.innerHTML = ` <div class="cookie-preferences-backdrop" data-action="close"></div> <div class="cookie-preferences-content"> <div class="cookie-preferences-header"> <div class="cookie-icon">🍪</div> <div class="cookie-content"> <h3 class="cookie-title">${t.title}</h3> <p class="cookie-message">${t.message}</p> </div> <button class="cookie-close" data-action="close"> ✕ </button> </div> <div class="cookie-preferences-body"> <div class="cookie-category"> <div class="cookie-category-header"> <h4>${t.functionalTitle}</h4> <div class="cookie-toggle"> <input type="checkbox" id="functional" checked disabled> <label for="functional" class="toggle-label"> <span class="toggle-switch"></span> </label> </div> </div> <p class="cookie-category-desc">${t.functionalDesc}</p> </div> <div class="cookie-category"> <div class="cookie-category-header"> <h4>${t.analyticsTitle}</h4> <div class="cookie-toggle"> <input type="checkbox" id="analytics"> <label for="analytics" class="toggle-label"> <span class="toggle-switch"></span> </label> </div> </div> <p class="cookie-category-desc">${t.analyticsDesc}</p> </div> </div> <div class="cookie-preferences-footer"> <button class="cookie-btn cookie-btn-continue" data-action="essential-close">${t.continue}</button> <button class="cookie-btn cookie-btn-reject" data-action="essential-close">${t.reject}</button> <button class="cookie-btn cookie-btn-save" data-action="save-close">${t.save}</button> <button class="cookie-btn cookie-btn-accept" data-action="all-close">${t.acceptAll}</button> </div> </div> `; document.body.appendChild(this.modal); this.addEventListeners(this.modal); // Show with animation setTimeout(() => { this.modal.classList.add('show'); }, 100); } hidePreferences() { if (!this.modal) return; this.modal.classList.remove('show'); setTimeout(() => { if (this.modal && this.modal.parentNode) { this.modal.remove(); this.modal = null; } }, 300); } addEventListeners(element) { element.addEventListener('click', (e) => { const action = e.target.getAttribute('data-action'); if (!action) return; switch (action) { case 'all': this.acceptAll(); break; case 'essential': this.acceptEssential(); break; case 'preferences': this.showPreferences(); break; case 'close': this.hidePreferences(); break; case 'essential-close': this.acceptEssential(); this.hidePreferences(); break; case 'save-close': this.saveCustomPreferences(); this.hidePreferences(); break; case 'all-close': this.acceptAll(); this.hidePreferences(); break; } }); } acceptAll() { this.setConsent('all'); this.hide(); if (this.options.onAcceptAll) { this.options.onAcceptAll(); } this.enableAnalytics(); } acceptEssential() { this.setConsent('essential'); this.hide(); if (this.options.onAcceptEssential) { this.options.onAcceptEssential(); } } saveCustomPreferences() { const analyticsEnabled = document.getElementById('analytics')?.checked; const consentLevel = analyticsEnabled ? 'all' : 'essential'; this.setConsent(consentLevel); if (analyticsEnabled) { this.enableAnalytics(); } if (this.options.onAcceptCustom) { this.options.onAcceptCustom(consentLevel, { analytics: analyticsEnabled }); } } enableAnalytics() { // Override this method or use the callback to implement your analytics console.log('Analytics enabled'); // Example: Google Analytics // if (typeof gtag !== 'undefined') { // gtag('consent', 'update', { // 'analytics_storage': 'granted' // }); // } } setConsent(level) { const expires = new Date(); expires.setTime(expires.getTime() + (this.options.cookieExpiryDays * 24 * 60 * 60 * 1000)); document.cookie = `${this.options.cookieName}=${level};expires=${expires.toUTCString()};path=/;SameSite=Lax`; } getConsent() { const nameEQ = this.options.cookieName + "="; const ca = document.cookie.split(';'); for (let i = 0; i < ca.length; i++) { let c = ca[i]; while (c.charAt(0) === ' ') c = c.substring(1, c.length); if (c.indexOf(nameEQ) === 0) return c.substring(nameEQ.length, c.length); } return null; } hasConsent(type = 'all') { const consent = this.getConsent(); if (!consent) return false; if (type === 'essential') { return consent === 'essential' || consent === 'all'; } return consent === type; } resetConsent() { document.cookie = `${this.options.cookieName}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;`; this.remove(); if (this.options.autoShow) { setTimeout(() => this.show(), 500); } } remove() { if (this.widget) { this.widget.remove(); this.widget = null; } if (this.modal) { this.modal.remove(); this.modal = null; } this.isVisible = false; } setLanguage(language) { this.currentLanguage = language; if (this.isVisible) { this.hide(); setTimeout(() => this.show(), 100); } } // Static method to create instance static create(options) { return new CookieConsent(options); } } // Export for different module systems if (typeof module !== 'undefined' && module.exports) { module.exports = CookieConsent; } else if (typeof define === 'function' && define.amd) { define([], function() { return CookieConsent; }); } else if (typeof window !== 'undefined') { window.CookieConsent = CookieConsent; } // Export default for ES6 modules if (typeof exports !== 'undefined') { exports.default = CookieConsent; }