Initial commit: breakpilot-compliance - Compliance SDK Platform
Services: Admin-Compliance, Backend-Compliance, AI-Compliance-SDK, Consent-SDK, Developer-Portal, PCA-Platform, DSMS Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,379 @@
|
||||
/**
|
||||
* <breakpilot-consent-banner> Web Component
|
||||
*
|
||||
* Usage:
|
||||
* <breakpilot-consent-banner
|
||||
* api-key="pk_live_xxx"
|
||||
* position="bottom"
|
||||
* theme="light"
|
||||
* language="de"
|
||||
* privacy-url="/privacy"
|
||||
* imprint-url="/imprint">
|
||||
* </breakpilot-consent-banner>
|
||||
*/
|
||||
|
||||
import type {
|
||||
ConsentPurpose,
|
||||
CookieBannerPosition,
|
||||
CookieBannerTheme,
|
||||
} from '@breakpilot/compliance-sdk-types'
|
||||
import { BreakPilotElement, COMMON_STYLES } from './base'
|
||||
|
||||
const TRANSLATIONS = {
|
||||
de: {
|
||||
title: 'Cookie-Einwilligung',
|
||||
description:
|
||||
'Wir verwenden Cookies, um Ihre Erfahrung zu verbessern. Weitere Informationen finden Sie in unserer Datenschutzerklärung.',
|
||||
acceptAll: 'Alle akzeptieren',
|
||||
rejectAll: 'Nur notwendige',
|
||||
settings: 'Einstellungen',
|
||||
save: 'Einstellungen speichern',
|
||||
back: 'Zurück',
|
||||
privacy: 'Datenschutz',
|
||||
imprint: 'Impressum',
|
||||
categories: {
|
||||
ESSENTIAL: { name: 'Notwendig', description: 'Erforderlich für die Grundfunktionen' },
|
||||
FUNCTIONAL: { name: 'Funktional', description: 'Verbesserte Funktionen' },
|
||||
ANALYTICS: { name: 'Analyse', description: 'Nutzungsstatistiken' },
|
||||
MARKETING: { name: 'Marketing', description: 'Personalisierte Werbung' },
|
||||
},
|
||||
},
|
||||
en: {
|
||||
title: 'Cookie Consent',
|
||||
description:
|
||||
'We use cookies to improve your experience. For more information, please see our privacy policy.',
|
||||
acceptAll: 'Accept All',
|
||||
rejectAll: 'Reject Non-Essential',
|
||||
settings: 'Settings',
|
||||
save: 'Save Settings',
|
||||
back: 'Back',
|
||||
privacy: 'Privacy Policy',
|
||||
imprint: 'Imprint',
|
||||
categories: {
|
||||
ESSENTIAL: { name: 'Essential', description: 'Required for basic functionality' },
|
||||
FUNCTIONAL: { name: 'Functional', description: 'Enhanced features' },
|
||||
ANALYTICS: { name: 'Analytics', description: 'Usage statistics' },
|
||||
MARKETING: { name: 'Marketing', description: 'Personalized advertising' },
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export class ConsentBannerElement extends BreakPilotElement {
|
||||
static get observedAttributes(): string[] {
|
||||
return [
|
||||
'api-key',
|
||||
'api-endpoint',
|
||||
'position',
|
||||
'theme',
|
||||
'language',
|
||||
'privacy-url',
|
||||
'imprint-url',
|
||||
]
|
||||
}
|
||||
|
||||
private consents: Record<ConsentPurpose, boolean> = {
|
||||
ESSENTIAL: true,
|
||||
FUNCTIONAL: false,
|
||||
ANALYTICS: false,
|
||||
MARKETING: false,
|
||||
PERSONALIZATION: false,
|
||||
THIRD_PARTY: false,
|
||||
}
|
||||
|
||||
private showSettings = false
|
||||
|
||||
connectedCallback(): void {
|
||||
super.connectedCallback()
|
||||
this.loadStoredConsents()
|
||||
}
|
||||
|
||||
private get position(): CookieBannerPosition {
|
||||
return (this.getAttribute('position')?.toUpperCase() as CookieBannerPosition) || 'BOTTOM'
|
||||
}
|
||||
|
||||
private get theme(): CookieBannerTheme {
|
||||
return (this.getAttribute('theme')?.toUpperCase() as CookieBannerTheme) || 'LIGHT'
|
||||
}
|
||||
|
||||
private get language(): 'de' | 'en' {
|
||||
return (this.getAttribute('language') as 'de' | 'en') || 'de'
|
||||
}
|
||||
|
||||
private get privacyUrl(): string {
|
||||
return this.getAttribute('privacy-url') || '/privacy'
|
||||
}
|
||||
|
||||
private get imprintUrl(): string {
|
||||
return this.getAttribute('imprint-url') || '/imprint'
|
||||
}
|
||||
|
||||
private get t() {
|
||||
return TRANSLATIONS[this.language]
|
||||
}
|
||||
|
||||
private loadStoredConsents(): void {
|
||||
try {
|
||||
const stored = localStorage.getItem('breakpilot_consents')
|
||||
if (stored) {
|
||||
this.consents = JSON.parse(stored)
|
||||
this.hide()
|
||||
}
|
||||
} catch {
|
||||
// Ignore errors
|
||||
}
|
||||
}
|
||||
|
||||
private storeConsents(): void {
|
||||
try {
|
||||
localStorage.setItem('breakpilot_consents', JSON.stringify(this.consents))
|
||||
localStorage.setItem('breakpilot_consents_timestamp', Date.now().toString())
|
||||
} catch {
|
||||
// Ignore errors
|
||||
}
|
||||
}
|
||||
|
||||
private handleAcceptAll = (): void => {
|
||||
this.consents = {
|
||||
ESSENTIAL: true,
|
||||
FUNCTIONAL: true,
|
||||
ANALYTICS: true,
|
||||
MARKETING: true,
|
||||
PERSONALIZATION: true,
|
||||
THIRD_PARTY: true,
|
||||
}
|
||||
this.applyConsents()
|
||||
}
|
||||
|
||||
private handleRejectAll = (): void => {
|
||||
this.consents = {
|
||||
ESSENTIAL: true,
|
||||
FUNCTIONAL: false,
|
||||
ANALYTICS: false,
|
||||
MARKETING: false,
|
||||
PERSONALIZATION: false,
|
||||
THIRD_PARTY: false,
|
||||
}
|
||||
this.applyConsents()
|
||||
}
|
||||
|
||||
private handleSave = (): void => {
|
||||
this.applyConsents()
|
||||
}
|
||||
|
||||
private applyConsents(): void {
|
||||
this.storeConsents()
|
||||
this.emit('consent-change', { consents: this.consents })
|
||||
this.hide()
|
||||
}
|
||||
|
||||
private toggleSettings = (): void => {
|
||||
this.showSettings = !this.showSettings
|
||||
this.render()
|
||||
}
|
||||
|
||||
public show(): void {
|
||||
this.style.display = 'block'
|
||||
this.render()
|
||||
}
|
||||
|
||||
public hide(): void {
|
||||
this.style.display = 'none'
|
||||
}
|
||||
|
||||
public getConsents(): Record<ConsentPurpose, boolean> {
|
||||
return { ...this.consents }
|
||||
}
|
||||
|
||||
protected render(): void {
|
||||
const isDark = this.theme === 'DARK'
|
||||
const bgColor = isDark ? '#1a1a1a' : '#ffffff'
|
||||
const textColor = isDark ? '#ffffff' : '#1a1a1a'
|
||||
const borderColor = isDark ? '#333' : '#eee'
|
||||
|
||||
const positionStyles = {
|
||||
TOP: 'top: 0; left: 0; right: 0;',
|
||||
BOTTOM: 'bottom: 0; left: 0; right: 0;',
|
||||
CENTER: 'top: 50%; left: 50%; transform: translate(-50%, -50%); max-width: 500px;',
|
||||
}
|
||||
|
||||
const styles = `
|
||||
${COMMON_STYLES}
|
||||
|
||||
:host {
|
||||
position: fixed;
|
||||
z-index: 99999;
|
||||
${positionStyles[this.position]}
|
||||
}
|
||||
|
||||
.banner {
|
||||
background-color: ${bgColor};
|
||||
color: ${textColor};
|
||||
padding: 20px;
|
||||
box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.title {
|
||||
margin: 0 0 10px;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.description {
|
||||
margin: 0 0 15px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 10px 20px;
|
||||
border-radius: 4px;
|
||||
border: none;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background-color: ${isDark ? '#ffffff' : '#1a1a1a'};
|
||||
color: ${isDark ? '#1a1a1a' : '#ffffff'};
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background-color: transparent;
|
||||
border: 1px solid ${textColor};
|
||||
color: ${textColor};
|
||||
}
|
||||
|
||||
.links {
|
||||
margin-left: auto;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.links a {
|
||||
color: ${textColor};
|
||||
text-decoration: none;
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
.links a:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.category {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 10px 0;
|
||||
border-bottom: 1px solid ${borderColor};
|
||||
}
|
||||
|
||||
.category-name {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.category-description {
|
||||
font-size: 12px;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.checkbox {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.checkbox:disabled {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
`
|
||||
|
||||
if (this.showSettings) {
|
||||
this.renderSettings(styles)
|
||||
} else {
|
||||
this.renderMain(styles)
|
||||
}
|
||||
}
|
||||
|
||||
private renderMain(styles: string): void {
|
||||
const t = this.t
|
||||
|
||||
this.shadow.innerHTML = `
|
||||
<style>${styles}</style>
|
||||
<div class="banner">
|
||||
<h3 class="title">${t.title}</h3>
|
||||
<p class="description">${t.description}</p>
|
||||
<div class="buttons">
|
||||
<button class="btn btn-primary" id="accept-all">${t.acceptAll}</button>
|
||||
<button class="btn btn-secondary" id="reject-all">${t.rejectAll}</button>
|
||||
<button class="btn btn-secondary" id="settings">${t.settings}</button>
|
||||
<div class="links">
|
||||
<a href="${this.privacyUrl}">${t.privacy}</a>
|
||||
<a href="${this.imprintUrl}">${t.imprint}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
|
||||
this.shadow.getElementById('accept-all')!.onclick = this.handleAcceptAll
|
||||
this.shadow.getElementById('reject-all')!.onclick = this.handleRejectAll
|
||||
this.shadow.getElementById('settings')!.onclick = this.toggleSettings
|
||||
}
|
||||
|
||||
private renderSettings(styles: string): void {
|
||||
const t = this.t
|
||||
const categories: ConsentPurpose[] = ['ESSENTIAL', 'FUNCTIONAL', 'ANALYTICS', 'MARKETING']
|
||||
|
||||
const categoriesHtml = categories
|
||||
.map(
|
||||
cat => `
|
||||
<div class="category">
|
||||
<div>
|
||||
<div class="category-name">${t.categories[cat].name}</div>
|
||||
<div class="category-description">${t.categories[cat].description}</div>
|
||||
</div>
|
||||
<input
|
||||
type="checkbox"
|
||||
class="checkbox"
|
||||
data-category="${cat}"
|
||||
${this.consents[cat] ? 'checked' : ''}
|
||||
${cat === 'ESSENTIAL' ? 'disabled' : ''}
|
||||
/>
|
||||
</div>
|
||||
`
|
||||
)
|
||||
.join('')
|
||||
|
||||
this.shadow.innerHTML = `
|
||||
<style>${styles}</style>
|
||||
<div class="banner">
|
||||
<h3 class="title">${t.settings}</h3>
|
||||
${categoriesHtml}
|
||||
<div class="buttons" style="margin-top: 15px;">
|
||||
<button class="btn btn-primary" id="save">${t.save}</button>
|
||||
<button class="btn btn-secondary" id="back">${t.back}</button>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
|
||||
this.shadow.getElementById('save')!.onclick = this.handleSave
|
||||
this.shadow.getElementById('back')!.onclick = this.toggleSettings
|
||||
|
||||
// Handle checkbox changes
|
||||
this.shadow.querySelectorAll<HTMLInputElement>('.checkbox').forEach(checkbox => {
|
||||
checkbox.onchange = () => {
|
||||
const category = checkbox.dataset.category as ConsentPurpose
|
||||
if (category !== 'ESSENTIAL') {
|
||||
this.consents[category] = checkbox.checked
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Register the custom element
|
||||
if (typeof customElements !== 'undefined') {
|
||||
customElements.define('breakpilot-consent-banner', ConsentBannerElement)
|
||||
}
|
||||
Reference in New Issue
Block a user