/** * Cookie Banner Generator * * Generiert Cookie-Banner Konfigurationen und Embed-Code aus dem Datenpunktkatalog. * Die Cookie-Kategorien werden automatisch aus den Datenpunkten abgeleitet. */ import { DataPoint, CookieCategory, CookieBannerCategory, CookieBannerConfig, CookieBannerStyling, CookieBannerTexts, CookieBannerEmbedCode, CookieInfo, LocalizedText, SupportedLanguage, } from '../types' import { DEFAULT_COOKIE_CATEGORIES } from '../catalog/loader' // ============================================================================= // HELPER FUNCTIONS // ============================================================================= /** * Holt den lokalisierten Text */ function t(text: LocalizedText, language: SupportedLanguage): string { return text[language] } // ============================================================================= // COOKIE BANNER CONFIGURATION // ============================================================================= /** * Standard Cookie Banner Texte */ export const DEFAULT_COOKIE_BANNER_TEXTS: CookieBannerTexts = { title: { de: 'Cookie-Einstellungen', en: 'Cookie Settings', }, description: { de: 'Wir verwenden Cookies, um Ihnen die bestmoegliche Nutzung unserer Website zu ermoeglichen. Einige Cookies sind technisch notwendig, waehrend andere uns helfen, Ihre Nutzererfahrung zu verbessern.', en: 'We use cookies to provide you with the best possible experience on our website. Some cookies are technically necessary, while others help us improve your user experience.', }, acceptAll: { de: 'Alle akzeptieren', en: 'Accept All', }, rejectAll: { de: 'Nur notwendige', en: 'Essential Only', }, customize: { de: 'Einstellungen', en: 'Customize', }, save: { de: 'Auswahl speichern', en: 'Save Selection', }, privacyPolicyLink: { de: 'Mehr in unserer Datenschutzerklaerung', en: 'More in our Privacy Policy', }, } /** * Standard Styling fuer Cookie Banner */ export const DEFAULT_COOKIE_BANNER_STYLING: CookieBannerStyling = { position: 'BOTTOM', theme: 'LIGHT', primaryColor: '#6366f1', // Indigo secondaryColor: '#f1f5f9', // Slate-100 textColor: '#1e293b', // Slate-800 backgroundColor: '#ffffff', borderRadius: 12, maxWidth: 480, } // ============================================================================= // GENERATOR FUNCTIONS // ============================================================================= /** * Generiert Cookie-Banner Kategorien aus Datenpunkten */ export function generateCookieCategories( dataPoints: DataPoint[] ): CookieBannerCategory[] { // Filtere nur Datenpunkte mit Cookie-Kategorie const cookieDataPoints = dataPoints.filter((dp) => dp.cookieCategory !== null) // Erstelle die Kategorien basierend auf den Defaults return DEFAULT_COOKIE_CATEGORIES.map((defaultCat) => { // Filtere die Datenpunkte fuer diese Kategorie const categoryDataPoints = cookieDataPoints.filter( (dp) => dp.cookieCategory === defaultCat.id ) // Erstelle Cookie-Infos aus den Datenpunkten const cookies: CookieInfo[] = categoryDataPoints.map((dp) => ({ name: dp.code, provider: 'First Party', purpose: dp.purpose, expiry: getExpiryFromRetention(dp.retentionPeriod), type: 'FIRST_PARTY', })) return { ...defaultCat, dataPointIds: categoryDataPoints.map((dp) => dp.id), cookies, } }).filter((cat) => cat.dataPointIds.length > 0 || cat.isRequired) } /** * Konvertiert Retention Period zu Cookie-Expiry String */ function getExpiryFromRetention(retention: string): string { const mapping: Record = { '24_HOURS': '24 Stunden / 24 hours', '30_DAYS': '30 Tage / 30 days', '90_DAYS': '90 Tage / 90 days', '12_MONTHS': '1 Jahr / 1 year', '24_MONTHS': '2 Jahre / 2 years', '36_MONTHS': '3 Jahre / 3 years', 'UNTIL_REVOCATION': 'Bis Widerruf / Until revocation', 'UNTIL_PURPOSE_FULFILLED': 'Session', 'UNTIL_ACCOUNT_DELETION': 'Bis Kontoschliessung / Until account deletion', } return mapping[retention] || 'Session' } /** * Generiert die vollstaendige Cookie Banner Konfiguration */ export function generateCookieBannerConfig( tenantId: string, dataPoints: DataPoint[], customTexts?: Partial, customStyling?: Partial ): CookieBannerConfig { const categories = generateCookieCategories(dataPoints) return { id: `cookie-banner-${tenantId}`, tenantId, categories, styling: { ...DEFAULT_COOKIE_BANNER_STYLING, ...customStyling, }, texts: { ...DEFAULT_COOKIE_BANNER_TEXTS, ...customTexts, }, updatedAt: new Date(), } } // ============================================================================= // EMBED CODE GENERATION // ============================================================================= /** * Generiert den Embed-Code fuer den Cookie Banner */ export function generateEmbedCode( config: CookieBannerConfig, privacyPolicyUrl: string = '/datenschutz' ): CookieBannerEmbedCode { const css = generateCSS(config.styling) const html = generateHTML(config, privacyPolicyUrl) const js = generateJS(config) const scriptTag = `` return { html, css, js, scriptTag, } } /** * Generiert das CSS fuer den Cookie Banner */ function generateCSS(styling: CookieBannerStyling): string { const positionStyles: Record = { BOTTOM: 'bottom: 0; left: 0; right: 0;', TOP: 'top: 0; left: 0; right: 0;', CENTER: 'top: 50%; left: 50%; transform: translate(-50%, -50%);', } const isDark = styling.theme === 'DARK' const bgColor = isDark ? '#1e293b' : styling.backgroundColor || '#ffffff' const textColor = isDark ? '#f1f5f9' : styling.textColor || '#1e293b' const borderColor = isDark ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.1)' return ` /* Cookie Banner Styles */ .cookie-banner-overlay { position: fixed; inset: 0; background: rgba(0, 0, 0, 0.4); z-index: 9998; opacity: 0; visibility: hidden; transition: all 0.3s ease; } .cookie-banner-overlay.active { opacity: 1; visibility: visible; } .cookie-banner { position: fixed; ${positionStyles[styling.position]} z-index: 9999; background: ${bgColor}; color: ${textColor}; border-radius: ${styling.borderRadius || 12}px; box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.1); padding: 24px; max-width: ${styling.maxWidth}px; margin: ${styling.position === 'CENTER' ? '0' : '16px'}; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; transform: translateY(100%); opacity: 0; transition: all 0.3s ease; } .cookie-banner.active { transform: translateY(0); opacity: 1; } .cookie-banner-title { font-size: 18px; font-weight: 600; margin-bottom: 12px; } .cookie-banner-description { font-size: 14px; line-height: 1.5; margin-bottom: 16px; opacity: 0.8; } .cookie-banner-buttons { display: flex; gap: 12px; flex-wrap: wrap; } .cookie-banner-btn { flex: 1; min-width: 120px; padding: 12px 20px; border-radius: ${(styling.borderRadius || 12) / 2}px; font-size: 14px; font-weight: 500; cursor: pointer; transition: all 0.2s ease; border: none; } .cookie-banner-btn-primary { background: ${styling.primaryColor}; color: white; } .cookie-banner-btn-primary:hover { filter: brightness(1.1); } .cookie-banner-btn-secondary { background: ${styling.secondaryColor || borderColor}; color: ${textColor}; } .cookie-banner-btn-secondary:hover { filter: brightness(0.95); } .cookie-banner-link { display: block; margin-top: 16px; font-size: 12px; color: ${styling.primaryColor}; text-decoration: none; } .cookie-banner-link:hover { text-decoration: underline; } /* Category Details */ .cookie-banner-details { margin-top: 16px; border-top: 1px solid ${borderColor}; padding-top: 16px; display: none; } .cookie-banner-details.active { display: block; } .cookie-banner-category { display: flex; justify-content: space-between; align-items: center; padding: 12px 0; border-bottom: 1px solid ${borderColor}; } .cookie-banner-category:last-child { border-bottom: none; } .cookie-banner-category-info { flex: 1; } .cookie-banner-category-name { font-weight: 500; font-size: 14px; } .cookie-banner-category-desc { font-size: 12px; opacity: 0.7; margin-top: 4px; } .cookie-banner-toggle { position: relative; width: 48px; height: 28px; background: ${borderColor}; border-radius: 14px; cursor: pointer; transition: all 0.2s ease; } .cookie-banner-toggle.active { background: ${styling.primaryColor}; } .cookie-banner-toggle.disabled { opacity: 0.5; cursor: not-allowed; } .cookie-banner-toggle::after { content: ''; position: absolute; top: 4px; left: 4px; width: 20px; height: 20px; background: white; border-radius: 50%; transition: all 0.2s ease; } .cookie-banner-toggle.active::after { left: 24px; } @media (max-width: 640px) { .cookie-banner { margin: 0; border-radius: ${styling.position === 'CENTER' ? (styling.borderRadius || 12) : 0}px; max-width: 100%; } .cookie-banner-buttons { flex-direction: column; } .cookie-banner-btn { width: 100%; } } `.trim() } /** * Generiert das HTML fuer den Cookie Banner */ function generateHTML(config: CookieBannerConfig, privacyPolicyUrl: string): string { const categoriesHTML = config.categories .map((cat) => { const isRequired = cat.isRequired return ` ` }) .join('') return ` `.trim() } /** * Generiert das JavaScript fuer den Cookie Banner */ function generateJS(config: CookieBannerConfig): string { const categoryIds = config.categories.map((c) => c.id) const requiredCategories = config.categories.filter((c) => c.isRequired).map((c) => c.id) return ` (function() { 'use strict'; const COOKIE_NAME = 'cookie_consent'; const COOKIE_EXPIRY_DAYS = 365; const CATEGORIES = ${JSON.stringify(categoryIds)}; const REQUIRED_CATEGORIES = ${JSON.stringify(requiredCategories)}; // Get consent from cookie function getConsent() { const cookie = document.cookie.split('; ').find(row => row.startsWith(COOKIE_NAME + '=')); if (!cookie) return null; try { return JSON.parse(decodeURIComponent(cookie.split('=')[1])); } catch { return null; } } // Save consent to cookie function saveConsent(consent) { const date = new Date(); date.setTime(date.getTime() + (COOKIE_EXPIRY_DAYS * 24 * 60 * 60 * 1000)); document.cookie = COOKIE_NAME + '=' + encodeURIComponent(JSON.stringify(consent)) + ';expires=' + date.toUTCString() + ';path=/;SameSite=Lax'; // Dispatch event window.dispatchEvent(new CustomEvent('cookieConsentUpdated', { detail: consent })); } // Check if category is consented function hasConsent(category) { const consent = getConsent(); if (!consent) return REQUIRED_CATEGORIES.includes(category); return consent[category] === true; } // Initialize banner function initBanner() { const banner = document.getElementById('cookieBanner'); const overlay = document.getElementById('cookieBannerOverlay'); const details = document.getElementById('cookieBannerDetails'); if (!banner) return; const consent = getConsent(); if (consent) { // User has already consented return; } // Show banner setTimeout(() => { banner.classList.add('active'); overlay.classList.add('active'); }, 500); // Accept all document.getElementById('cookieBannerAccept')?.addEventListener('click', () => { const consent = {}; CATEGORIES.forEach(cat => consent[cat] = true); saveConsent(consent); closeBanner(); }); // Reject all (only essential) document.getElementById('cookieBannerReject')?.addEventListener('click', () => { const consent = {}; CATEGORIES.forEach(cat => consent[cat] = REQUIRED_CATEGORIES.includes(cat)); saveConsent(consent); closeBanner(); }); // Customize document.getElementById('cookieBannerCustomize')?.addEventListener('click', () => { details.classList.toggle('active'); }); // Save selection document.getElementById('cookieBannerSave')?.addEventListener('click', () => { const consent = {}; CATEGORIES.forEach(cat => { const toggle = document.querySelector('.cookie-banner-toggle[data-category="' + cat + '"]'); consent[cat] = toggle?.classList.contains('active') || REQUIRED_CATEGORIES.includes(cat); }); saveConsent(consent); closeBanner(); }); // Toggle handlers document.querySelectorAll('.cookie-banner-toggle').forEach(toggle => { if (toggle.dataset.required === 'true') return; toggle.addEventListener('click', () => { toggle.classList.toggle('active'); }); }); // Close on overlay click overlay?.addEventListener('click', () => { // Don't close - user must make a choice }); } function closeBanner() { const banner = document.getElementById('cookieBanner'); const overlay = document.getElementById('cookieBannerOverlay'); banner?.classList.remove('active'); overlay?.classList.remove('active'); } // Expose API window.CookieConsent = { getConsent, saveConsent, hasConsent, show: () => { document.getElementById('cookieBanner')?.classList.add('active'); document.getElementById('cookieBannerOverlay')?.classList.add('active'); }, hide: closeBanner }; // Initialize on DOM ready if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initBanner); } else { initBanner(); } })(); `.trim() } // Note: All exports are defined inline with 'export const' and 'export function'