fix(admin-v2): Restore complete admin-v2 application
The admin-v2 application was incomplete in the repository. This commit restores all missing components: - Admin pages (76 pages): dashboard, ai, compliance, dsgvo, education, infrastructure, communication, development, onboarding, rbac - SDK pages (45 pages): tom, dsfa, vvt, loeschfristen, einwilligungen, vendor-compliance, tom-generator, dsr, and more - Developer portal (25 pages): API docs, SDK guides, frameworks - All components, lib files, hooks, and types - Updated package.json with all dependencies The issue was caused by incomplete initial repository state - the full admin-v2 codebase existed in backend/admin-v2 and docs-src/admin-v2 but was never fully synced to the main admin-v2 directory. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
321
admin-v2/components/sdk/einwilligungen/PrivacyPolicyPreview.tsx
Normal file
321
admin-v2/components/sdk/einwilligungen/PrivacyPolicyPreview.tsx
Normal file
@@ -0,0 +1,321 @@
|
||||
'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
|
||||
Reference in New Issue
Block a user