@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
JavaScript
/**
* 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;
}
(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;
}