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:
611
breakpilot-compliance-sdk/packages/vanilla/src/embed.ts
Normal file
611
breakpilot-compliance-sdk/packages/vanilla/src/embed.ts
Normal file
@@ -0,0 +1,611 @@
|
||||
/**
|
||||
* BreakPilot Compliance SDK - Embed Script
|
||||
*
|
||||
* Usage:
|
||||
* <script src="https://cdn.breakpilot.app/sdk/v1/compliance.min.js"></script>
|
||||
* <script>
|
||||
* BreakPilotSDK.init({
|
||||
* apiEndpoint: 'https://compliance.example.com/api/v1',
|
||||
* apiKey: 'pk_live_xxx',
|
||||
* autoInjectBanner: true,
|
||||
* bannerConfig: { position: 'bottom', theme: 'dark', language: 'de' }
|
||||
* });
|
||||
* </script>
|
||||
*/
|
||||
|
||||
import { ComplianceClient } from '@breakpilot/compliance-sdk-core'
|
||||
import type {
|
||||
ConsentPurpose,
|
||||
CookieBannerPosition,
|
||||
CookieBannerTheme,
|
||||
DSRRequestType,
|
||||
} from '@breakpilot/compliance-sdk-types'
|
||||
|
||||
// ============================================================================
|
||||
// Types
|
||||
// ============================================================================
|
||||
|
||||
export interface BreakPilotSDKConfig {
|
||||
apiEndpoint: string
|
||||
apiKey: string
|
||||
tenantId?: string
|
||||
autoInjectBanner?: boolean
|
||||
bannerConfig?: BannerConfig
|
||||
onConsentChange?: (consents: Record<ConsentPurpose, boolean>) => void
|
||||
onReady?: () => void
|
||||
onError?: (error: Error) => void
|
||||
debug?: boolean
|
||||
}
|
||||
|
||||
export interface BannerConfig {
|
||||
position?: CookieBannerPosition
|
||||
theme?: CookieBannerTheme
|
||||
language?: 'de' | 'en'
|
||||
privacyPolicyUrl?: string
|
||||
imprintUrl?: string
|
||||
texts?: {
|
||||
title?: string
|
||||
description?: string
|
||||
acceptAll?: string
|
||||
rejectAll?: string
|
||||
settings?: string
|
||||
save?: string
|
||||
}
|
||||
customColors?: {
|
||||
background?: string
|
||||
text?: string
|
||||
primary?: string
|
||||
secondary?: string
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Internal State
|
||||
// ============================================================================
|
||||
|
||||
let _client: ComplianceClient | null = null
|
||||
let _config: BreakPilotSDKConfig | null = null
|
||||
let _consents: Record<ConsentPurpose, boolean> = {
|
||||
ESSENTIAL: true,
|
||||
FUNCTIONAL: false,
|
||||
ANALYTICS: false,
|
||||
MARKETING: false,
|
||||
PERSONALIZATION: false,
|
||||
THIRD_PARTY: false,
|
||||
}
|
||||
let _bannerElement: HTMLElement | null = null
|
||||
let _isInitialized = false
|
||||
|
||||
// ============================================================================
|
||||
// Translations
|
||||
// ============================================================================
|
||||
|
||||
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: 'Speichern',
|
||||
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' },
|
||||
PERSONALIZATION: { name: 'Personalisierung', description: 'Angepasste Inhalte' },
|
||||
THIRD_PARTY: { name: 'Drittanbieter', description: 'Externe Dienste' },
|
||||
},
|
||||
},
|
||||
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',
|
||||
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' },
|
||||
PERSONALIZATION: { name: 'Personalization', description: 'Customized content' },
|
||||
THIRD_PARTY: { name: 'Third Party', description: 'External services' },
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Utility Functions
|
||||
// ============================================================================
|
||||
|
||||
function log(message: string, ...args: unknown[]): void {
|
||||
if (_config?.debug) {
|
||||
console.log(`[BreakPilotSDK] ${message}`, ...args)
|
||||
}
|
||||
}
|
||||
|
||||
function getStoredConsents(): Record<ConsentPurpose, boolean> | null {
|
||||
try {
|
||||
const stored = localStorage.getItem('breakpilot_consents')
|
||||
return stored ? JSON.parse(stored) : null
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function storeConsents(consents: Record<ConsentPurpose, boolean>): void {
|
||||
try {
|
||||
localStorage.setItem('breakpilot_consents', JSON.stringify(consents))
|
||||
localStorage.setItem('breakpilot_consents_timestamp', Date.now().toString())
|
||||
} catch {
|
||||
log('Failed to store consents')
|
||||
}
|
||||
}
|
||||
|
||||
function createElement<K extends keyof HTMLElementTagNameMap>(
|
||||
tag: K,
|
||||
styles: Partial<CSSStyleDeclaration> = {},
|
||||
attributes: Record<string, string> = {}
|
||||
): HTMLElementTagNameMap[K] {
|
||||
const el = document.createElement(tag)
|
||||
Object.assign(el.style, styles)
|
||||
Object.entries(attributes).forEach(([key, value]) => el.setAttribute(key, value))
|
||||
return el
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Banner Implementation
|
||||
// ============================================================================
|
||||
|
||||
function createBanner(): HTMLElement {
|
||||
const config = _config!
|
||||
const bannerConfig = config.bannerConfig || {}
|
||||
const lang = bannerConfig.language || 'de'
|
||||
const t = TRANSLATIONS[lang]
|
||||
const position = bannerConfig.position || 'BOTTOM'
|
||||
const theme = bannerConfig.theme || 'LIGHT'
|
||||
|
||||
const isDark = theme === 'DARK'
|
||||
const bgColor = bannerConfig.customColors?.background || (isDark ? '#1a1a1a' : '#ffffff')
|
||||
const textColor = bannerConfig.customColors?.text || (isDark ? '#ffffff' : '#1a1a1a')
|
||||
const primaryColor = bannerConfig.customColors?.primary || (isDark ? '#ffffff' : '#1a1a1a')
|
||||
const secondaryColor = bannerConfig.customColors?.secondary || (isDark ? '#ffffff' : '#1a1a1a')
|
||||
|
||||
// Container
|
||||
const container = createElement(
|
||||
'div',
|
||||
{
|
||||
position: 'fixed',
|
||||
zIndex: '99999',
|
||||
left: position === 'CENTER' ? '50%' : '0',
|
||||
right: position === 'CENTER' ? 'auto' : '0',
|
||||
top: position === 'TOP' ? '0' : position === 'CENTER' ? '50%' : 'auto',
|
||||
bottom: position === 'BOTTOM' ? '0' : 'auto',
|
||||
transform: position === 'CENTER' ? 'translate(-50%, -50%)' : 'none',
|
||||
maxWidth: position === 'CENTER' ? '500px' : 'none',
|
||||
backgroundColor: bgColor,
|
||||
color: textColor,
|
||||
padding: '20px',
|
||||
boxShadow: '0 -2px 10px rgba(0, 0, 0, 0.1)',
|
||||
fontFamily: 'system-ui, -apple-system, sans-serif',
|
||||
fontSize: '14px',
|
||||
lineHeight: '1.5',
|
||||
},
|
||||
{ id: 'breakpilot-consent-banner', role: 'dialog', 'aria-label': t.title }
|
||||
)
|
||||
|
||||
// Title
|
||||
const title = createElement('h3', {
|
||||
margin: '0 0 10px',
|
||||
fontSize: '18px',
|
||||
fontWeight: '600',
|
||||
})
|
||||
title.textContent = bannerConfig.texts?.title || t.title
|
||||
|
||||
// Description
|
||||
const description = createElement('p', {
|
||||
margin: '0 0 15px',
|
||||
opacity: '0.8',
|
||||
})
|
||||
description.textContent = bannerConfig.texts?.description || t.description
|
||||
|
||||
// Buttons container
|
||||
const buttonsContainer = createElement('div', {
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
gap: '10px',
|
||||
alignItems: 'center',
|
||||
})
|
||||
|
||||
// Accept All button
|
||||
const acceptBtn = createElement(
|
||||
'button',
|
||||
{
|
||||
padding: '10px 20px',
|
||||
borderRadius: '4px',
|
||||
border: 'none',
|
||||
cursor: 'pointer',
|
||||
fontWeight: '500',
|
||||
backgroundColor: primaryColor,
|
||||
color: isDark ? '#1a1a1a' : '#ffffff',
|
||||
},
|
||||
{ type: 'button' }
|
||||
)
|
||||
acceptBtn.textContent = bannerConfig.texts?.acceptAll || t.acceptAll
|
||||
acceptBtn.onclick = () => handleAcceptAll()
|
||||
|
||||
// Reject button
|
||||
const rejectBtn = createElement(
|
||||
'button',
|
||||
{
|
||||
padding: '10px 20px',
|
||||
borderRadius: '4px',
|
||||
backgroundColor: 'transparent',
|
||||
border: `1px solid ${secondaryColor}`,
|
||||
color: textColor,
|
||||
cursor: 'pointer',
|
||||
fontWeight: '500',
|
||||
},
|
||||
{ type: 'button' }
|
||||
)
|
||||
rejectBtn.textContent = bannerConfig.texts?.rejectAll || t.rejectAll
|
||||
rejectBtn.onclick = () => handleRejectAll()
|
||||
|
||||
// Settings button
|
||||
const settingsBtn = createElement(
|
||||
'button',
|
||||
{
|
||||
padding: '10px 20px',
|
||||
borderRadius: '4px',
|
||||
backgroundColor: 'transparent',
|
||||
border: `1px solid ${secondaryColor}`,
|
||||
color: textColor,
|
||||
cursor: 'pointer',
|
||||
fontWeight: '500',
|
||||
},
|
||||
{ type: 'button' }
|
||||
)
|
||||
settingsBtn.textContent = bannerConfig.texts?.settings || t.settings
|
||||
settingsBtn.onclick = () => showSettingsPanel()
|
||||
|
||||
// Links container
|
||||
const linksContainer = createElement('div', {
|
||||
marginLeft: 'auto',
|
||||
fontSize: '12px',
|
||||
})
|
||||
|
||||
const privacyLink = createElement('a', {
|
||||
marginRight: '15px',
|
||||
color: textColor,
|
||||
textDecoration: 'none',
|
||||
})
|
||||
privacyLink.href = bannerConfig.privacyPolicyUrl || '/privacy'
|
||||
privacyLink.textContent = t.privacy
|
||||
|
||||
const imprintLink = createElement('a', {
|
||||
color: textColor,
|
||||
textDecoration: 'none',
|
||||
})
|
||||
imprintLink.href = bannerConfig.imprintUrl || '/imprint'
|
||||
imprintLink.textContent = t.imprint
|
||||
|
||||
// Assemble
|
||||
linksContainer.appendChild(privacyLink)
|
||||
linksContainer.appendChild(imprintLink)
|
||||
|
||||
buttonsContainer.appendChild(acceptBtn)
|
||||
buttonsContainer.appendChild(rejectBtn)
|
||||
buttonsContainer.appendChild(settingsBtn)
|
||||
buttonsContainer.appendChild(linksContainer)
|
||||
|
||||
container.appendChild(title)
|
||||
container.appendChild(description)
|
||||
container.appendChild(buttonsContainer)
|
||||
|
||||
return container
|
||||
}
|
||||
|
||||
function showSettingsPanel(): void {
|
||||
if (!_bannerElement) return
|
||||
|
||||
const config = _config!
|
||||
const bannerConfig = config.bannerConfig || {}
|
||||
const lang = bannerConfig.language || 'de'
|
||||
const t = TRANSLATIONS[lang]
|
||||
const theme = bannerConfig.theme || 'LIGHT'
|
||||
const isDark = theme === 'DARK'
|
||||
const textColor = bannerConfig.customColors?.text || (isDark ? '#ffffff' : '#1a1a1a')
|
||||
|
||||
// Clear banner content
|
||||
_bannerElement.innerHTML = ''
|
||||
|
||||
// Title
|
||||
const title = createElement('h3', {
|
||||
margin: '0 0 15px',
|
||||
fontSize: '18px',
|
||||
fontWeight: '600',
|
||||
})
|
||||
title.textContent = t.settings
|
||||
|
||||
_bannerElement.appendChild(title)
|
||||
|
||||
// Categories
|
||||
const categories: ConsentPurpose[] = [
|
||||
'ESSENTIAL',
|
||||
'FUNCTIONAL',
|
||||
'ANALYTICS',
|
||||
'MARKETING',
|
||||
]
|
||||
|
||||
categories.forEach(category => {
|
||||
const catInfo = t.categories[category]
|
||||
const row = createElement('div', {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
padding: '10px 0',
|
||||
borderBottom: `1px solid ${isDark ? '#333' : '#eee'}`,
|
||||
})
|
||||
|
||||
const labelContainer = createElement('div', {})
|
||||
const labelName = createElement('div', { fontWeight: '500' })
|
||||
labelName.textContent = catInfo.name
|
||||
const labelDesc = createElement('div', { fontSize: '12px', opacity: '0.7' })
|
||||
labelDesc.textContent = catInfo.description
|
||||
|
||||
labelContainer.appendChild(labelName)
|
||||
labelContainer.appendChild(labelDesc)
|
||||
|
||||
const checkbox = createElement(
|
||||
'input',
|
||||
{
|
||||
width: '20px',
|
||||
height: '20px',
|
||||
cursor: category === 'ESSENTIAL' ? 'not-allowed' : 'pointer',
|
||||
},
|
||||
{
|
||||
type: 'checkbox',
|
||||
'data-category': category,
|
||||
}
|
||||
)
|
||||
checkbox.checked = _consents[category]
|
||||
checkbox.disabled = category === 'ESSENTIAL'
|
||||
checkbox.onchange = () => {
|
||||
if (category !== 'ESSENTIAL') {
|
||||
_consents[category] = checkbox.checked
|
||||
}
|
||||
}
|
||||
|
||||
row.appendChild(labelContainer)
|
||||
row.appendChild(checkbox)
|
||||
_bannerElement!.appendChild(row)
|
||||
})
|
||||
|
||||
// Buttons
|
||||
const buttonsContainer = createElement('div', {
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
gap: '10px',
|
||||
marginTop: '15px',
|
||||
})
|
||||
|
||||
const primaryColor = bannerConfig.customColors?.primary || (isDark ? '#ffffff' : '#1a1a1a')
|
||||
const secondaryColor = bannerConfig.customColors?.secondary || (isDark ? '#ffffff' : '#1a1a1a')
|
||||
|
||||
const saveBtn = createElement(
|
||||
'button',
|
||||
{
|
||||
padding: '10px 20px',
|
||||
borderRadius: '4px',
|
||||
border: 'none',
|
||||
cursor: 'pointer',
|
||||
fontWeight: '500',
|
||||
backgroundColor: primaryColor,
|
||||
color: isDark ? '#1a1a1a' : '#ffffff',
|
||||
},
|
||||
{ type: 'button' }
|
||||
)
|
||||
saveBtn.textContent = bannerConfig.texts?.save || t.save
|
||||
saveBtn.onclick = () => handleSaveSettings()
|
||||
|
||||
const backBtn = createElement(
|
||||
'button',
|
||||
{
|
||||
padding: '10px 20px',
|
||||
borderRadius: '4px',
|
||||
backgroundColor: 'transparent',
|
||||
border: `1px solid ${secondaryColor}`,
|
||||
color: textColor,
|
||||
cursor: 'pointer',
|
||||
fontWeight: '500',
|
||||
},
|
||||
{ type: 'button' }
|
||||
)
|
||||
backBtn.textContent = 'Zurück'
|
||||
backBtn.onclick = () => showMainBanner()
|
||||
|
||||
buttonsContainer.appendChild(saveBtn)
|
||||
buttonsContainer.appendChild(backBtn)
|
||||
_bannerElement.appendChild(buttonsContainer)
|
||||
}
|
||||
|
||||
function showMainBanner(): void {
|
||||
if (_bannerElement) {
|
||||
_bannerElement.remove()
|
||||
}
|
||||
_bannerElement = createBanner()
|
||||
document.body.appendChild(_bannerElement)
|
||||
}
|
||||
|
||||
function hideBanner(): void {
|
||||
if (_bannerElement) {
|
||||
_bannerElement.remove()
|
||||
_bannerElement = null
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Consent Handlers
|
||||
// ============================================================================
|
||||
|
||||
function handleAcceptAll(): void {
|
||||
_consents = {
|
||||
ESSENTIAL: true,
|
||||
FUNCTIONAL: true,
|
||||
ANALYTICS: true,
|
||||
MARKETING: true,
|
||||
PERSONALIZATION: true,
|
||||
THIRD_PARTY: true,
|
||||
}
|
||||
applyConsents()
|
||||
}
|
||||
|
||||
function handleRejectAll(): void {
|
||||
_consents = {
|
||||
ESSENTIAL: true,
|
||||
FUNCTIONAL: false,
|
||||
ANALYTICS: false,
|
||||
MARKETING: false,
|
||||
PERSONALIZATION: false,
|
||||
THIRD_PARTY: false,
|
||||
}
|
||||
applyConsents()
|
||||
}
|
||||
|
||||
function handleSaveSettings(): void {
|
||||
applyConsents()
|
||||
}
|
||||
|
||||
function applyConsents(): void {
|
||||
storeConsents(_consents)
|
||||
hideBanner()
|
||||
_config?.onConsentChange?.(_consents)
|
||||
log('Consents applied:', _consents)
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Public API
|
||||
// ============================================================================
|
||||
|
||||
function init(config: BreakPilotSDKConfig): void {
|
||||
if (_isInitialized) {
|
||||
log('SDK already initialized')
|
||||
return
|
||||
}
|
||||
|
||||
_config = config
|
||||
log('Initializing with config:', config)
|
||||
|
||||
try {
|
||||
_client = new ComplianceClient({
|
||||
apiEndpoint: config.apiEndpoint,
|
||||
apiKey: config.apiKey,
|
||||
tenantId: config.tenantId,
|
||||
})
|
||||
|
||||
// Check for stored consents
|
||||
const storedConsents = getStoredConsents()
|
||||
if (storedConsents) {
|
||||
_consents = storedConsents
|
||||
log('Loaded stored consents:', _consents)
|
||||
config.onConsentChange?.(storedConsents)
|
||||
} else if (config.autoInjectBanner !== false) {
|
||||
// Show banner if no stored consents
|
||||
showMainBanner()
|
||||
}
|
||||
|
||||
_isInitialized = true
|
||||
config.onReady?.()
|
||||
log('SDK initialized successfully')
|
||||
} catch (error) {
|
||||
const err = error instanceof Error ? error : new Error(String(error))
|
||||
config.onError?.(err)
|
||||
log('SDK initialization failed:', err)
|
||||
}
|
||||
}
|
||||
|
||||
function getConsents(): Record<ConsentPurpose, boolean> {
|
||||
return { ..._consents }
|
||||
}
|
||||
|
||||
function updateConsent(purpose: ConsentPurpose, granted: boolean): void {
|
||||
if (purpose === 'ESSENTIAL') {
|
||||
log('Cannot change essential consent')
|
||||
return
|
||||
}
|
||||
_consents[purpose] = granted
|
||||
storeConsents(_consents)
|
||||
_config?.onConsentChange?.(_consents)
|
||||
log('Consent updated:', purpose, granted)
|
||||
}
|
||||
|
||||
function showBanner(): void {
|
||||
showMainBanner()
|
||||
}
|
||||
|
||||
function hasConsent(purpose: ConsentPurpose): boolean {
|
||||
return _consents[purpose]
|
||||
}
|
||||
|
||||
function getClient(): ComplianceClient | null {
|
||||
return _client
|
||||
}
|
||||
|
||||
async function submitDSR(
|
||||
type: DSRRequestType,
|
||||
email: string,
|
||||
name: string
|
||||
): Promise<{ success: boolean; requestId?: string; error?: string }> {
|
||||
if (!_client) {
|
||||
return { success: false, error: 'SDK not initialized' }
|
||||
}
|
||||
|
||||
try {
|
||||
// This would call the DSR endpoint
|
||||
log('Submitting DSR:', { type, email, name })
|
||||
// In a real implementation, this would use the client to submit
|
||||
return { success: true, requestId: `dsr_${Date.now()}` }
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Unknown error'
|
||||
return { success: false, error: message }
|
||||
}
|
||||
}
|
||||
|
||||
function destroy(): void {
|
||||
hideBanner()
|
||||
_client = null
|
||||
_config = null
|
||||
_isInitialized = false
|
||||
log('SDK destroyed')
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Export for IIFE
|
||||
// ============================================================================
|
||||
|
||||
export const BreakPilotSDK = {
|
||||
init,
|
||||
getConsents,
|
||||
updateConsent,
|
||||
showBanner,
|
||||
hideBanner,
|
||||
hasConsent,
|
||||
getClient,
|
||||
submitDSR,
|
||||
destroy,
|
||||
version: '0.0.1',
|
||||
}
|
||||
|
||||
// Auto-attach to window for script tag usage
|
||||
if (typeof window !== 'undefined') {
|
||||
;(window as unknown as Record<string, unknown>).BreakPilotSDK = BreakPilotSDK
|
||||
}
|
||||
|
||||
export default BreakPilotSDK
|
||||
29
breakpilot-compliance-sdk/packages/vanilla/src/index.ts
Normal file
29
breakpilot-compliance-sdk/packages/vanilla/src/index.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* @breakpilot/compliance-sdk-vanilla
|
||||
*
|
||||
* Vanilla JS integration for BreakPilot Compliance SDK
|
||||
*
|
||||
* Includes:
|
||||
* - Embed Script (IIFE for <script> tag)
|
||||
* - Web Components (Custom Elements)
|
||||
*/
|
||||
|
||||
// Re-export embed script
|
||||
export { BreakPilotSDK, type BreakPilotSDKConfig, type BannerConfig } from './embed'
|
||||
|
||||
// Re-export web components
|
||||
export {
|
||||
BreakPilotElement,
|
||||
COMMON_STYLES,
|
||||
ConsentBannerElement,
|
||||
DSRPortalElement,
|
||||
ComplianceScoreElement,
|
||||
} from './web-components'
|
||||
|
||||
// Re-export types from core
|
||||
export type {
|
||||
ConsentPurpose,
|
||||
CookieBannerPosition,
|
||||
CookieBannerTheme,
|
||||
DSRRequestType,
|
||||
} from '@breakpilot/compliance-sdk-types'
|
||||
@@ -0,0 +1,95 @@
|
||||
/**
|
||||
* Base class for BreakPilot Web Components
|
||||
*/
|
||||
|
||||
import { ComplianceClient } from '@breakpilot/compliance-sdk-core'
|
||||
|
||||
export abstract class BreakPilotElement extends HTMLElement {
|
||||
protected client: ComplianceClient | null = null
|
||||
protected shadow: ShadowRoot
|
||||
|
||||
constructor() {
|
||||
super()
|
||||
this.shadow = this.attachShadow({ mode: 'open' })
|
||||
}
|
||||
|
||||
connectedCallback(): void {
|
||||
this.initializeClient()
|
||||
this.render()
|
||||
}
|
||||
|
||||
disconnectedCallback(): void {
|
||||
this.cleanup()
|
||||
}
|
||||
|
||||
attributeChangedCallback(
|
||||
name: string,
|
||||
_oldValue: string | null,
|
||||
_newValue: string | null
|
||||
): void {
|
||||
if (name === 'api-key' || name === 'api-endpoint') {
|
||||
this.initializeClient()
|
||||
}
|
||||
this.render()
|
||||
}
|
||||
|
||||
protected initializeClient(): void {
|
||||
const apiKey = this.getAttribute('api-key')
|
||||
const apiEndpoint =
|
||||
this.getAttribute('api-endpoint') || 'https://compliance.breakpilot.app/api/v1'
|
||||
const tenantId = this.getAttribute('tenant-id') || undefined
|
||||
|
||||
if (apiKey) {
|
||||
this.client = new ComplianceClient({
|
||||
apiKey,
|
||||
apiEndpoint,
|
||||
tenantId,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract render(): void
|
||||
protected cleanup(): void {}
|
||||
|
||||
protected createStyles(css: string): HTMLStyleElement {
|
||||
const style = document.createElement('style')
|
||||
style.textContent = css
|
||||
return style
|
||||
}
|
||||
|
||||
protected emit<T>(eventName: string, detail: T): void {
|
||||
this.dispatchEvent(
|
||||
new CustomEvent(eventName, {
|
||||
detail,
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Common styles
|
||||
export const COMMON_STYLES = `
|
||||
:host {
|
||||
display: block;
|
||||
font-family: system-ui, -apple-system, sans-serif;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
*, *::before, *::after {
|
||||
box-sizing: inherit;
|
||||
}
|
||||
|
||||
button {
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
input, textarea {
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
}
|
||||
`
|
||||
@@ -0,0 +1,136 @@
|
||||
/**
|
||||
* <breakpilot-compliance-score> Web Component
|
||||
*
|
||||
* Displays a circular compliance score indicator
|
||||
*
|
||||
* Usage:
|
||||
* <breakpilot-compliance-score
|
||||
* score="75"
|
||||
* size="medium"
|
||||
* show-label="true">
|
||||
* </breakpilot-compliance-score>
|
||||
*/
|
||||
|
||||
import { BreakPilotElement, COMMON_STYLES } from './base'
|
||||
|
||||
export class ComplianceScoreElement extends BreakPilotElement {
|
||||
static get observedAttributes(): string[] {
|
||||
return ['score', 'size', 'show-label', 'label']
|
||||
}
|
||||
|
||||
private get score(): number {
|
||||
const value = parseInt(this.getAttribute('score') || '0', 10)
|
||||
return Math.max(0, Math.min(100, value))
|
||||
}
|
||||
|
||||
private get size(): 'small' | 'medium' | 'large' {
|
||||
return (this.getAttribute('size') as 'small' | 'medium' | 'large') || 'medium'
|
||||
}
|
||||
|
||||
private get showLabel(): boolean {
|
||||
return this.getAttribute('show-label') !== 'false'
|
||||
}
|
||||
|
||||
private get label(): string {
|
||||
return this.getAttribute('label') || 'Compliance Score'
|
||||
}
|
||||
|
||||
private getColor(score: number): string {
|
||||
if (score >= 80) return '#16a34a' // Green
|
||||
if (score >= 60) return '#f59e0b' // Yellow
|
||||
if (score >= 40) return '#f97316' // Orange
|
||||
return '#dc2626' // Red
|
||||
}
|
||||
|
||||
protected render(): void {
|
||||
const sizes = {
|
||||
small: { width: 80, strokeWidth: 6, fontSize: 18 },
|
||||
medium: { width: 120, strokeWidth: 8, fontSize: 24 },
|
||||
large: { width: 160, strokeWidth: 10, fontSize: 32 },
|
||||
}
|
||||
|
||||
const { width, strokeWidth, fontSize } = sizes[this.size]
|
||||
const radius = (width - strokeWidth) / 2
|
||||
const circumference = radius * 2 * Math.PI
|
||||
const offset = circumference - (this.score / 100) * circumference
|
||||
const color = this.getColor(this.score)
|
||||
|
||||
const styles = `
|
||||
${COMMON_STYLES}
|
||||
|
||||
:host {
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.score-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
svg {
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
|
||||
.background-circle {
|
||||
fill: none;
|
||||
stroke: #e5e5e5;
|
||||
}
|
||||
|
||||
.progress-circle {
|
||||
fill: none;
|
||||
stroke: ${color};
|
||||
stroke-linecap: round;
|
||||
transition: stroke-dashoffset 0.5s ease-in-out;
|
||||
}
|
||||
|
||||
.score-text {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
font-size: ${fontSize}px;
|
||||
font-weight: 700;
|
||||
color: ${color};
|
||||
}
|
||||
|
||||
.label {
|
||||
margin-top: 8px;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
text-align: center;
|
||||
}
|
||||
`
|
||||
|
||||
this.shadow.innerHTML = `
|
||||
<style>${styles}</style>
|
||||
<div class="score-container">
|
||||
<svg width="${width}" height="${width}">
|
||||
<circle
|
||||
class="background-circle"
|
||||
cx="${width / 2}"
|
||||
cy="${width / 2}"
|
||||
r="${radius}"
|
||||
stroke-width="${strokeWidth}"
|
||||
/>
|
||||
<circle
|
||||
class="progress-circle"
|
||||
cx="${width / 2}"
|
||||
cy="${width / 2}"
|
||||
r="${radius}"
|
||||
stroke-width="${strokeWidth}"
|
||||
stroke-dasharray="${circumference}"
|
||||
stroke-dashoffset="${offset}"
|
||||
/>
|
||||
</svg>
|
||||
<div class="score-text">${this.score}%</div>
|
||||
</div>
|
||||
${this.showLabel ? `<div class="label">${this.label}</div>` : ''}
|
||||
`
|
||||
}
|
||||
}
|
||||
|
||||
// Register the custom element
|
||||
if (typeof customElements !== 'undefined') {
|
||||
customElements.define('breakpilot-compliance-score', ComplianceScoreElement)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -0,0 +1,464 @@
|
||||
/**
|
||||
* <breakpilot-dsr-portal> Web Component
|
||||
*
|
||||
* Data Subject Request Portal for GDPR rights
|
||||
*
|
||||
* Usage:
|
||||
* <breakpilot-dsr-portal
|
||||
* api-key="pk_live_xxx"
|
||||
* language="de">
|
||||
* </breakpilot-dsr-portal>
|
||||
*/
|
||||
|
||||
import type { DSRRequestType } from '@breakpilot/compliance-sdk-types'
|
||||
import { BreakPilotElement, COMMON_STYLES } from './base'
|
||||
|
||||
const TRANSLATIONS = {
|
||||
de: {
|
||||
title: 'Betroffenenrechte-Portal',
|
||||
subtitle:
|
||||
'Hier können Sie Ihre Rechte gemäß DSGVO wahrnehmen. Wählen Sie die gewünschte Anfrage und füllen Sie das Formular aus.',
|
||||
requestType: 'Art der Anfrage',
|
||||
name: 'Ihr Name',
|
||||
namePlaceholder: 'Max Mustermann',
|
||||
email: 'E-Mail-Adresse',
|
||||
emailPlaceholder: 'max@example.com',
|
||||
additionalInfo: 'Zusätzliche Informationen (optional)',
|
||||
additionalInfoPlaceholder: 'Weitere Details zu Ihrer Anfrage...',
|
||||
submit: 'Anfrage einreichen',
|
||||
submitting: 'Wird gesendet...',
|
||||
successTitle: 'Anfrage eingereicht',
|
||||
successMessage:
|
||||
'Wir werden Ihre Anfrage innerhalb von 30 Tagen bearbeiten. Sie erhalten eine Bestätigung per E-Mail an',
|
||||
disclaimer:
|
||||
'Ihre Anfrage wird gemäß Art. 12 DSGVO innerhalb von einem Monat bearbeitet. In komplexen Fällen kann diese Frist um weitere zwei Monate verlängert werden.',
|
||||
types: {
|
||||
ACCESS: {
|
||||
name: 'Auskunft (Art. 15)',
|
||||
description: 'Welche Daten haben Sie über mich gespeichert?',
|
||||
},
|
||||
RECTIFICATION: {
|
||||
name: 'Berichtigung (Art. 16)',
|
||||
description: 'Korrigieren Sie falsche Daten über mich.',
|
||||
},
|
||||
ERASURE: {
|
||||
name: 'Löschung (Art. 17)',
|
||||
description: 'Löschen Sie alle meine personenbezogenen Daten.',
|
||||
},
|
||||
PORTABILITY: {
|
||||
name: 'Datenübertragbarkeit (Art. 20)',
|
||||
description: 'Exportieren Sie meine Daten in einem maschinenlesbaren Format.',
|
||||
},
|
||||
RESTRICTION: {
|
||||
name: 'Einschränkung (Art. 18)',
|
||||
description: 'Schränken Sie die Verarbeitung meiner Daten ein.',
|
||||
},
|
||||
OBJECTION: {
|
||||
name: 'Widerspruch (Art. 21)',
|
||||
description: 'Ich widerspreche der Verarbeitung meiner Daten.',
|
||||
},
|
||||
},
|
||||
},
|
||||
en: {
|
||||
title: 'Data Subject Rights Portal',
|
||||
subtitle:
|
||||
'Here you can exercise your rights under GDPR. Select the type of request and fill out the form.',
|
||||
requestType: 'Request Type',
|
||||
name: 'Your Name',
|
||||
namePlaceholder: 'John Doe',
|
||||
email: 'Email Address',
|
||||
emailPlaceholder: 'john@example.com',
|
||||
additionalInfo: 'Additional Information (optional)',
|
||||
additionalInfoPlaceholder: 'Any additional details about your request...',
|
||||
submit: 'Submit Request',
|
||||
submitting: 'Submitting...',
|
||||
successTitle: 'Request Submitted',
|
||||
successMessage:
|
||||
'We will process your request within 30 days. You will receive a confirmation email at',
|
||||
disclaimer:
|
||||
'Your request will be processed in accordance with Article 12 GDPR within one month. In complex cases, this period may be extended by up to two additional months.',
|
||||
types: {
|
||||
ACCESS: {
|
||||
name: 'Access (Art. 15)',
|
||||
description: 'What data do you have about me?',
|
||||
},
|
||||
RECTIFICATION: {
|
||||
name: 'Rectification (Art. 16)',
|
||||
description: 'Correct inaccurate data about me.',
|
||||
},
|
||||
ERASURE: {
|
||||
name: 'Erasure (Art. 17)',
|
||||
description: 'Delete all my personal data.',
|
||||
},
|
||||
PORTABILITY: {
|
||||
name: 'Data Portability (Art. 20)',
|
||||
description: 'Export my data in a machine-readable format.',
|
||||
},
|
||||
RESTRICTION: {
|
||||
name: 'Restriction (Art. 18)',
|
||||
description: 'Restrict the processing of my data.',
|
||||
},
|
||||
OBJECTION: {
|
||||
name: 'Objection (Art. 21)',
|
||||
description: 'I object to the processing of my data.',
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export class DSRPortalElement extends BreakPilotElement {
|
||||
static get observedAttributes(): string[] {
|
||||
return ['api-key', 'api-endpoint', 'tenant-id', 'language']
|
||||
}
|
||||
|
||||
private selectedType: DSRRequestType | null = null
|
||||
private name = ''
|
||||
private email = ''
|
||||
private additionalInfo = ''
|
||||
private isSubmitting = false
|
||||
private isSubmitted = false
|
||||
private error: string | null = null
|
||||
|
||||
private get language(): 'de' | 'en' {
|
||||
return (this.getAttribute('language') as 'de' | 'en') || 'de'
|
||||
}
|
||||
|
||||
private get t() {
|
||||
return TRANSLATIONS[this.language]
|
||||
}
|
||||
|
||||
private handleTypeSelect = (type: DSRRequestType): void => {
|
||||
this.selectedType = type
|
||||
this.render()
|
||||
}
|
||||
|
||||
private handleSubmit = async (e: Event): Promise<void> => {
|
||||
e.preventDefault()
|
||||
|
||||
if (!this.selectedType || !this.email || !this.name) {
|
||||
return
|
||||
}
|
||||
|
||||
this.isSubmitting = true
|
||||
this.error = null
|
||||
this.render()
|
||||
|
||||
try {
|
||||
// In a real implementation, this would use the client
|
||||
// For now, we simulate a successful submission
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
|
||||
this.emit('dsr-submitted', {
|
||||
type: this.selectedType,
|
||||
email: this.email,
|
||||
name: this.name,
|
||||
additionalInfo: this.additionalInfo,
|
||||
})
|
||||
|
||||
this.isSubmitted = true
|
||||
} catch (err) {
|
||||
this.error = err instanceof Error ? err.message : 'Ein Fehler ist aufgetreten'
|
||||
} finally {
|
||||
this.isSubmitting = false
|
||||
this.render()
|
||||
}
|
||||
}
|
||||
|
||||
protected render(): void {
|
||||
const styles = `
|
||||
${COMMON_STYLES}
|
||||
|
||||
:host {
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.portal {
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin: 0 0 10px;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #1a1a1a;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
margin: 0 0 20px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.label {
|
||||
display: block;
|
||||
font-weight: 500;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.type-options {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.type-option {
|
||||
display: flex;
|
||||
padding: 15px;
|
||||
border: 2px solid #ddd;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
background: #fff;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.type-option:hover {
|
||||
border-color: #999;
|
||||
}
|
||||
|
||||
.type-option.selected {
|
||||
border-color: #1a1a1a;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.type-option input {
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
.type-name {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.type-description {
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.input {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
font-size: 14px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.input:focus {
|
||||
outline: none;
|
||||
border-color: #1a1a1a;
|
||||
}
|
||||
|
||||
.textarea {
|
||||
resize: vertical;
|
||||
min-height: 100px;
|
||||
}
|
||||
|
||||
.error {
|
||||
padding: 12px;
|
||||
background: #fef2f2;
|
||||
color: #dc2626;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.btn-submit {
|
||||
width: 100%;
|
||||
padding: 12px 24px;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: #fff;
|
||||
background: #1a1a1a;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn-submit:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.disclaimer {
|
||||
margin-top: 20px;
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.success {
|
||||
text-align: center;
|
||||
padding: 40px 20px;
|
||||
background: #f0fdf4;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.success-icon {
|
||||
font-size: 48px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.success-title {
|
||||
margin: 0 0 10px;
|
||||
color: #166534;
|
||||
}
|
||||
|
||||
.success-message {
|
||||
margin: 0;
|
||||
color: #166534;
|
||||
}
|
||||
`
|
||||
|
||||
if (this.isSubmitted) {
|
||||
this.renderSuccess(styles)
|
||||
} else {
|
||||
this.renderForm(styles)
|
||||
}
|
||||
}
|
||||
|
||||
private renderForm(styles: string): void {
|
||||
const t = this.t
|
||||
const types: DSRRequestType[] = [
|
||||
'ACCESS',
|
||||
'RECTIFICATION',
|
||||
'ERASURE',
|
||||
'PORTABILITY',
|
||||
'RESTRICTION',
|
||||
'OBJECTION',
|
||||
]
|
||||
|
||||
const typesHtml = types
|
||||
.map(
|
||||
type => `
|
||||
<label class="type-option ${this.selectedType === type ? 'selected' : ''}">
|
||||
<input
|
||||
type="radio"
|
||||
name="dsrType"
|
||||
value="${type}"
|
||||
${this.selectedType === type ? 'checked' : ''}
|
||||
/>
|
||||
<div>
|
||||
<div class="type-name">${t.types[type].name}</div>
|
||||
<div class="type-description">${t.types[type].description}</div>
|
||||
</div>
|
||||
</label>
|
||||
`
|
||||
)
|
||||
.join('')
|
||||
|
||||
const isValid = this.selectedType && this.email && this.name
|
||||
const isDisabled = !isValid || this.isSubmitting
|
||||
|
||||
this.shadow.innerHTML = `
|
||||
<style>${styles}</style>
|
||||
<div class="portal">
|
||||
<h2 class="title">${t.title}</h2>
|
||||
<p class="subtitle">${t.subtitle}</p>
|
||||
|
||||
<form id="dsr-form">
|
||||
<div class="form-group">
|
||||
<label class="label">${t.requestType} *</label>
|
||||
<div class="type-options">
|
||||
${typesHtml}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="label">${t.name} *</label>
|
||||
<input
|
||||
type="text"
|
||||
class="input"
|
||||
id="name-input"
|
||||
value="${this.name}"
|
||||
placeholder="${t.namePlaceholder}"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="label">${t.email} *</label>
|
||||
<input
|
||||
type="email"
|
||||
class="input"
|
||||
id="email-input"
|
||||
value="${this.email}"
|
||||
placeholder="${t.emailPlaceholder}"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="label">${t.additionalInfo}</label>
|
||||
<textarea
|
||||
class="input textarea"
|
||||
id="info-input"
|
||||
placeholder="${t.additionalInfoPlaceholder}"
|
||||
rows="4"
|
||||
>${this.additionalInfo}</textarea>
|
||||
</div>
|
||||
|
||||
${this.error ? `<div class="error">${this.error}</div>` : ''}
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
class="btn-submit"
|
||||
${isDisabled ? 'disabled' : ''}
|
||||
>
|
||||
${this.isSubmitting ? t.submitting : t.submit}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<p class="disclaimer">${t.disclaimer}</p>
|
||||
</div>
|
||||
`
|
||||
|
||||
// Bind events
|
||||
const form = this.shadow.getElementById('dsr-form') as HTMLFormElement
|
||||
form.onsubmit = this.handleSubmit
|
||||
|
||||
const nameInput = this.shadow.getElementById('name-input') as HTMLInputElement
|
||||
nameInput.oninput = e => {
|
||||
this.name = (e.target as HTMLInputElement).value
|
||||
}
|
||||
|
||||
const emailInput = this.shadow.getElementById('email-input') as HTMLInputElement
|
||||
emailInput.oninput = e => {
|
||||
this.email = (e.target as HTMLInputElement).value
|
||||
}
|
||||
|
||||
const infoInput = this.shadow.getElementById('info-input') as HTMLTextAreaElement
|
||||
infoInput.oninput = e => {
|
||||
this.additionalInfo = (e.target as HTMLTextAreaElement).value
|
||||
}
|
||||
|
||||
// Bind radio buttons
|
||||
this.shadow.querySelectorAll<HTMLInputElement>('input[name="dsrType"]').forEach(radio => {
|
||||
radio.onchange = () => {
|
||||
this.handleTypeSelect(radio.value as DSRRequestType)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private renderSuccess(styles: string): void {
|
||||
const t = this.t
|
||||
|
||||
this.shadow.innerHTML = `
|
||||
<style>${styles}</style>
|
||||
<div class="portal">
|
||||
<div class="success">
|
||||
<div class="success-icon">✓</div>
|
||||
<h2 class="success-title">${t.successTitle}</h2>
|
||||
<p class="success-message">
|
||||
${t.successMessage} ${this.email}.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
}
|
||||
|
||||
// Register the custom element
|
||||
if (typeof customElements !== 'undefined') {
|
||||
customElements.define('breakpilot-dsr-portal', DSRPortalElement)
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* BreakPilot Compliance SDK - Web Components
|
||||
*
|
||||
* Available components:
|
||||
* - <breakpilot-consent-banner>
|
||||
* - <breakpilot-dsr-portal>
|
||||
* - <breakpilot-compliance-score>
|
||||
*/
|
||||
|
||||
export { BreakPilotElement, COMMON_STYLES } from './base'
|
||||
export { ConsentBannerElement } from './consent-banner'
|
||||
export { DSRPortalElement } from './dsr-portal'
|
||||
export { ComplianceScoreElement } from './compliance-score'
|
||||
Reference in New Issue
Block a user