fix: Move EWR toggle to banner header with info button
Build + Deploy / build-backend-compliance (push) Successful in 8s
CI / branch-name (push) Has been skipped
CI / guardrail-integrity (push) Has been skipped
CI / nodejs-build (push) Successful in 3m9s
CI / dep-audit (push) Has been skipped
CI / sbom-scan (push) Has been skipped
CI / test-go (push) Successful in 42s
CI / test-python-backend (push) Successful in 43s
CI / test-python-document-crawler (push) Successful in 29s
CI / test-python-dsms-gateway (push) Successful in 23s
Build + Deploy / build-admin-compliance (push) Successful in 2m9s
Build + Deploy / build-ai-sdk (push) Successful in 8s
Build + Deploy / build-developer-portal (push) Successful in 7s
Build + Deploy / build-tts (push) Successful in 11s
Build + Deploy / build-document-crawler (push) Successful in 7s
Build + Deploy / build-dsms-gateway (push) Successful in 7s
Build + Deploy / build-dsms-node (push) Successful in 13s
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
Build + Deploy / trigger-orca (push) Successful in 2m26s
CI / validate-canonical-controls (push) Successful in 14s

- EWR toggle now visible on initial banner view (top-right, always visible)
- Info button (i) with tooltip explaining EWR-only mode
- Blocked vendors count badge below toggle
- Blocked vendor pills shown below header text
- Removed duplicate EWR section from settings view

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-05-02 22:18:45 +02:00
parent 9bc816e55c
commit 9510ce0ff9
@@ -125,6 +125,8 @@ export function CookieBannerOverlay() {
<div className="max-w-3xl mx-auto m-4 bg-white rounded-2xl shadow-2xl border border-gray-200 overflow-hidden"> <div className="max-w-3xl mx-auto m-4 bg-white rounded-2xl shadow-2xl border border-gray-200 overflow-hidden">
{/* Header */} {/* Header */}
<div className="px-6 pt-6 pb-4"> <div className="px-6 pt-6 pb-4">
<div className="flex items-start justify-between gap-4">
<div>
<h2 className="text-lg font-semibold text-gray-900 flex items-center gap-2"> <h2 className="text-lg font-semibold text-gray-900 flex items-center gap-2">
<svg className="w-5 h-5 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg className="w-5 h-5 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
@@ -133,8 +135,31 @@ export function CookieBannerOverlay() {
</h2> </h2>
<p className="text-sm text-gray-600 mt-2 leading-relaxed"> <p className="text-sm text-gray-600 mt-2 leading-relaxed">
Wir verwenden Cookies und aehnliche Technologien, um Ihnen die bestmoegliche Erfahrung zu bieten. Wir verwenden Cookies und aehnliche Technologien, um Ihnen die bestmoegliche Erfahrung zu bieten.
Sie koennen Ihre Praeferenzen jederzeit aendern.
</p> </p>
</div>
{/* EWR-Only Toggle — always visible in header */}
<EWRToggle
checked={consent.ewrOnly}
onChange={() => setConsent(prev => ({ ...prev, ewrOnly: !prev.ewrOnly }))}
blockedCount={blockedVendors.length}
/>
</div>
{/* Blocked vendors pills */}
{consent.ewrOnly && blockedVendors.length > 0 && (
<div className="mt-3 flex flex-wrap gap-1">
{blockedVendors.map(name => (
<span key={name} className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full bg-red-50 text-red-600 text-[10px] font-medium border border-red-100">
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728A9 9 0 015.636 5.636m12.728 12.728L5.636 5.636" />
</svg>
{name}
</span>
))}
</div>
)}
<div className="flex items-center gap-4 mt-2 text-xs text-gray-500"> <div className="flex items-center gap-4 mt-2 text-xs text-gray-500">
<a href="/sdk/einwilligungen/cookie-banner" className="hover:text-purple-600 underline"> <a href="/sdk/einwilligungen/cookie-banner" className="hover:text-purple-600 underline">
Datenschutzerklaerung Datenschutzerklaerung
@@ -149,51 +174,6 @@ export function CookieBannerOverlay() {
{/* Settings */} {/* Settings */}
{showSettings && ( {showSettings && (
<div className="border-t border-gray-100"> <div className="border-t border-gray-100">
{/* === DRITTLAND-SCHUTZ === */}
<div className="px-6 py-4 bg-gradient-to-r from-blue-50 to-indigo-50 border-b border-blue-100">
<div className="flex items-start justify-between gap-4">
<div className="flex items-start gap-3">
<div className="w-8 h-8 rounded-lg bg-blue-100 flex items-center justify-center shrink-0 mt-0.5">
<svg className="w-5 h-5 text-blue-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3.055 11H5a2 2 0 012 2v1a2 2 0 002 2 2 2 0 012 2v2.945M8 3.935V5.5A2.5 2.5 0 0010.5 8h.5a2 2 0 012 2 2 2 0 104 0 2 2 0 012-2h1.064M15 20.488V18a2 2 0 012-2h3.064M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
<div>
<div className="text-sm font-semibold text-blue-900">
Nur EU/EWR-Anbieter
</div>
<p className="text-xs text-blue-700 mt-0.5 leading-relaxed">
Erlaubt nur Anbieter mit Sitz im Europaeischen Wirtschaftsraum (EWR) oder
der Schweiz. Anbieter ausserhalb werden blockiert auch wenn Sie einer
Kategorie zustimmen. {nonEWRCount} Anbieter betroffen.
</p>
{consent.ewrOnly && blockedVendors.length > 0 && (
<div className="mt-2 flex flex-wrap gap-1">
{blockedVendors.map(name => (
<span key={name} className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full bg-red-100 text-red-700 text-[10px] font-medium">
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728A9 9 0 015.636 5.636m12.728 12.728L5.636 5.636" />
</svg>
{name}
</span>
))}
</div>
)}
</div>
</div>
<button
onClick={() => setConsent(prev => ({ ...prev, ewrOnly: !prev.ewrOnly }))}
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors shrink-0 mt-1 ${
consent.ewrOnly ? 'bg-blue-600' : 'bg-gray-200'
} cursor-pointer`}
>
<span className={`inline-block h-4 w-4 transform rounded-full bg-white shadow-sm transition-transform ${
consent.ewrOnly ? 'translate-x-6' : 'translate-x-1'
}`} />
</button>
</div>
</div>
{/* Category Sections */} {/* Category Sections */}
<div className="px-6 py-4 space-y-1 max-h-[40vh] overflow-y-auto"> <div className="px-6 py-4 space-y-1 max-h-[40vh] overflow-y-auto">
{Object.entries(CATEGORY_VENDORS).map(([key, cat]) => ( {Object.entries(CATEGORY_VENDORS).map(([key, cat]) => (
@@ -430,3 +410,81 @@ function CategorySection({
</div> </div>
) )
} }
function EWRToggle({
checked,
onChange,
blockedCount,
}: {
checked: boolean
onChange: () => void
blockedCount: number
}) {
const [showInfo, setShowInfo] = useState(false)
return (
<div className="flex flex-col items-end gap-1.5 shrink-0">
{/* Toggle Row */}
<div className="flex items-center gap-2">
{/* Info Button */}
<button
onClick={() => setShowInfo(!showInfo)}
className="w-5 h-5 rounded-full bg-blue-100 text-blue-600 hover:bg-blue-200 flex items-center justify-center text-xs font-bold transition-colors"
aria-label="Info zu Nur EU/EWR"
>
i
</button>
<span className={`text-xs font-medium whitespace-nowrap ${checked ? 'text-blue-700' : 'text-gray-500'}`}>
Nur EU/EWR
</span>
<button
onClick={onChange}
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors shrink-0 cursor-pointer ${
checked ? 'bg-blue-600' : 'bg-gray-200'
}`}
>
<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>
{/* Blocked count badge */}
{checked && blockedCount > 0 && (
<span className="text-[10px] text-red-600 font-medium">
{blockedCount} Anbieter blockiert
</span>
)}
{/* Info Tooltip */}
{showInfo && (
<div className="absolute right-6 top-16 w-72 p-3 bg-blue-50 border border-blue-200 rounded-lg shadow-lg z-10 text-xs text-blue-800 leading-relaxed">
<div className="font-semibold mb-1 flex items-center gap-1.5">
<svg className="w-4 h-4 text-blue-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3.055 11H5a2 2 0 012 2v1a2 2 0 002 2 2 2 0 012 2v2.945M8 3.935V5.5A2.5 2.5 0 0010.5 8h.5a2 2 0 012 2 2 2 0 104 0 2 2 0 012-2h1.064M15 20.488V18a2 2 0 012-2h3.064M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
Nur EU/EWR-Anbieter
</div>
<p>
Erlaubt nur Datenverarbeitung durch Anbieter mit Sitz im Europaeischen
Wirtschaftsraum (EU + Island, Liechtenstein, Norwegen) oder der Schweiz.
</p>
<p className="mt-1.5">
Anbieter ausserhalb (z.B. USA) werden blockiert auch wenn Sie einer
Cookie-Kategorie zustimmen. So behalten Sie die volle Kontrolle ueber
internationale Datentransfers.
</p>
<button
onClick={() => setShowInfo(false)}
className="mt-2 text-blue-600 hover:text-blue-800 font-medium"
>
Verstanden
</button>
</div>
)}
</div>
)
}