Services: Admin-Compliance, Backend-Compliance, AI-Compliance-SDK, Consent-SDK, Developer-Portal, PCA-Platform, DSMS Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
322 lines
11 KiB
TypeScript
322 lines
11 KiB
TypeScript
'use client'
|
|
|
|
/**
|
|
* PrivacyPolicyPreview Component
|
|
*
|
|
* Zeigt eine Vorschau der generierten Datenschutzerklaerung.
|
|
*/
|
|
|
|
import { useState } from 'react'
|
|
import {
|
|
FileText,
|
|
Download,
|
|
Globe,
|
|
Eye,
|
|
Code,
|
|
ChevronDown,
|
|
ChevronRight,
|
|
Copy,
|
|
Check,
|
|
} from 'lucide-react'
|
|
import {
|
|
GeneratedPrivacyPolicy,
|
|
PrivacyPolicySection,
|
|
SupportedLanguage,
|
|
ExportFormat,
|
|
} from '@/lib/sdk/einwilligungen/types'
|
|
|
|
// =============================================================================
|
|
// TYPES
|
|
// =============================================================================
|
|
|
|
interface PrivacyPolicyPreviewProps {
|
|
policy: GeneratedPrivacyPolicy | null
|
|
isLoading?: boolean
|
|
language: SupportedLanguage
|
|
format: ExportFormat
|
|
onLanguageChange: (language: SupportedLanguage) => void
|
|
onFormatChange: (format: ExportFormat) => void
|
|
onGenerate: () => void
|
|
onDownload?: (format: ExportFormat) => void
|
|
}
|
|
|
|
// =============================================================================
|
|
// MAIN COMPONENT
|
|
// =============================================================================
|
|
|
|
export function PrivacyPolicyPreview({
|
|
policy,
|
|
isLoading = false,
|
|
language,
|
|
format,
|
|
onLanguageChange,
|
|
onFormatChange,
|
|
onGenerate,
|
|
onDownload,
|
|
}: PrivacyPolicyPreviewProps) {
|
|
const [viewMode, setViewMode] = useState<'preview' | 'source'>('preview')
|
|
const [expandedSections, setExpandedSections] = useState<Set<string>>(new Set())
|
|
const [copied, setCopied] = useState(false)
|
|
|
|
const toggleSection = (sectionId: string) => {
|
|
setExpandedSections((prev) => {
|
|
const next = new Set(prev)
|
|
if (next.has(sectionId)) {
|
|
next.delete(sectionId)
|
|
} else {
|
|
next.add(sectionId)
|
|
}
|
|
return next
|
|
})
|
|
}
|
|
|
|
const expandAll = () => {
|
|
if (policy) {
|
|
setExpandedSections(new Set(policy.sections.map((s) => s.id)))
|
|
}
|
|
}
|
|
|
|
const collapseAll = () => {
|
|
setExpandedSections(new Set())
|
|
}
|
|
|
|
const copyToClipboard = async () => {
|
|
if (policy?.content) {
|
|
await navigator.clipboard.writeText(policy.content)
|
|
setCopied(true)
|
|
setTimeout(() => setCopied(false), 2000)
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
{/* Controls */}
|
|
<div className="flex flex-wrap items-center justify-between gap-4 p-4 bg-slate-50 rounded-xl">
|
|
<div className="flex items-center gap-3">
|
|
{/* Language Selector */}
|
|
<div className="flex items-center gap-2">
|
|
<Globe className="w-4 h-4 text-slate-400" />
|
|
<select
|
|
value={language}
|
|
onChange={(e) => onLanguageChange(e.target.value as SupportedLanguage)}
|
|
className="px-3 py-1.5 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>
|
|
|
|
{/* Format Selector */}
|
|
<select
|
|
value={format}
|
|
onChange={(e) => onFormatChange(e.target.value as ExportFormat)}
|
|
className="px-3 py-1.5 border border-slate-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-indigo-500"
|
|
>
|
|
<option value="HTML">HTML</option>
|
|
<option value="MARKDOWN">Markdown</option>
|
|
<option value="PDF">PDF</option>
|
|
<option value="DOCX">Word (DOCX)</option>
|
|
</select>
|
|
|
|
{/* View Mode Toggle */}
|
|
<div className="flex items-center border border-slate-300 rounded-lg overflow-hidden">
|
|
<button
|
|
onClick={() => setViewMode('preview')}
|
|
className={`flex items-center gap-1.5 px-3 py-1.5 text-sm ${
|
|
viewMode === 'preview'
|
|
? 'bg-indigo-600 text-white'
|
|
: 'bg-white text-slate-600 hover:bg-slate-50'
|
|
}`}
|
|
>
|
|
<Eye className="w-4 h-4" />
|
|
Vorschau
|
|
</button>
|
|
<button
|
|
onClick={() => setViewMode('source')}
|
|
className={`flex items-center gap-1.5 px-3 py-1.5 text-sm ${
|
|
viewMode === 'source'
|
|
? 'bg-indigo-600 text-white'
|
|
: 'bg-white text-slate-600 hover:bg-slate-50'
|
|
}`}
|
|
>
|
|
<Code className="w-4 h-4" />
|
|
Quelltext
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-2">
|
|
<button
|
|
onClick={onGenerate}
|
|
disabled={isLoading}
|
|
className="flex items-center gap-2 px-4 py-2 bg-indigo-600 text-white rounded-lg text-sm font-medium hover:bg-indigo-700 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
>
|
|
{isLoading ? (
|
|
<>
|
|
<div className="w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin" />
|
|
Generiere...
|
|
</>
|
|
) : (
|
|
<>
|
|
<FileText className="w-4 h-4" />
|
|
Generieren
|
|
</>
|
|
)}
|
|
</button>
|
|
|
|
{policy && onDownload && (
|
|
<button
|
|
onClick={() => onDownload(format)}
|
|
className="flex items-center gap-2 px-4 py-2 border border-slate-300 rounded-lg text-sm font-medium text-slate-700 hover:bg-slate-50"
|
|
>
|
|
<Download className="w-4 h-4" />
|
|
Download
|
|
</button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Content */}
|
|
{policy ? (
|
|
<div className="border border-slate-200 rounded-xl overflow-hidden bg-white">
|
|
{/* Header */}
|
|
<div className="flex items-center justify-between px-4 py-3 bg-slate-50 border-b border-slate-200">
|
|
<div>
|
|
<h3 className="font-semibold text-slate-900">
|
|
{language === 'de' ? 'Datenschutzerklaerung' : 'Privacy Policy'}
|
|
</h3>
|
|
<p className="text-xs text-slate-500">
|
|
Version {policy.version} |{' '}
|
|
{new Date(policy.generatedAt).toLocaleDateString(
|
|
language === 'de' ? 'de-DE' : 'en-US'
|
|
)}
|
|
</p>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<button
|
|
onClick={expandAll}
|
|
className="text-xs text-indigo-600 hover:text-indigo-700"
|
|
>
|
|
Alle aufklappen
|
|
</button>
|
|
<span className="text-slate-300">|</span>
|
|
<button
|
|
onClick={collapseAll}
|
|
className="text-xs text-slate-600 hover:text-slate-700"
|
|
>
|
|
Alle zuklappen
|
|
</button>
|
|
{viewMode === 'source' && (
|
|
<>
|
|
<span className="text-slate-300">|</span>
|
|
<button
|
|
onClick={copyToClipboard}
|
|
className="flex items-center gap-1 text-xs text-slate-600 hover:text-slate-700"
|
|
>
|
|
{copied ? (
|
|
<>
|
|
<Check className="w-3 h-3 text-green-600" />
|
|
Kopiert
|
|
</>
|
|
) : (
|
|
<>
|
|
<Copy className="w-3 h-3" />
|
|
Kopieren
|
|
</>
|
|
)}
|
|
</button>
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Sections */}
|
|
{viewMode === 'preview' ? (
|
|
<div className="divide-y divide-slate-100">
|
|
{policy.sections.map((section) => {
|
|
const isExpanded = expandedSections.has(section.id)
|
|
return (
|
|
<div key={section.id}>
|
|
<button
|
|
onClick={() => toggleSection(section.id)}
|
|
className="w-full flex items-center justify-between px-4 py-3 hover:bg-slate-50 transition-colors"
|
|
>
|
|
<span className="font-medium text-slate-900">
|
|
{section.title[language]}
|
|
</span>
|
|
{isExpanded ? (
|
|
<ChevronDown className="w-5 h-5 text-slate-400" />
|
|
) : (
|
|
<ChevronRight className="w-5 h-5 text-slate-400" />
|
|
)}
|
|
</button>
|
|
{isExpanded && (
|
|
<div className="px-4 pb-4">
|
|
<div
|
|
className="prose prose-sm prose-slate max-w-none"
|
|
dangerouslySetInnerHTML={{
|
|
__html: formatContent(section.content[language]),
|
|
}}
|
|
/>
|
|
{section.isGenerated && (
|
|
<div className="mt-2 text-xs text-slate-400 flex items-center gap-1">
|
|
<span className="w-2 h-2 bg-green-400 rounded-full" />
|
|
Automatisch aus Datenpunkten generiert
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
})}
|
|
</div>
|
|
) : (
|
|
<div className="p-4">
|
|
<pre className="bg-slate-900 text-slate-100 p-4 rounded-lg overflow-x-auto text-sm font-mono whitespace-pre-wrap">
|
|
{policy.content}
|
|
</pre>
|
|
</div>
|
|
)}
|
|
</div>
|
|
) : (
|
|
<div className="border border-slate-200 rounded-xl p-12 text-center bg-white">
|
|
<FileText className="w-16 h-16 mx-auto mb-4 text-slate-300" />
|
|
<h3 className="font-semibold text-slate-900 mb-2">
|
|
Keine Datenschutzerklaerung generiert
|
|
</h3>
|
|
<p className="text-sm text-slate-500 mb-4">
|
|
Waehlen Sie die gewuenschten Datenpunkte aus und klicken Sie auf "Generieren", um eine
|
|
Datenschutzerklaerung zu erstellen.
|
|
</p>
|
|
<button
|
|
onClick={onGenerate}
|
|
disabled={isLoading}
|
|
className="inline-flex items-center gap-2 px-4 py-2 bg-indigo-600 text-white rounded-lg text-sm font-medium hover:bg-indigo-700"
|
|
>
|
|
<FileText className="w-4 h-4" />
|
|
Jetzt generieren
|
|
</button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
/**
|
|
* Formatiert Markdown-aehnlichen Content zu HTML
|
|
*/
|
|
function formatContent(content: string): string {
|
|
return content
|
|
.replace(/### (.+)/g, '<h4 class="font-semibold text-slate-800 mt-4 mb-2">$1</h4>')
|
|
.replace(/## (.+)/g, '<h3 class="font-semibold text-lg text-slate-900 mt-6 mb-3">$1</h3>')
|
|
.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
|
|
.replace(/\n\n/g, '</p><p class="mb-3">')
|
|
.replace(/\n- /g, '</p><ul class="list-disc pl-5 mb-3"><li>')
|
|
.replace(/<li>(.+?)(?=<li>|<\/p>|$)/g, '<li class="mb-1">$1</li>')
|
|
.replace(/(<li[^>]*>.*?<\/li>)+/g, '<ul class="list-disc pl-5 mb-3">$&</ul>')
|
|
.replace(/<\/ul><ul[^>]*>/g, '')
|
|
.replace(/\n/g, '<br>')
|
|
}
|
|
|
|
export default PrivacyPolicyPreview
|