Files
breakpilot-compliance/admin-compliance/app/sdk/cookie-banner/preview/page.tsx
T
Benjamin Admin 3bf0804af6
CI / nodejs-build (push) Successful in 2m37s
CI / dep-audit (push) Has been skipped
CI / sbom-scan (push) Has been skipped
CI / test-go (push) Successful in 47s
CI / test-python-backend (push) Successful in 39s
CI / test-python-document-crawler (push) Successful in 25s
CI / test-python-dsms-gateway (push) Successful in 20s
CI / validate-canonical-controls (push) Successful in 14s
Build + Deploy / trigger-orca (push) Successful in 2m10s
Build + Deploy / build-admin-compliance (push) Successful in 1m55s
Build + Deploy / build-backend-compliance (push) Successful in 2m57s
Build + Deploy / build-ai-sdk (push) Successful in 36s
Build + Deploy / build-developer-portal (push) Successful in 1m8s
Build + Deploy / build-tts (push) Successful in 1m17s
Build + Deploy / build-document-crawler (push) Successful in 35s
Build + Deploy / build-dsms-gateway (push) Successful in 21s
Build + Deploy / build-dsms-node (push) Successful in 10s
CI / branch-name (push) Has been skipped
CI / guardrail-integrity (push) Has been skipped
CI / loc-budget (push) Failing after 15s
CI / secret-scan (push) Has been skipped
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
feat: CMP sidebar section + cookie banner live preview page
CMP Section in Sidebar:
- New "CMP" group with purple accent, above other module sections
- Links: Cookie-Banner, Live-Vorschau, Consent-Records, Consent-Verwaltung,
  Vendor-Compliance, DSR Portal, Loeschfristen, E-Mail-Templates

Live Preview (/sdk/cookie-banner/preview):
- Simulated "MusterShop GmbH" website with full cookie banner
- Real API calls to POST /banner/consent (saves to DB)
- EWR-Only toggle functional in preview
- API Debug panel shows fingerprint, consent status, blocked vendors
- Response JSON viewer for API debugging
- Links to verify in Consent-Verwaltung, Consent-Records, DSR Portal
- "Consent zuruecksetzen" button to re-test
- Footer "Cookie-Einstellungen" link to reopen banner

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-03 08:07:00 +02:00

354 lines
16 KiB
TypeScript

'use client'
import { useState, useEffect, useCallback } from 'react'
import {
CATEGORY_VENDORS, countNonEWRVendors, isEWR, isOutsideEWR,
} from '@/components/sdk/cookie-banner-vendors'
/**
* Cookie Banner Live-Vorschau — simulates a real website with the banner.
*
* Purpose: Test the full consent flow end-to-end:
* 1. Visitor lands on simulated website → banner appears
* 2. Visitor makes consent choice (accept/reject/custom + EWR toggle)
* 3. Consent is recorded via Banner API (POST /banner/consent)
* 4. Admin can verify in /sdk/consent-management and /sdk/einwilligungen
*
* This page runs OUTSIDE the SDK layout to simulate a real website experience.
*/
const API_BASE = typeof window !== 'undefined'
? (process.env.NEXT_PUBLIC_SDK_URL || `${window.location.protocol}//${window.location.hostname}:8093`)
: ''
const SITE_ID = 'preview-test-site'
const TENANT_ID = '9282a473-5c95-4b3a-bf78-0ecc0ec71d3e'
interface ConsentRecord {
id: string
categories: string[]
ewrOnly: boolean
blockedVendors: string[]
timestamp: string
device_fingerprint: string
}
function generateFingerprint(): string {
const nav = typeof navigator !== 'undefined' ? navigator : null
const seed = [
nav?.userAgent || '',
nav?.language || '',
screen?.width || 0,
screen?.height || 0,
new Date().getTimezoneOffset(),
].join('|')
let hash = 0
for (let i = 0; i < seed.length; i++) {
hash = ((hash << 5) - hash + seed.charCodeAt(i)) | 0
}
return `fp-${Math.abs(hash).toString(36)}-${Date.now().toString(36)}`
}
export default function CookieBannerPreviewPage() {
const [consent, setConsent] = useState<ConsentRecord | null>(null)
const [showBanner, setShowBanner] = useState(true)
const [ewrOnly, setEwrOnly] = useState(false)
const [categories, setCategories] = useState({ necessary: true, statistics: false, marketing: false, functional: false })
const [saving, setSaving] = useState(false)
const [apiResult, setApiResult] = useState<any>(null)
const [fingerprint] = useState(() => generateFingerprint())
// Check for existing consent on this simulated site
useEffect(() => {
async function check() {
try {
const res = await fetch(
`${API_BASE}/banner/consent?site_id=${SITE_ID}&device_fingerprint=${fingerprint}`,
{ headers: { 'X-Tenant-ID': TENANT_ID } },
)
if (res.ok) {
const data = await res.json()
if (data.has_consent) {
setConsent(data.consent)
setShowBanner(false)
}
}
} catch { /* first visit */ }
}
check()
}, [fingerprint])
const saveConsent = useCallback(async (cats: typeof categories) => {
setSaving(true)
const blocked: string[] = []
if (ewrOnly) {
for (const [key, cat] of Object.entries(CATEGORY_VENDORS)) {
if (!cats[key as keyof typeof cats]) continue
for (const v of cat.vendors) {
if (isOutsideEWR(v.country)) blocked.push(v.name)
}
}
}
try {
const res = await fetch(`${API_BASE}/banner/consent`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-Tenant-ID': TENANT_ID },
body: JSON.stringify({
site_id: SITE_ID,
device_fingerprint: fingerprint,
categories: Object.entries(cats).filter(([, v]) => v).map(([k]) => k),
vendors: [],
consent_string: JSON.stringify({ ewrOnly, blockedVendors: blocked }),
user_agent: navigator.userAgent,
}),
})
const data = await res.json()
setApiResult(data)
setConsent({ ...data, ewrOnly, blockedVendors: blocked, timestamp: new Date().toISOString() })
setShowBanner(false)
} catch (err: any) {
setApiResult({ error: err.message })
}
setSaving(false)
}, [ewrOnly, fingerprint])
const nonEWRCount = countNonEWRVendors()
return (
<div className="min-h-screen bg-white">
{/* Simulated Website Header */}
<header className="bg-slate-800 text-white px-8 py-4">
<div className="max-w-6xl mx-auto flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="w-8 h-8 bg-blue-500 rounded-lg" />
<span className="font-semibold text-lg">MusterShop GmbH</span>
</div>
<nav className="flex items-center gap-6 text-sm text-slate-300">
<span className="hover:text-white cursor-pointer">Produkte</span>
<span className="hover:text-white cursor-pointer">Ueber uns</span>
<span className="hover:text-white cursor-pointer">Kontakt</span>
<span className="px-3 py-1.5 bg-blue-600 text-white rounded-lg text-sm">Warenkorb (2)</span>
</nav>
</div>
</header>
{/* Simulated Website Content */}
<main className="max-w-6xl mx-auto px-8 py-12">
<div className="grid grid-cols-3 gap-8">
<div className="col-span-2 space-y-6">
<h1 className="text-3xl font-bold text-gray-900">Willkommen bei MusterShop</h1>
<p className="text-gray-600 leading-relaxed">
Dies ist eine simulierte Website um den Cookie-Banner zu testen.
Die Consent-Daten werden ueber die echte Banner-API gespeichert und
erscheinen in Ihrem CMP unter Consent-Records und Consent-Verwaltung.
</p>
<div className="grid grid-cols-2 gap-4">
{['Premium Paket', 'Standard Paket', 'Starter Paket', 'Enterprise'].map(p => (
<div key={p} className="bg-gray-50 border border-gray-200 rounded-xl p-6">
<div className="w-full h-24 bg-gray-200 rounded-lg mb-3" />
<h3 className="font-semibold text-gray-900">{p}</h3>
<p className="text-sm text-gray-500 mt-1">Lorem ipsum dolor sit amet</p>
</div>
))}
</div>
</div>
{/* API Debug Panel */}
<div className="space-y-4">
<div className="bg-slate-50 border border-slate-200 rounded-xl p-4">
<h3 className="font-semibold text-slate-800 text-sm flex items-center gap-2">
<svg className="w-4 h-4 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4" />
</svg>
API Debug
</h3>
<div className="mt-3 space-y-2 text-xs">
<div className="flex justify-between">
<span className="text-slate-500">Site ID</span>
<code className="text-slate-700">{SITE_ID}</code>
</div>
<div className="flex justify-between">
<span className="text-slate-500">Fingerprint</span>
<code className="text-slate-700 truncate ml-2">{fingerprint}</code>
</div>
<div className="flex justify-between">
<span className="text-slate-500">Consent</span>
<span className={consent ? 'text-green-600 font-medium' : 'text-amber-600'}>
{consent ? 'Gespeichert' : 'Ausstehend'}
</span>
</div>
{consent && (
<>
<div className="flex justify-between">
<span className="text-slate-500">Kategorien</span>
<span className="text-slate-700">{consent.categories?.join(', ')}</span>
</div>
<div className="flex justify-between">
<span className="text-slate-500">EWR-Only</span>
<span className={consent.ewrOnly ? 'text-blue-600' : 'text-slate-400'}>
{consent.ewrOnly ? 'Ja' : 'Nein'}
</span>
</div>
{consent.blockedVendors?.length > 0 && (
<div>
<span className="text-slate-500">Blockiert:</span>
<div className="mt-1 flex flex-wrap gap-1">
{consent.blockedVendors.map(v => (
<span key={v} className="px-1.5 py-0.5 bg-red-100 text-red-700 rounded text-[10px]">{v}</span>
))}
</div>
</div>
)}
</>
)}
</div>
{consent && (
<button
onClick={() => { setConsent(null); setShowBanner(true); setApiResult(null) }}
className="mt-3 w-full text-xs text-purple-600 hover:text-purple-700 underline"
>
Consent zuruecksetzen (Banner erneut anzeigen)
</button>
)}
</div>
{apiResult && (
<div className="bg-slate-900 text-green-400 rounded-xl p-4 text-xs font-mono overflow-auto max-h-48">
<div className="text-slate-500 mb-1">POST /banner/consent Response:</div>
{JSON.stringify(apiResult, null, 2)}
</div>
)}
<div className="bg-purple-50 border border-purple-200 rounded-xl p-4 text-xs text-purple-800">
<div className="font-semibold mb-1">Pruefen Sie das Ergebnis in:</div>
<ul className="space-y-1 mt-2">
<li><a href="/sdk/consent-management" className="underline hover:text-purple-600">Consent-Verwaltung</a></li>
<li><a href="/sdk/einwilligungen" className="underline hover:text-purple-600">Consent-Records</a></li>
<li><a href="/sdk/dsr" className="underline hover:text-purple-600">DSR Portal</a></li>
</ul>
</div>
</div>
</div>
</main>
{/* Simulated Website Footer */}
<footer className="bg-slate-100 border-t border-slate-200 px-8 py-6 mt-12">
<div className="max-w-6xl mx-auto flex items-center justify-between text-sm text-slate-500">
<span>MusterShop GmbH Simulierte Test-Website</span>
<div className="flex items-center gap-4">
<button onClick={() => setShowBanner(true)} className="underline hover:text-purple-600">
Cookie-Einstellungen
</button>
<span>Datenschutz</span>
<span>Impressum</span>
</div>
</div>
</footer>
{/* === REAL COOKIE BANNER === */}
{showBanner && (
<>
<div className="fixed inset-0 bg-black/40 z-[9998]" />
<div className="fixed bottom-0 left-0 right-0 z-[9999]">
<div className="max-w-3xl mx-auto m-4 bg-white rounded-2xl shadow-2xl border border-gray-200 overflow-hidden">
{/* Header */}
<div className="px-6 pt-5 pb-3">
<div className="flex items-start justify-between gap-4">
<div className="flex-1">
<h2 className="text-lg font-semibold text-gray-900">Cookie-Einstellungen</h2>
<p className="text-sm text-gray-600 mt-1">
Waehlen Sie, welche Cookie-Kategorien Sie zulassen moechten.
</p>
</div>
{/* EWR Toggle */}
<div className="flex flex-col items-end gap-1 shrink-0">
<div className="flex items-center gap-2">
<span className={`text-xs font-medium ${ewrOnly ? 'text-blue-700' : 'text-gray-500'}`}>
Nur EU/EWR
</span>
<button
onClick={() => setEwrOnly(!ewrOnly)}
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors cursor-pointer ${
ewrOnly ? 'bg-blue-600' : 'bg-gray-200'
}`}
>
<span className={`inline-block h-4 w-4 transform rounded-full bg-white shadow-sm transition-transform ${
ewrOnly ? 'translate-x-6' : 'translate-x-1'
}`} />
</button>
</div>
</div>
</div>
</div>
{/* Categories */}
<div className="px-6 pb-3 space-y-1.5 max-h-[40vh] overflow-y-auto border-t border-gray-100 pt-3">
{Object.entries(CATEGORY_VENDORS).map(([key, cat]) => {
const checked = key === 'necessary' ? true : categories[key as keyof typeof categories]
const nonEU = cat.vendors.filter(v => isOutsideEWR(v.country))
const blocked = ewrOnly && checked ? nonEU.length : 0
return (
<div key={key} className="flex items-center justify-between gap-3 px-4 py-2.5 border border-gray-100 rounded-lg bg-gray-50/50">
<div>
<div className="text-sm font-medium text-gray-900">
{cat.label}
<span className="ml-2 text-xs font-normal text-gray-400">
{blocked > 0 ? `${cat.vendors.length - blocked} aktiv, ${blocked} blockiert` : `${cat.vendors.length} Verarbeiter`}
</span>
</div>
<div className="text-xs text-gray-500">{cat.description}</div>
</div>
<button
onClick={() => key !== 'necessary' && setCategories(prev => ({ ...prev, [key]: !prev[key as keyof typeof prev] }))}
disabled={key === 'necessary'}
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors shrink-0 ${
checked ? (key === 'necessary' ? 'bg-gray-400' : 'bg-purple-600') : 'bg-gray-200'
} ${key === 'necessary' ? 'cursor-not-allowed opacity-60' : 'cursor-pointer'}`}
>
<span className={`inline-block h-4 w-4 transform rounded-full bg-white shadow-sm transition-transform ${
checked ? 'translate-x-6' : 'translate-x-1'
}`} />
</button>
</div>
)
})}
</div>
{/* Buttons */}
<div className="px-6 py-4 bg-gray-50 border-t border-gray-100">
<div className="flex items-center gap-3">
<button
onClick={() => saveConsent({ necessary: true, statistics: true, marketing: true, functional: true })}
disabled={saving}
className="flex-1 px-4 py-2.5 bg-purple-600 text-white rounded-lg font-medium hover:bg-purple-700 transition-colors text-sm disabled:opacity-50"
>
{saving ? 'Speichern...' : 'Alle akzeptieren'}
</button>
<button
onClick={() => saveConsent(categories)}
disabled={saving}
className="flex-1 px-4 py-2.5 bg-gray-200 text-gray-700 rounded-lg font-medium hover:bg-gray-300 transition-colors text-sm disabled:opacity-50"
>
Auswahl speichern
</button>
</div>
<div className="flex items-center justify-between mt-3">
<button
onClick={() => saveConsent({ necessary: true, statistics: false, marketing: false, functional: false })}
className="text-xs text-gray-500 hover:text-gray-700 underline"
>
Nur notwendige Cookies
</button>
<div className="flex items-center gap-3 text-xs text-gray-400">
<span>Datenschutzerklaerung</span>
<span>Impressum</span>
</div>
</div>
</div>
</div>
</div>
</>
)}
</div>
)
}