diff --git a/admin-compliance/app/sdk/cookie-banner/preview/page.tsx b/admin-compliance/app/sdk/cookie-banner/preview/page.tsx new file mode 100644 index 0000000..f832afe --- /dev/null +++ b/admin-compliance/app/sdk/cookie-banner/preview/page.tsx @@ -0,0 +1,353 @@ +'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(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(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 ( +
+ {/* Simulated Website Header */} +
+
+
+
+ MusterShop GmbH +
+ +
+
+ + {/* Simulated Website Content */} +
+
+
+

Willkommen bei MusterShop

+

+ 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. +

+
+ {['Premium Paket', 'Standard Paket', 'Starter Paket', 'Enterprise'].map(p => ( +
+
+

{p}

+

Lorem ipsum dolor sit amet

+
+ ))} +
+
+ + {/* API Debug Panel */} +
+
+

+ + + + API Debug +

+
+
+ Site ID + {SITE_ID} +
+
+ Fingerprint + {fingerprint} +
+
+ Consent + + {consent ? 'Gespeichert' : 'Ausstehend'} + +
+ {consent && ( + <> +
+ Kategorien + {consent.categories?.join(', ')} +
+
+ EWR-Only + + {consent.ewrOnly ? 'Ja' : 'Nein'} + +
+ {consent.blockedVendors?.length > 0 && ( +
+ Blockiert: +
+ {consent.blockedVendors.map(v => ( + {v} + ))} +
+
+ )} + + )} +
+ {consent && ( + + )} +
+ + {apiResult && ( +
+
POST /banner/consent Response:
+ {JSON.stringify(apiResult, null, 2)} +
+ )} + +
+
Pruefen Sie das Ergebnis in:
+ +
+
+
+
+ + {/* Simulated Website Footer */} +
+
+ MusterShop GmbH — Simulierte Test-Website +
+ + Datenschutz + Impressum +
+
+
+ + {/* === REAL COOKIE BANNER === */} + {showBanner && ( + <> +
+
+
+ {/* Header */} +
+
+
+

Cookie-Einstellungen

+

+ Waehlen Sie, welche Cookie-Kategorien Sie zulassen moechten. +

+
+ {/* EWR Toggle */} +
+
+ + Nur EU/EWR + + +
+
+
+
+ + {/* Categories */} +
+ {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 ( +
+
+
+ {cat.label} + + {blocked > 0 ? `${cat.vendors.length - blocked} aktiv, ${blocked} blockiert` : `${cat.vendors.length} Verarbeiter`} + +
+
{cat.description}
+
+ +
+ ) + })} +
+ + {/* Buttons */} +
+
+ + +
+
+ +
+ Datenschutzerklaerung + Impressum +
+
+
+
+
+ + )} +
+ ) +} diff --git a/admin-compliance/components/sdk/Sidebar/SidebarModuleList.tsx b/admin-compliance/components/sdk/Sidebar/SidebarModuleList.tsx index e5545bd..a3aedfa 100644 --- a/admin-compliance/components/sdk/Sidebar/SidebarModuleList.tsx +++ b/admin-compliance/components/sdk/Sidebar/SidebarModuleList.tsx @@ -20,6 +20,23 @@ export function SidebarModuleList({ collapsed, projectId, pendingCRCount }: Side return ( <> + {/* CMP — Consent Management Platform */} +
+ {!collapsed && ( +
+ CMP +
+ )} + } label="Cookie-Banner" isActive={pathname?.startsWith('/sdk/cookie-banner') ?? false} collapsed={collapsed} projectId={projectId} /> + } label="Live-Vorschau" isActive={pathname === '/sdk/cookie-banner/preview'} collapsed={collapsed} projectId={projectId} /> + } label="Consent-Records" isActive={pathname?.startsWith('/sdk/einwilligungen') ?? false} collapsed={collapsed} projectId={projectId} /> + } label="Consent-Verwaltung" isActive={pathname === '/sdk/consent-management'} collapsed={collapsed} projectId={projectId} /> + } label="Vendor-Compliance" isActive={pathname?.startsWith('/sdk/vendor-compliance') ?? false} collapsed={collapsed} projectId={projectId} /> + } label="DSR Portal" isActive={pathname?.startsWith('/sdk/dsr') ?? false} collapsed={collapsed} projectId={projectId} /> + } label="Loeschfristen" isActive={pathname === '/sdk/loeschfristen'} collapsed={collapsed} projectId={projectId} /> + } label="E-Mail-Templates" isActive={pathname === '/sdk/email-templates'} collapsed={collapsed} projectId={projectId} /> +
+ {/* Maschinenrecht / CE */}
{!collapsed && (