This repository has been archived on 2026-02-15. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
breakpilot-pwa/breakpilot-compliance-sdk/packages/react/src/components/ConsentBanner.tsx
BreakPilot Dev 19855efacc
Some checks failed
Tests / Go Tests (push) Has been cancelled
Tests / Python Tests (push) Has been cancelled
Tests / Integration Tests (push) Has been cancelled
Tests / Go Lint (push) Has been cancelled
Tests / Python Lint (push) Has been cancelled
Tests / Security Scan (push) Has been cancelled
Tests / All Checks Passed (push) Has been cancelled
Security Scanning / Secret Scanning (push) Has been cancelled
Security Scanning / Dependency Vulnerability Scan (push) Has been cancelled
Security Scanning / Go Security Scan (push) Has been cancelled
Security Scanning / Python Security Scan (push) Has been cancelled
Security Scanning / Node.js Security Scan (push) Has been cancelled
Security Scanning / Docker Image Security (push) Has been cancelled
Security Scanning / Security Summary (push) Has been cancelled
CI/CD Pipeline / Go Tests (push) Has been cancelled
CI/CD Pipeline / Python Tests (push) Has been cancelled
CI/CD Pipeline / Website Tests (push) Has been cancelled
CI/CD Pipeline / Linting (push) Has been cancelled
CI/CD Pipeline / Security Scan (push) Has been cancelled
CI/CD Pipeline / Docker Build & Push (push) Has been cancelled
CI/CD Pipeline / Integration Tests (push) Has been cancelled
CI/CD Pipeline / Deploy to Staging (push) Has been cancelled
CI/CD Pipeline / Deploy to Production (push) Has been cancelled
CI/CD Pipeline / CI Summary (push) Has been cancelled
ci/woodpecker/manual/build-ci-image Pipeline was successful
ci/woodpecker/manual/main Pipeline failed
feat: BreakPilot PWA - Full codebase (clean push without large binaries)
All services: admin-v2, studio-v2, website, ai-compliance-sdk,
consent-service, klausur-service, voice-service, and infrastructure.
Large PDFs and compiled binaries excluded via .gitignore.
2026-02-11 13:25:58 +01:00

214 lines
7.0 KiB
TypeScript

'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>
)
}