All checks were successful
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Successful in 32s
CI / test-python-backend-compliance (push) Successful in 31s
CI / test-python-document-crawler (push) Successful in 21s
CI / test-python-dsms-gateway (push) Successful in 19s
Kaputtes (admin) Layout geloescht (Role-Selection, 404-Sidebar, localhost-Dashboard). SDK-Flow nach /sdk/sdk-flow verschoben. Route-Gruppe (sdk) aufgeloest. Root-Seite redirected auf /sdk. ~25 ungenutzte Dateien/Verzeichnisse entfernt. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
764 lines
26 KiB
TypeScript
764 lines
26 KiB
TypeScript
'use client'
|
|
|
|
/**
|
|
* Cookie Banner Configuration Page
|
|
*
|
|
* Konfiguriert den Cookie-Banner basierend auf dem Datenpunktkatalog.
|
|
*/
|
|
|
|
import { useState, useEffect, useMemo } from 'react'
|
|
import { useSDK } from '@/lib/sdk'
|
|
import { StepHeader, STEP_EXPLANATIONS } from '@/components/sdk/StepHeader'
|
|
import {
|
|
EinwilligungenProvider,
|
|
useEinwilligungen,
|
|
} from '@/lib/sdk/einwilligungen/context'
|
|
import {
|
|
generateCookieBannerConfig,
|
|
generateEmbedCode,
|
|
DEFAULT_COOKIE_BANNER_TEXTS,
|
|
DEFAULT_COOKIE_BANNER_STYLING,
|
|
} from '@/lib/sdk/einwilligungen/generator/cookie-banner'
|
|
import {
|
|
CookieBannerConfig,
|
|
CookieBannerStyling,
|
|
CookieBannerTexts,
|
|
SupportedLanguage,
|
|
} from '@/lib/sdk/einwilligungen/types'
|
|
import {
|
|
Cookie,
|
|
Settings,
|
|
Palette,
|
|
Code,
|
|
Copy,
|
|
Check,
|
|
Eye,
|
|
ArrowLeft,
|
|
Monitor,
|
|
Smartphone,
|
|
Tablet,
|
|
ChevronDown,
|
|
ChevronRight,
|
|
ExternalLink,
|
|
} from 'lucide-react'
|
|
import Link from 'next/link'
|
|
|
|
// =============================================================================
|
|
// STYLING FORM
|
|
// =============================================================================
|
|
|
|
interface StylingFormProps {
|
|
styling: CookieBannerStyling
|
|
onChange: (styling: CookieBannerStyling) => void
|
|
}
|
|
|
|
function StylingForm({ styling, onChange }: StylingFormProps) {
|
|
const handleChange = (field: keyof CookieBannerStyling, value: string | number) => {
|
|
onChange({ ...styling, [field]: value })
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
{/* Position */}
|
|
<div>
|
|
<label className="block text-sm font-medium text-slate-700 mb-2">
|
|
Position
|
|
</label>
|
|
<div className="grid grid-cols-3 gap-2">
|
|
{(['BOTTOM', 'TOP', 'CENTER'] as const).map((pos) => (
|
|
<button
|
|
key={pos}
|
|
onClick={() => handleChange('position', pos)}
|
|
className={`px-4 py-2 text-sm rounded-lg border transition-colors ${
|
|
styling.position === pos
|
|
? 'bg-indigo-50 border-indigo-300 text-indigo-700'
|
|
: 'bg-white border-slate-200 text-slate-600 hover:bg-slate-50'
|
|
}`}
|
|
>
|
|
{pos === 'BOTTOM' ? 'Unten' : pos === 'TOP' ? 'Oben' : 'Zentriert'}
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Theme */}
|
|
<div>
|
|
<label className="block text-sm font-medium text-slate-700 mb-2">
|
|
Theme
|
|
</label>
|
|
<div className="grid grid-cols-2 gap-2">
|
|
{(['LIGHT', 'DARK'] as const).map((theme) => (
|
|
<button
|
|
key={theme}
|
|
onClick={() => handleChange('theme', theme)}
|
|
className={`px-4 py-2 text-sm rounded-lg border transition-colors ${
|
|
styling.theme === theme
|
|
? 'bg-indigo-50 border-indigo-300 text-indigo-700'
|
|
: 'bg-white border-slate-200 text-slate-600 hover:bg-slate-50'
|
|
}`}
|
|
>
|
|
{theme === 'LIGHT' ? 'Hell' : 'Dunkel'}
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Colors */}
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<label className="block text-sm font-medium text-slate-700 mb-1">
|
|
Primaerfarbe
|
|
</label>
|
|
<div className="flex items-center gap-2">
|
|
<input
|
|
type="color"
|
|
value={styling.primaryColor}
|
|
onChange={(e) => handleChange('primaryColor', e.target.value)}
|
|
className="w-10 h-10 rounded border border-slate-200 cursor-pointer"
|
|
/>
|
|
<input
|
|
type="text"
|
|
value={styling.primaryColor}
|
|
onChange={(e) => handleChange('primaryColor', e.target.value)}
|
|
className="flex-1 px-3 py-2 border border-slate-300 rounded-lg text-sm"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-slate-700 mb-1">
|
|
Sekundaerfarbe
|
|
</label>
|
|
<div className="flex items-center gap-2">
|
|
<input
|
|
type="color"
|
|
value={styling.secondaryColor || '#f1f5f9'}
|
|
onChange={(e) => handleChange('secondaryColor', e.target.value)}
|
|
className="w-10 h-10 rounded border border-slate-200 cursor-pointer"
|
|
/>
|
|
<input
|
|
type="text"
|
|
value={styling.secondaryColor || '#f1f5f9'}
|
|
onChange={(e) => handleChange('secondaryColor', e.target.value)}
|
|
className="flex-1 px-3 py-2 border border-slate-300 rounded-lg text-sm"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Border Radius & Max Width */}
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<label className="block text-sm font-medium text-slate-700 mb-1">
|
|
Eckenradius (px)
|
|
</label>
|
|
<input
|
|
type="number"
|
|
min={0}
|
|
max={32}
|
|
value={styling.borderRadius}
|
|
onChange={(e) => handleChange('borderRadius', parseInt(e.target.value))}
|
|
className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-slate-700 mb-1">
|
|
Max. Breite (px)
|
|
</label>
|
|
<input
|
|
type="number"
|
|
min={320}
|
|
max={800}
|
|
value={styling.maxWidth}
|
|
onChange={(e) => handleChange('maxWidth', parseInt(e.target.value))}
|
|
className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
// =============================================================================
|
|
// TEXTS FORM
|
|
// =============================================================================
|
|
|
|
interface TextsFormProps {
|
|
texts: CookieBannerTexts
|
|
language: SupportedLanguage
|
|
onChange: (texts: CookieBannerTexts) => void
|
|
}
|
|
|
|
function TextsForm({ texts, language, onChange }: TextsFormProps) {
|
|
const handleChange = (field: keyof CookieBannerTexts, value: string) => {
|
|
onChange({
|
|
...texts,
|
|
[field]: { ...texts[field], [language]: value },
|
|
})
|
|
}
|
|
|
|
const fields: { key: keyof CookieBannerTexts; label: string; multiline?: boolean }[] = [
|
|
{ key: 'title', label: 'Titel' },
|
|
{ key: 'description', label: 'Beschreibung', multiline: true },
|
|
{ key: 'acceptAll', label: 'Alle akzeptieren Button' },
|
|
{ key: 'rejectAll', label: 'Nur notwendige Button' },
|
|
{ key: 'customize', label: 'Einstellungen Button' },
|
|
{ key: 'save', label: 'Speichern Button' },
|
|
{ key: 'privacyPolicyLink', label: 'Datenschutz-Link Text' },
|
|
]
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
{fields.map(({ key, label, multiline }) => (
|
|
<div key={key}>
|
|
<label className="block text-sm font-medium text-slate-700 mb-1">
|
|
{label}
|
|
</label>
|
|
{multiline ? (
|
|
<textarea
|
|
value={texts[key][language]}
|
|
onChange={(e) => handleChange(key, e.target.value)}
|
|
rows={3}
|
|
className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-indigo-500"
|
|
/>
|
|
) : (
|
|
<input
|
|
type="text"
|
|
value={texts[key][language]}
|
|
onChange={(e) => handleChange(key, e.target.value)}
|
|
className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-indigo-500"
|
|
/>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
// =============================================================================
|
|
// BANNER PREVIEW
|
|
// =============================================================================
|
|
|
|
interface BannerPreviewProps {
|
|
config: CookieBannerConfig | null
|
|
language: SupportedLanguage
|
|
device: 'desktop' | 'tablet' | 'mobile'
|
|
}
|
|
|
|
function BannerPreview({ config, language, device }: BannerPreviewProps) {
|
|
const [showDetails, setShowDetails] = useState(false)
|
|
|
|
if (!config) {
|
|
return (
|
|
<div className="flex items-center justify-center h-64 bg-slate-100 rounded-xl">
|
|
<p className="text-slate-400">Konfiguration wird geladen...</p>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
const isDark = config.styling.theme === 'DARK'
|
|
const bgColor = isDark ? '#1e293b' : config.styling.backgroundColor || '#ffffff'
|
|
const textColor = isDark ? '#f1f5f9' : config.styling.textColor || '#1e293b'
|
|
|
|
const deviceWidths = {
|
|
desktop: '100%',
|
|
tablet: '768px',
|
|
mobile: '375px',
|
|
}
|
|
|
|
return (
|
|
<div
|
|
className="border rounded-xl overflow-hidden"
|
|
style={{
|
|
maxWidth: deviceWidths[device],
|
|
margin: '0 auto',
|
|
}}
|
|
>
|
|
{/* Simulated Browser */}
|
|
<div className="bg-slate-100 h-8 flex items-center px-3 gap-2">
|
|
<div className="w-3 h-3 rounded-full bg-red-400" />
|
|
<div className="w-3 h-3 rounded-full bg-yellow-400" />
|
|
<div className="w-3 h-3 rounded-full bg-green-400" />
|
|
<div className="flex-1 bg-white rounded h-5 mx-4" />
|
|
</div>
|
|
|
|
{/* Page Content */}
|
|
<div className="relative bg-slate-50 min-h-[400px]">
|
|
{/* Placeholder Content */}
|
|
<div className="p-6 space-y-4">
|
|
<div className="h-4 bg-slate-200 rounded w-3/4" />
|
|
<div className="h-4 bg-slate-200 rounded w-1/2" />
|
|
<div className="h-32 bg-slate-200 rounded" />
|
|
<div className="h-4 bg-slate-200 rounded w-2/3" />
|
|
<div className="h-4 bg-slate-200 rounded w-1/2" />
|
|
</div>
|
|
|
|
{/* Overlay */}
|
|
<div className="absolute inset-0 bg-black/40" />
|
|
|
|
{/* Cookie Banner */}
|
|
<div
|
|
className={`absolute ${
|
|
config.styling.position === 'TOP'
|
|
? 'top-0 left-0 right-0'
|
|
: config.styling.position === 'CENTER'
|
|
? 'top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2'
|
|
: 'bottom-0 left-0 right-0'
|
|
}`}
|
|
style={{
|
|
maxWidth: config.styling.maxWidth,
|
|
margin: config.styling.position === 'CENTER' ? '0' : '16px auto',
|
|
}}
|
|
>
|
|
<div
|
|
className="shadow-xl"
|
|
style={{
|
|
background: bgColor,
|
|
color: textColor,
|
|
borderRadius: config.styling.borderRadius,
|
|
padding: '20px',
|
|
}}
|
|
>
|
|
<h3 className="font-semibold text-lg mb-2">
|
|
{config.texts.title[language]}
|
|
</h3>
|
|
<p className="text-sm opacity-80 mb-4">
|
|
{config.texts.description[language]}
|
|
</p>
|
|
|
|
<div className="flex flex-wrap gap-2 mb-3">
|
|
<button
|
|
style={{ background: config.styling.secondaryColor }}
|
|
className="flex-1 min-w-[100px] px-4 py-2 rounded-lg text-sm font-medium"
|
|
>
|
|
{config.texts.rejectAll[language]}
|
|
</button>
|
|
<button
|
|
onClick={() => setShowDetails(!showDetails)}
|
|
style={{ background: config.styling.secondaryColor }}
|
|
className="flex-1 min-w-[100px] px-4 py-2 rounded-lg text-sm font-medium"
|
|
>
|
|
{config.texts.customize[language]}
|
|
</button>
|
|
<button
|
|
style={{ background: config.styling.primaryColor, color: 'white' }}
|
|
className="flex-1 min-w-[100px] px-4 py-2 rounded-lg text-sm font-medium"
|
|
>
|
|
{config.texts.acceptAll[language]}
|
|
</button>
|
|
</div>
|
|
|
|
{showDetails && (
|
|
<div className="border-t pt-3 mt-3 space-y-2" style={{ borderColor: 'rgba(128,128,128,0.2)' }}>
|
|
{config.categories.map((cat) => (
|
|
<div key={cat.id} className="flex items-center justify-between py-2">
|
|
<div>
|
|
<div className="font-medium text-sm">{cat.name[language]}</div>
|
|
<div className="text-xs opacity-60">{cat.description[language]}</div>
|
|
</div>
|
|
<div
|
|
className={`w-10 h-6 rounded-full relative ${
|
|
cat.isRequired || cat.defaultEnabled
|
|
? ''
|
|
: 'opacity-50'
|
|
}`}
|
|
style={{
|
|
background: cat.isRequired || cat.defaultEnabled
|
|
? config.styling.primaryColor
|
|
: 'rgba(128,128,128,0.3)',
|
|
}}
|
|
>
|
|
<div
|
|
className="absolute top-1 w-4 h-4 bg-white rounded-full transition-all"
|
|
style={{
|
|
left: cat.isRequired || cat.defaultEnabled ? '20px' : '4px',
|
|
}}
|
|
/>
|
|
</div>
|
|
</div>
|
|
))}
|
|
<button
|
|
style={{ background: config.styling.primaryColor, color: 'white' }}
|
|
className="w-full px-4 py-2 rounded-lg text-sm font-medium mt-2"
|
|
>
|
|
{config.texts.save[language]}
|
|
</button>
|
|
</div>
|
|
)}
|
|
|
|
<a
|
|
href="#"
|
|
className="block text-xs mt-3"
|
|
style={{ color: config.styling.primaryColor }}
|
|
>
|
|
{config.texts.privacyPolicyLink[language]}
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
// =============================================================================
|
|
// EMBED CODE VIEWER
|
|
// =============================================================================
|
|
|
|
interface EmbedCodeViewerProps {
|
|
config: CookieBannerConfig | null
|
|
}
|
|
|
|
function EmbedCodeViewer({ config }: EmbedCodeViewerProps) {
|
|
const [activeTab, setActiveTab] = useState<'script' | 'html' | 'css' | 'js'>('script')
|
|
const [copied, setCopied] = useState(false)
|
|
|
|
const embedCode = useMemo(() => {
|
|
if (!config) return null
|
|
return generateEmbedCode(config, '/datenschutz')
|
|
}, [config])
|
|
|
|
const copyToClipboard = async (text: string) => {
|
|
await navigator.clipboard.writeText(text)
|
|
setCopied(true)
|
|
setTimeout(() => setCopied(false), 2000)
|
|
}
|
|
|
|
if (!embedCode) {
|
|
return (
|
|
<div className="flex items-center justify-center h-48 bg-slate-100 rounded-xl">
|
|
<p className="text-slate-400">Embed-Code wird generiert...</p>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
const tabs = [
|
|
{ id: 'script', label: 'Script-Tag', content: embedCode.scriptTag },
|
|
{ id: 'html', label: 'HTML', content: embedCode.html },
|
|
{ id: 'css', label: 'CSS', content: embedCode.css },
|
|
{ id: 'js', label: 'JavaScript', content: embedCode.js },
|
|
] as const
|
|
|
|
const currentContent = tabs.find((t) => t.id === activeTab)?.content || ''
|
|
|
|
return (
|
|
<div className="border border-slate-200 rounded-xl overflow-hidden">
|
|
{/* Tabs */}
|
|
<div className="flex border-b border-slate-200 bg-slate-50">
|
|
{tabs.map((tab) => (
|
|
<button
|
|
key={tab.id}
|
|
onClick={() => setActiveTab(tab.id)}
|
|
className={`px-4 py-2 text-sm font-medium transition-colors ${
|
|
activeTab === tab.id
|
|
? 'bg-white text-indigo-600 border-b-2 border-indigo-600 -mb-px'
|
|
: 'text-slate-600 hover:text-slate-900'
|
|
}`}
|
|
>
|
|
{tab.label}
|
|
</button>
|
|
))}
|
|
</div>
|
|
|
|
{/* Code */}
|
|
<div className="relative">
|
|
<pre className="p-4 bg-slate-900 text-slate-100 text-sm font-mono overflow-x-auto max-h-[400px]">
|
|
{currentContent}
|
|
</pre>
|
|
<button
|
|
onClick={() => copyToClipboard(currentContent)}
|
|
className="absolute top-3 right-3 flex items-center gap-1.5 px-3 py-1.5 bg-slate-800 hover:bg-slate-700 text-slate-200 rounded-lg text-xs"
|
|
>
|
|
{copied ? (
|
|
<>
|
|
<Check className="w-3.5 h-3.5 text-green-400" />
|
|
Kopiert
|
|
</>
|
|
) : (
|
|
<>
|
|
<Copy className="w-3.5 h-3.5" />
|
|
Kopieren
|
|
</>
|
|
)}
|
|
</button>
|
|
</div>
|
|
|
|
{/* Integration Instructions */}
|
|
{activeTab === 'script' && (
|
|
<div className="p-4 bg-amber-50 border-t border-amber-200">
|
|
<p className="text-sm text-amber-800">
|
|
<strong>Integration:</strong> Fuegen Sie den Script-Tag in den{' '}
|
|
<code className="bg-amber-100 px-1 rounded"><head></code> oder vor dem
|
|
schliessenden{' '}
|
|
<code className="bg-amber-100 px-1 rounded"></body></code>-Tag ein.
|
|
</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
// =============================================================================
|
|
// CATEGORY LIST
|
|
// =============================================================================
|
|
|
|
interface CategoryListProps {
|
|
config: CookieBannerConfig | null
|
|
language: SupportedLanguage
|
|
}
|
|
|
|
function CategoryList({ config, language }: CategoryListProps) {
|
|
const [expandedCategories, setExpandedCategories] = useState<Set<string>>(new Set())
|
|
|
|
if (!config) return null
|
|
|
|
const toggleCategory = (id: string) => {
|
|
setExpandedCategories((prev) => {
|
|
const next = new Set(prev)
|
|
if (next.has(id)) {
|
|
next.delete(id)
|
|
} else {
|
|
next.add(id)
|
|
}
|
|
return next
|
|
})
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-2">
|
|
{config.categories.map((cat) => {
|
|
const isExpanded = expandedCategories.has(cat.id)
|
|
return (
|
|
<div key={cat.id} className="border border-slate-200 rounded-lg overflow-hidden">
|
|
<button
|
|
onClick={() => toggleCategory(cat.id)}
|
|
className="w-full flex items-center justify-between p-4 hover:bg-slate-50 transition-colors"
|
|
>
|
|
<div className="flex items-center gap-3">
|
|
<div
|
|
className={`w-3 h-3 rounded-full ${
|
|
cat.isRequired ? 'bg-green-500' : 'bg-amber-500'
|
|
}`}
|
|
/>
|
|
<div className="text-left">
|
|
<div className="font-medium text-slate-900">{cat.name[language]}</div>
|
|
<div className="text-sm text-slate-500">
|
|
{cat.cookies.length} Cookie(s) | {cat.dataPointIds.length} Datenpunkt(e)
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
{cat.isRequired && (
|
|
<span className="px-2 py-0.5 text-xs bg-green-100 text-green-700 rounded-full">
|
|
Erforderlich
|
|
</span>
|
|
)}
|
|
{isExpanded ? (
|
|
<ChevronDown className="w-5 h-5 text-slate-400" />
|
|
) : (
|
|
<ChevronRight className="w-5 h-5 text-slate-400" />
|
|
)}
|
|
</div>
|
|
</button>
|
|
|
|
{isExpanded && (
|
|
<div className="px-4 pb-4 border-t border-slate-100 bg-slate-50">
|
|
<p className="text-sm text-slate-600 py-3">{cat.description[language]}</p>
|
|
|
|
{cat.cookies.length > 0 && (
|
|
<div className="space-y-2">
|
|
<h4 className="text-xs font-semibold text-slate-500 uppercase">Cookies</h4>
|
|
<div className="space-y-1">
|
|
{cat.cookies.map((cookie, idx) => (
|
|
<div
|
|
key={idx}
|
|
className="flex items-center justify-between p-2 bg-white rounded border border-slate-200"
|
|
>
|
|
<div>
|
|
<span className="font-mono text-sm text-slate-700">{cookie.name}</span>
|
|
<span className="text-xs text-slate-400 ml-2">({cookie.provider})</span>
|
|
</div>
|
|
<span className="text-xs text-slate-500">{cookie.expiry}</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
})}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
// =============================================================================
|
|
// MAIN CONTENT
|
|
// =============================================================================
|
|
|
|
function CookieBannerContent() {
|
|
const { state } = useSDK()
|
|
const { allDataPoints } = useEinwilligungen()
|
|
|
|
const [styling, setStyling] = useState<CookieBannerStyling>(DEFAULT_COOKIE_BANNER_STYLING)
|
|
const [texts, setTexts] = useState<CookieBannerTexts>(DEFAULT_COOKIE_BANNER_TEXTS)
|
|
const [language, setLanguage] = useState<SupportedLanguage>('de')
|
|
const [activeTab, setActiveTab] = useState<'styling' | 'texts' | 'embed' | 'categories'>('styling')
|
|
const [device, setDevice] = useState<'desktop' | 'tablet' | 'mobile'>('desktop')
|
|
|
|
const config = useMemo(() => {
|
|
return generateCookieBannerConfig(
|
|
state.tenantId || 'demo',
|
|
allDataPoints,
|
|
texts,
|
|
styling
|
|
)
|
|
}, [state.tenantId, allDataPoints, texts, styling])
|
|
|
|
const cookieDataPoints = useMemo(
|
|
() => allDataPoints.filter((dp) => dp.cookieCategory !== null),
|
|
[allDataPoints]
|
|
)
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
{/* Back Link */}
|
|
<Link
|
|
href="/sdk/einwilligungen/catalog"
|
|
className="inline-flex items-center gap-2 text-sm text-slate-600 hover:text-slate-900"
|
|
>
|
|
<ArrowLeft className="w-4 h-4" />
|
|
Zurueck zum Katalog
|
|
</Link>
|
|
|
|
{/* Header */}
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<h1 className="text-2xl font-bold text-slate-900">Cookie-Banner Konfiguration</h1>
|
|
<p className="text-slate-600 mt-1">
|
|
Konfigurieren Sie Ihren DSGVO-konformen Cookie-Banner.
|
|
</p>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<select
|
|
value={language}
|
|
onChange={(e) => setLanguage(e.target.value as SupportedLanguage)}
|
|
className="px-3 py-2 border border-slate-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-indigo-500"
|
|
>
|
|
<option value="de">Deutsch</option>
|
|
<option value="en">English</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Stats */}
|
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
|
<div className="bg-white rounded-xl border border-slate-200 p-4">
|
|
<div className="text-sm text-slate-500">Kategorien</div>
|
|
<div className="text-2xl font-bold text-slate-900">{config?.categories.length || 0}</div>
|
|
</div>
|
|
<div className="bg-white rounded-xl border border-slate-200 p-4">
|
|
<div className="text-sm text-slate-500">Cookie-Datenpunkte</div>
|
|
<div className="text-2xl font-bold text-indigo-600">{cookieDataPoints.length}</div>
|
|
</div>
|
|
<div className="bg-white rounded-xl border border-green-200 p-4">
|
|
<div className="text-sm text-green-600">Erforderlich</div>
|
|
<div className="text-2xl font-bold text-green-600">
|
|
{config?.categories.filter((c) => c.isRequired).length || 0}
|
|
</div>
|
|
</div>
|
|
<div className="bg-white rounded-xl border border-amber-200 p-4">
|
|
<div className="text-sm text-amber-600">Optional</div>
|
|
<div className="text-2xl font-bold text-amber-600">
|
|
{config?.categories.filter((c) => !c.isRequired).length || 0}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Two Column Layout */}
|
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
{/* Left: Configuration */}
|
|
<div className="space-y-4">
|
|
{/* Tabs */}
|
|
<div className="flex border-b border-slate-200">
|
|
{[
|
|
{ id: 'styling', label: 'Design', icon: Palette },
|
|
{ id: 'texts', label: 'Texte', icon: Settings },
|
|
{ id: 'categories', label: 'Kategorien', icon: Cookie },
|
|
{ id: 'embed', label: 'Embed-Code', icon: Code },
|
|
].map(({ id, label, icon: Icon }) => (
|
|
<button
|
|
key={id}
|
|
onClick={() => setActiveTab(id as typeof activeTab)}
|
|
className={`flex items-center gap-2 px-4 py-3 text-sm font-medium border-b-2 transition-colors ${
|
|
activeTab === id
|
|
? 'text-indigo-600 border-indigo-600'
|
|
: 'text-slate-600 border-transparent hover:text-slate-900'
|
|
}`}
|
|
>
|
|
<Icon className="w-4 h-4" />
|
|
{label}
|
|
</button>
|
|
))}
|
|
</div>
|
|
|
|
{/* Tab Content */}
|
|
<div className="bg-white rounded-xl border border-slate-200 p-6">
|
|
{activeTab === 'styling' && (
|
|
<StylingForm styling={styling} onChange={setStyling} />
|
|
)}
|
|
{activeTab === 'texts' && (
|
|
<TextsForm texts={texts} language={language} onChange={setTexts} />
|
|
)}
|
|
{activeTab === 'categories' && (
|
|
<CategoryList config={config} language={language} />
|
|
)}
|
|
{activeTab === 'embed' && <EmbedCodeViewer config={config} />}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Right: Preview */}
|
|
<div className="space-y-4">
|
|
{/* Device Selector */}
|
|
<div className="flex items-center justify-between">
|
|
<h3 className="font-semibold text-slate-900">Vorschau</h3>
|
|
<div className="flex items-center border border-slate-200 rounded-lg overflow-hidden">
|
|
{[
|
|
{ id: 'desktop', icon: Monitor },
|
|
{ id: 'tablet', icon: Tablet },
|
|
{ id: 'mobile', icon: Smartphone },
|
|
].map(({ id, icon: Icon }) => (
|
|
<button
|
|
key={id}
|
|
onClick={() => setDevice(id as typeof device)}
|
|
className={`p-2 ${
|
|
device === id
|
|
? 'bg-indigo-50 text-indigo-600'
|
|
: 'text-slate-400 hover:text-slate-600'
|
|
}`}
|
|
>
|
|
<Icon className="w-5 h-5" />
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Preview */}
|
|
<BannerPreview config={config} language={language} device={device} />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
// =============================================================================
|
|
// MAIN PAGE
|
|
// =============================================================================
|
|
|
|
export default function CookieBannerPage() {
|
|
return (
|
|
<EinwilligungenProvider>
|
|
<CookieBannerContent />
|
|
</EinwilligungenProvider>
|
|
)
|
|
}
|