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,213 @@
|
||||
'use client'
|
||||
|
||||
import React, { useState, useCallback } from 'react'
|
||||
import { useCompliance } from '../provider'
|
||||
import type { ConsentPurpose, CookieBannerPosition, CookieBannerTheme } from '@breakpilot/compliance-sdk-types'
|
||||
|
||||
export interface ConsentBannerProps {
|
||||
position?: CookieBannerPosition
|
||||
theme?: CookieBannerTheme
|
||||
onConsentChange?: (consents: Record<ConsentPurpose, boolean>) => void
|
||||
privacyPolicyUrl?: string
|
||||
imprintUrl?: string
|
||||
className?: string
|
||||
style?: React.CSSProperties
|
||||
}
|
||||
|
||||
export function ConsentBanner({
|
||||
position = 'BOTTOM',
|
||||
theme = 'LIGHT',
|
||||
onConsentChange,
|
||||
privacyPolicyUrl = '/privacy',
|
||||
imprintUrl = '/imprint',
|
||||
className,
|
||||
style,
|
||||
}: ConsentBannerProps) {
|
||||
const { state, dispatch } = useCompliance()
|
||||
const [showSettings, setShowSettings] = useState(false)
|
||||
const [consents, setConsents] = useState<Record<ConsentPurpose, boolean>>({
|
||||
ESSENTIAL: true,
|
||||
FUNCTIONAL: false,
|
||||
ANALYTICS: false,
|
||||
MARKETING: false,
|
||||
PERSONALIZATION: false,
|
||||
THIRD_PARTY: false,
|
||||
})
|
||||
|
||||
const config = state.cookieBanner
|
||||
|
||||
const handleAcceptAll = useCallback(() => {
|
||||
const allConsents: Record<ConsentPurpose, boolean> = {
|
||||
ESSENTIAL: true,
|
||||
FUNCTIONAL: true,
|
||||
ANALYTICS: true,
|
||||
MARKETING: true,
|
||||
PERSONALIZATION: true,
|
||||
THIRD_PARTY: true,
|
||||
}
|
||||
setConsents(allConsents)
|
||||
onConsentChange?.(allConsents)
|
||||
// Would save to backend here
|
||||
}, [onConsentChange])
|
||||
|
||||
const handleRejectAll = useCallback(() => {
|
||||
const minimalConsents: Record<ConsentPurpose, boolean> = {
|
||||
ESSENTIAL: true,
|
||||
FUNCTIONAL: false,
|
||||
ANALYTICS: false,
|
||||
MARKETING: false,
|
||||
PERSONALIZATION: false,
|
||||
THIRD_PARTY: false,
|
||||
}
|
||||
setConsents(minimalConsents)
|
||||
onConsentChange?.(minimalConsents)
|
||||
}, [onConsentChange])
|
||||
|
||||
const handleSaveSettings = useCallback(() => {
|
||||
onConsentChange?.(consents)
|
||||
setShowSettings(false)
|
||||
}, [consents, onConsentChange])
|
||||
|
||||
const handleConsentToggle = useCallback((purpose: ConsentPurpose) => {
|
||||
if (purpose === 'ESSENTIAL') return // Cannot disable essential
|
||||
setConsents(prev => ({ ...prev, [purpose]: !prev[purpose] }))
|
||||
}, [])
|
||||
|
||||
const positionStyles: Record<CookieBannerPosition, React.CSSProperties> = {
|
||||
TOP: { top: 0, left: 0, right: 0 },
|
||||
BOTTOM: { bottom: 0, left: 0, right: 0 },
|
||||
CENTER: { top: '50%', left: '50%', transform: 'translate(-50%, -50%)' },
|
||||
}
|
||||
|
||||
const themeStyles: Record<CookieBannerTheme, React.CSSProperties> = {
|
||||
LIGHT: { backgroundColor: '#ffffff', color: '#1a1a1a' },
|
||||
DARK: { backgroundColor: '#1a1a1a', color: '#ffffff' },
|
||||
CUSTOM: config?.customColors
|
||||
? {
|
||||
backgroundColor: config.customColors.background,
|
||||
color: config.customColors.text,
|
||||
}
|
||||
: {},
|
||||
}
|
||||
|
||||
const bannerStyle: React.CSSProperties = {
|
||||
position: 'fixed',
|
||||
zIndex: 9999,
|
||||
padding: '20px',
|
||||
boxShadow: '0 -2px 10px rgba(0, 0, 0, 0.1)',
|
||||
fontFamily: 'system-ui, -apple-system, sans-serif',
|
||||
...positionStyles[position],
|
||||
...themeStyles[theme],
|
||||
...style,
|
||||
}
|
||||
|
||||
const buttonBaseStyle: React.CSSProperties = {
|
||||
padding: '10px 20px',
|
||||
borderRadius: '4px',
|
||||
border: 'none',
|
||||
cursor: 'pointer',
|
||||
fontWeight: 500,
|
||||
marginRight: '10px',
|
||||
}
|
||||
|
||||
const primaryButtonStyle: React.CSSProperties = {
|
||||
...buttonBaseStyle,
|
||||
backgroundColor: theme === 'DARK' ? '#ffffff' : '#1a1a1a',
|
||||
color: theme === 'DARK' ? '#1a1a1a' : '#ffffff',
|
||||
}
|
||||
|
||||
const secondaryButtonStyle: React.CSSProperties = {
|
||||
...buttonBaseStyle,
|
||||
backgroundColor: 'transparent',
|
||||
border: `1px solid ${theme === 'DARK' ? '#ffffff' : '#1a1a1a'}`,
|
||||
color: theme === 'DARK' ? '#ffffff' : '#1a1a1a',
|
||||
}
|
||||
|
||||
if (showSettings) {
|
||||
return (
|
||||
<div style={bannerStyle} className={className}>
|
||||
<h3 style={{ margin: '0 0 15px', fontSize: '18px' }}>
|
||||
{config?.texts?.settings || 'Cookie-Einstellungen'}
|
||||
</h3>
|
||||
|
||||
<div style={{ marginBottom: '20px' }}>
|
||||
{Object.entries({
|
||||
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' },
|
||||
}).map(([key, { name, description }]) => (
|
||||
<div
|
||||
key={key}
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
padding: '10px 0',
|
||||
borderBottom: `1px solid ${theme === 'DARK' ? '#333' : '#eee'}`,
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<div style={{ fontWeight: 500 }}>{name}</div>
|
||||
<div style={{ fontSize: '12px', opacity: 0.7 }}>{description}</div>
|
||||
</div>
|
||||
<label style={{ cursor: key === 'ESSENTIAL' ? 'not-allowed' : 'pointer' }}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={consents[key as ConsentPurpose]}
|
||||
onChange={() => handleConsentToggle(key as ConsentPurpose)}
|
||||
disabled={key === 'ESSENTIAL'}
|
||||
style={{ width: '20px', height: '20px' }}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '10px' }}>
|
||||
<button style={primaryButtonStyle} onClick={handleSaveSettings}>
|
||||
{config?.texts?.save || 'Einstellungen speichern'}
|
||||
</button>
|
||||
<button style={secondaryButtonStyle} onClick={() => setShowSettings(false)}>
|
||||
Zurück
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={bannerStyle} className={className}>
|
||||
<div style={{ marginBottom: '15px' }}>
|
||||
<h3 style={{ margin: '0 0 10px', fontSize: '18px' }}>
|
||||
{config?.texts?.title || 'Cookie-Einwilligung'}
|
||||
</h3>
|
||||
<p style={{ margin: 0, fontSize: '14px', opacity: 0.8 }}>
|
||||
{config?.texts?.description ||
|
||||
'Wir verwenden Cookies, um Ihre Erfahrung zu verbessern. Weitere Informationen finden Sie in unserer Datenschutzerklärung.'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '10px', alignItems: 'center' }}>
|
||||
<button style={primaryButtonStyle} onClick={handleAcceptAll}>
|
||||
{config?.texts?.acceptAll || 'Alle akzeptieren'}
|
||||
</button>
|
||||
<button style={secondaryButtonStyle} onClick={handleRejectAll}>
|
||||
{config?.texts?.rejectAll || 'Nur notwendige'}
|
||||
</button>
|
||||
<button style={secondaryButtonStyle} onClick={() => setShowSettings(true)}>
|
||||
{config?.texts?.settings || 'Einstellungen'}
|
||||
</button>
|
||||
|
||||
<div style={{ marginLeft: 'auto', fontSize: '12px' }}>
|
||||
<a href={privacyPolicyUrl} style={{ marginRight: '15px', color: 'inherit' }}>
|
||||
Datenschutz
|
||||
</a>
|
||||
<a href={imprintUrl} style={{ color: 'inherit' }}>
|
||||
Impressum
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user