refactor(admin-v2): Consolidate compliance/DSGVO pages into SDK pipeline
Remove duplicate compliance and DSGVO admin pages that have been superseded by the unified SDK pipeline. Update navigation, sidebar, roles, and module registry to reflect the new structure. Add DSFA corpus API proxy and source-policy components. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,769 +0,0 @@
|
||||
'use client'
|
||||
|
||||
/**
|
||||
* Admin Panel for Website Content
|
||||
*
|
||||
* Allows editing all website texts:
|
||||
* - Hero Section
|
||||
* - Features
|
||||
* - FAQ
|
||||
* - Pricing
|
||||
* - Trust Indicators
|
||||
* - Testimonial
|
||||
*
|
||||
* Includes Live-Preview of website
|
||||
*/
|
||||
|
||||
import { useState, useEffect, useRef, useCallback } from 'react'
|
||||
import { WebsiteContent, HeroContent, FeatureContent } from '@/lib/content-types'
|
||||
|
||||
// Admin Key (in production via login)
|
||||
const ADMIN_KEY = 'breakpilot-admin-2024'
|
||||
|
||||
// Mapping tabs to website sections
|
||||
const SECTION_MAP: Record<string, { selector: string; scrollTo: string }> = {
|
||||
hero: { selector: '#hero', scrollTo: 'hero' },
|
||||
features: { selector: '#features', scrollTo: 'features' },
|
||||
faq: { selector: '#faq', scrollTo: 'faq' },
|
||||
pricing: { selector: '#pricing', scrollTo: 'pricing' },
|
||||
other: { selector: '#trust', scrollTo: 'trust' },
|
||||
}
|
||||
|
||||
export default function ContentPage() {
|
||||
const [content, setContent] = useState<WebsiteContent | null>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [saving, setSaving] = useState(false)
|
||||
const [message, setMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null)
|
||||
const [activeTab, setActiveTab] = useState<'hero' | 'features' | 'faq' | 'pricing' | 'other'>('hero')
|
||||
const [showPreview, setShowPreview] = useState(true)
|
||||
const iframeRef = useRef<HTMLIFrameElement>(null)
|
||||
|
||||
// Scroll preview to section
|
||||
const scrollToSection = useCallback((tab: string) => {
|
||||
if (!iframeRef.current?.contentWindow) return
|
||||
const section = SECTION_MAP[tab]
|
||||
if (section) {
|
||||
try {
|
||||
iframeRef.current.contentWindow.postMessage(
|
||||
{ type: 'scrollTo', section: section.scrollTo },
|
||||
'*'
|
||||
)
|
||||
} catch {
|
||||
// Same-origin policy - fallback
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
// Scroll to section on tab change
|
||||
useEffect(() => {
|
||||
scrollToSection(activeTab)
|
||||
}, [activeTab, scrollToSection])
|
||||
|
||||
// Load content
|
||||
useEffect(() => {
|
||||
loadContent()
|
||||
}, [])
|
||||
|
||||
async function loadContent() {
|
||||
try {
|
||||
const res = await fetch('/api/development/content')
|
||||
if (res.ok) {
|
||||
const data = await res.json()
|
||||
setContent(data)
|
||||
} else {
|
||||
setMessage({ type: 'error', text: 'Fehler beim Laden' })
|
||||
}
|
||||
} catch {
|
||||
setMessage({ type: 'error', text: 'Fehler beim Laden' })
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
async function saveChanges() {
|
||||
if (!content) return
|
||||
|
||||
setSaving(true)
|
||||
setMessage(null)
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/development/content', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'x-admin-key': ADMIN_KEY,
|
||||
},
|
||||
body: JSON.stringify(content),
|
||||
})
|
||||
|
||||
if (res.ok) {
|
||||
setMessage({ type: 'success', text: 'Gespeichert!' })
|
||||
} else {
|
||||
const error = await res.json()
|
||||
setMessage({ type: 'error', text: error.error || 'Fehler beim Speichern' })
|
||||
}
|
||||
} catch {
|
||||
setMessage({ type: 'error', text: 'Fehler beim Speichern' })
|
||||
} finally {
|
||||
setSaving(false)
|
||||
}
|
||||
}
|
||||
|
||||
// Hero Section update
|
||||
function updateHero(field: keyof HeroContent, value: string) {
|
||||
if (!content) return
|
||||
setContent({
|
||||
...content,
|
||||
hero: { ...content.hero, [field]: value },
|
||||
})
|
||||
}
|
||||
|
||||
// Feature update
|
||||
function updateFeature(index: number, field: keyof FeatureContent, value: string) {
|
||||
if (!content) return
|
||||
const newFeatures = [...content.features]
|
||||
newFeatures[index] = { ...newFeatures[index], [field]: value }
|
||||
setContent({ ...content, features: newFeatures })
|
||||
}
|
||||
|
||||
// FAQ update
|
||||
function updateFAQ(index: number, field: 'question' | 'answer', value: string | string[]) {
|
||||
if (!content) return
|
||||
const newFAQ = [...content.faq]
|
||||
if (field === 'answer' && typeof value === 'string') {
|
||||
newFAQ[index] = { ...newFAQ[index], answer: value.split('\n') }
|
||||
} else if (field === 'question' && typeof value === 'string') {
|
||||
newFAQ[index] = { ...newFAQ[index], question: value }
|
||||
}
|
||||
setContent({ ...content, faq: newFAQ })
|
||||
}
|
||||
|
||||
// Add FAQ
|
||||
function addFAQ() {
|
||||
if (!content) return
|
||||
setContent({
|
||||
...content,
|
||||
faq: [...content.faq, { question: 'Neue Frage?', answer: ['Antwort hier...'] }],
|
||||
})
|
||||
}
|
||||
|
||||
// Remove FAQ
|
||||
function removeFAQ(index: number) {
|
||||
if (!content) return
|
||||
const newFAQ = content.faq.filter((_, i) => i !== index)
|
||||
setContent({ ...content, faq: newFAQ })
|
||||
}
|
||||
|
||||
// Pricing update
|
||||
function updatePricing(index: number, field: string, value: string | number | boolean) {
|
||||
if (!content) return
|
||||
const newPricing = [...content.pricing]
|
||||
if (field === 'price') {
|
||||
newPricing[index] = { ...newPricing[index], price: Number(value) }
|
||||
} else if (field === 'popular') {
|
||||
newPricing[index] = { ...newPricing[index], popular: Boolean(value) }
|
||||
} else if (field.startsWith('features.')) {
|
||||
const subField = field.replace('features.', '')
|
||||
if (subField === 'included' && typeof value === 'string') {
|
||||
newPricing[index] = {
|
||||
...newPricing[index],
|
||||
features: {
|
||||
...newPricing[index].features,
|
||||
included: value.split('\n'),
|
||||
},
|
||||
}
|
||||
} else {
|
||||
newPricing[index] = {
|
||||
...newPricing[index],
|
||||
features: {
|
||||
...newPricing[index].features,
|
||||
[subField]: value,
|
||||
},
|
||||
}
|
||||
}
|
||||
} else {
|
||||
newPricing[index] = { ...newPricing[index], [field]: value }
|
||||
}
|
||||
setContent({ ...content, pricing: newPricing })
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<div className="text-xl text-slate-600">Laden...</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (!content) {
|
||||
return (
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<div className="text-xl text-red-600">Fehler beim Laden</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* Toolbar */}
|
||||
<div className="bg-white rounded-xl border border-slate-200 p-4 mb-6 flex items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<h1 className="text-lg font-semibold text-slate-900">Website Content</h1>
|
||||
{/* Preview Toggle */}
|
||||
<button
|
||||
onClick={() => setShowPreview(!showPreview)}
|
||||
className={`flex items-center gap-2 px-3 py-2 rounded-lg text-sm font-medium transition-colors ${
|
||||
showPreview
|
||||
? 'bg-blue-100 text-blue-700'
|
||||
: 'bg-slate-100 text-slate-600 hover:bg-slate-200'
|
||||
}`}
|
||||
title={showPreview ? 'Preview ausblenden' : 'Preview einblenden'}
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
|
||||
</svg>
|
||||
Live-Preview
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
{message && (
|
||||
<span
|
||||
className={`px-3 py-1 rounded text-sm ${
|
||||
message.type === 'success'
|
||||
? 'bg-green-100 text-green-800'
|
||||
: 'bg-red-100 text-red-800'
|
||||
}`}
|
||||
>
|
||||
{message.text}
|
||||
</span>
|
||||
)}
|
||||
<button
|
||||
onClick={saveChanges}
|
||||
disabled={saving}
|
||||
className="bg-blue-600 text-white px-6 py-2 rounded-lg font-medium hover:bg-blue-700 disabled:opacity-50 transition-colors"
|
||||
>
|
||||
{saving ? 'Speichern...' : 'Speichern'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tabs */}
|
||||
<div className="mb-6">
|
||||
<div className="flex gap-1 bg-slate-100 p-1 rounded-lg w-fit">
|
||||
{(['hero', 'features', 'faq', 'pricing', 'other'] as const).map((tab) => (
|
||||
<button
|
||||
key={tab}
|
||||
onClick={() => setActiveTab(tab)}
|
||||
className={`px-4 py-2 text-sm font-medium rounded-md transition-colors ${
|
||||
activeTab === tab
|
||||
? 'bg-white text-slate-900 shadow-sm'
|
||||
: 'text-slate-600 hover:text-slate-900'
|
||||
}`}
|
||||
>
|
||||
{tab === 'hero' && 'Hero'}
|
||||
{tab === 'features' && 'Features'}
|
||||
{tab === 'faq' && 'FAQ'}
|
||||
{tab === 'pricing' && 'Preise'}
|
||||
{tab === 'other' && 'Sonstige'}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Split Layout: Editor + Preview */}
|
||||
<div className={`grid gap-6 ${showPreview ? 'grid-cols-2' : 'grid-cols-1'}`}>
|
||||
{/* Editor Panel */}
|
||||
<div className="bg-white rounded-xl border border-slate-200 shadow-sm p-6 max-h-[calc(100vh-280px)] overflow-y-auto">
|
||||
{/* Hero Tab */}
|
||||
{activeTab === 'hero' && (
|
||||
<div className="space-y-6">
|
||||
<h2 className="text-xl font-semibold text-slate-900">Hero Section</h2>
|
||||
|
||||
<div className="grid gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 mb-1">Badge</label>
|
||||
<input
|
||||
type="text"
|
||||
value={content.hero.badge}
|
||||
onChange={(e) => updateHero('badge', e.target.value)}
|
||||
className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 mb-1">
|
||||
Titel (vor Highlight)
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={content.hero.title}
|
||||
onChange={(e) => updateHero('title', e.target.value)}
|
||||
className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 mb-1">
|
||||
Highlight 1
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={content.hero.titleHighlight1}
|
||||
onChange={(e) => updateHero('titleHighlight1', e.target.value)}
|
||||
className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 mb-1">
|
||||
Highlight 2
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={content.hero.titleHighlight2}
|
||||
onChange={(e) => updateHero('titleHighlight2', e.target.value)}
|
||||
className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 mb-1">Untertitel</label>
|
||||
<textarea
|
||||
value={content.hero.subtitle}
|
||||
onChange={(e) => updateHero('subtitle', e.target.value)}
|
||||
rows={3}
|
||||
className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 mb-1">
|
||||
CTA Primaer
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={content.hero.ctaPrimary}
|
||||
onChange={(e) => updateHero('ctaPrimary', e.target.value)}
|
||||
className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 mb-1">
|
||||
CTA Sekundaer
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={content.hero.ctaSecondary}
|
||||
onChange={(e) => updateHero('ctaSecondary', e.target.value)}
|
||||
className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 mb-1">CTA Hinweis</label>
|
||||
<input
|
||||
type="text"
|
||||
value={content.hero.ctaHint}
|
||||
onChange={(e) => updateHero('ctaHint', e.target.value)}
|
||||
className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Features Tab */}
|
||||
{activeTab === 'features' && (
|
||||
<div className="space-y-6">
|
||||
<h2 className="text-xl font-semibold text-slate-900">Features</h2>
|
||||
|
||||
{content.features.map((feature, index) => (
|
||||
<div key={feature.id} className="border border-slate-200 rounded-lg p-4">
|
||||
<div className="grid gap-4">
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 mb-1">Icon</label>
|
||||
<input
|
||||
type="text"
|
||||
value={feature.icon}
|
||||
onChange={(e) => updateFeature(index, 'icon', e.target.value)}
|
||||
className="w-full px-3 py-2 border border-slate-300 rounded-lg text-2xl text-center"
|
||||
/>
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<label className="block text-sm font-medium text-slate-700 mb-1">Titel</label>
|
||||
<input
|
||||
type="text"
|
||||
value={feature.title}
|
||||
onChange={(e) => updateFeature(index, 'title', e.target.value)}
|
||||
className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 mb-1">
|
||||
Beschreibung
|
||||
</label>
|
||||
<textarea
|
||||
value={feature.description}
|
||||
onChange={(e) => updateFeature(index, 'description', e.target.value)}
|
||||
rows={2}
|
||||
className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* FAQ Tab */}
|
||||
{activeTab === 'faq' && (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-xl font-semibold text-slate-900">FAQ</h2>
|
||||
<button
|
||||
onClick={addFAQ}
|
||||
className="px-4 py-2 bg-slate-100 text-slate-700 rounded-lg hover:bg-slate-200 transition-colors"
|
||||
>
|
||||
+ Frage hinzufuegen
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{content.faq.map((item, index) => (
|
||||
<div key={index} className="border border-slate-200 rounded-lg p-4">
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="flex-1 space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 mb-1">
|
||||
Frage {index + 1}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={item.question}
|
||||
onChange={(e) => updateFAQ(index, 'question', e.target.value)}
|
||||
className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 mb-1">
|
||||
Antwort
|
||||
</label>
|
||||
<textarea
|
||||
value={item.answer.join('\n')}
|
||||
onChange={(e) => updateFAQ(index, 'answer', e.target.value)}
|
||||
rows={4}
|
||||
className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 font-mono text-sm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => removeFAQ(index)}
|
||||
className="p-2 text-red-600 hover:bg-red-50 rounded-lg transition-colors"
|
||||
title="Frage entfernen"
|
||||
>
|
||||
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Pricing Tab */}
|
||||
{activeTab === 'pricing' && (
|
||||
<div className="space-y-6">
|
||||
<h2 className="text-xl font-semibold text-slate-900">Preise</h2>
|
||||
|
||||
{content.pricing.map((plan, index) => (
|
||||
<div key={plan.id} className="border border-slate-200 rounded-lg p-4">
|
||||
<div className="grid gap-4">
|
||||
<div className="grid grid-cols-4 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 mb-1">Name</label>
|
||||
<input
|
||||
type="text"
|
||||
value={plan.name}
|
||||
onChange={(e) => updatePricing(index, 'name', e.target.value)}
|
||||
className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 mb-1">
|
||||
Preis (EUR)
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
step="0.01"
|
||||
value={plan.price}
|
||||
onChange={(e) => updatePricing(index, 'price', e.target.value)}
|
||||
className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 mb-1">
|
||||
Intervall
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={plan.interval}
|
||||
onChange={(e) => updatePricing(index, 'interval', e.target.value)}
|
||||
className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-end">
|
||||
<label className="flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={plan.popular || false}
|
||||
onChange={(e) => updatePricing(index, 'popular', e.target.checked)}
|
||||
className="w-4 h-4 text-blue-600 rounded"
|
||||
/>
|
||||
<span className="text-sm text-slate-700">Beliebt</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 mb-1">
|
||||
Beschreibung
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={plan.description}
|
||||
onChange={(e) => updatePricing(index, 'description', e.target.value)}
|
||||
className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 mb-1">
|
||||
Aufgaben
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={plan.features.tasks}
|
||||
onChange={(e) => updatePricing(index, 'features.tasks', e.target.value)}
|
||||
className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 mb-1">
|
||||
Aufgaben-Beschreibung
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={plan.features.taskDescription}
|
||||
onChange={(e) =>
|
||||
updatePricing(index, 'features.taskDescription', e.target.value)
|
||||
}
|
||||
className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 mb-1">
|
||||
Features (eine pro Zeile)
|
||||
</label>
|
||||
<textarea
|
||||
value={plan.features.included.join('\n')}
|
||||
onChange={(e) => updatePricing(index, 'features.included', e.target.value)}
|
||||
rows={4}
|
||||
className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 font-mono text-sm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Other Tab */}
|
||||
{activeTab === 'other' && (
|
||||
<div className="space-y-8">
|
||||
{/* Trust Indicators */}
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold text-slate-900 mb-4">Trust Indicators</h2>
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
{(['item1', 'item2', 'item3'] as const).map((key, index) => (
|
||||
<div key={key} className="border border-slate-200 rounded-lg p-4">
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 mb-1">
|
||||
Wert {index + 1}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={content.trust[key].value}
|
||||
onChange={(e) =>
|
||||
setContent({
|
||||
...content,
|
||||
trust: {
|
||||
...content.trust,
|
||||
[key]: { ...content.trust[key], value: e.target.value },
|
||||
},
|
||||
})
|
||||
}
|
||||
className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 mb-1">
|
||||
Label {index + 1}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={content.trust[key].label}
|
||||
onChange={(e) =>
|
||||
setContent({
|
||||
...content,
|
||||
trust: {
|
||||
...content.trust,
|
||||
[key]: { ...content.trust[key], label: e.target.value },
|
||||
},
|
||||
})
|
||||
}
|
||||
className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Testimonial */}
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold text-slate-900 mb-4">Testimonial</h2>
|
||||
<div className="border border-slate-200 rounded-lg p-4 space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 mb-1">Zitat</label>
|
||||
<textarea
|
||||
value={content.testimonial.quote}
|
||||
onChange={(e) =>
|
||||
setContent({
|
||||
...content,
|
||||
testimonial: { ...content.testimonial, quote: e.target.value },
|
||||
})
|
||||
}
|
||||
rows={3}
|
||||
className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 mb-1">Autor</label>
|
||||
<input
|
||||
type="text"
|
||||
value={content.testimonial.author}
|
||||
onChange={(e) =>
|
||||
setContent({
|
||||
...content,
|
||||
testimonial: { ...content.testimonial, author: e.target.value },
|
||||
})
|
||||
}
|
||||
className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 mb-1">Rolle</label>
|
||||
<input
|
||||
type="text"
|
||||
value={content.testimonial.role}
|
||||
onChange={(e) =>
|
||||
setContent({
|
||||
...content,
|
||||
testimonial: { ...content.testimonial, role: e.target.value },
|
||||
})
|
||||
}
|
||||
className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Live Preview Panel */}
|
||||
{showPreview && (
|
||||
<div className="bg-white rounded-xl border border-slate-200 shadow-sm overflow-hidden">
|
||||
{/* Preview Header */}
|
||||
<div className="bg-slate-50 border-b border-slate-200 px-4 py-3 flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex gap-1.5">
|
||||
<div className="w-3 h-3 rounded-full bg-red-400"></div>
|
||||
<div className="w-3 h-3 rounded-full bg-yellow-400"></div>
|
||||
<div className="w-3 h-3 rounded-full bg-green-400"></div>
|
||||
</div>
|
||||
<span className="text-xs text-slate-500 ml-2">breakpilot.app</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs font-medium text-slate-600 bg-slate-200 px-2 py-1 rounded">
|
||||
{activeTab === 'hero' && 'Hero Section'}
|
||||
{activeTab === 'features' && 'Features'}
|
||||
{activeTab === 'faq' && 'FAQ'}
|
||||
{activeTab === 'pricing' && 'Pricing'}
|
||||
{activeTab === 'other' && 'Trust & Testimonial'}
|
||||
</span>
|
||||
<button
|
||||
onClick={() => iframeRef.current?.contentWindow?.location.reload()}
|
||||
className="p-1.5 text-slate-500 hover:text-slate-700 hover:bg-slate-200 rounded transition-colors"
|
||||
title="Preview neu laden"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/* Preview Frame */}
|
||||
<div className="relative h-[calc(100vh-340px)] bg-slate-100">
|
||||
<iframe
|
||||
ref={iframeRef}
|
||||
src={`https://macmini:3000/?preview=true§ion=${activeTab}#${activeTab}`}
|
||||
className="w-full h-full border-0 scale-75 origin-top-left"
|
||||
style={{
|
||||
width: '133.33%',
|
||||
height: '133.33%',
|
||||
transform: 'scale(0.75)',
|
||||
transformOrigin: 'top left',
|
||||
}}
|
||||
title="Website Preview"
|
||||
sandbox="allow-same-origin allow-scripts"
|
||||
/>
|
||||
{/* Section Indicator */}
|
||||
<div className="absolute bottom-4 left-4 right-4 bg-blue-600 text-white px-4 py-2 rounded-lg shadow-lg flex items-center gap-2 text-sm">
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
<span>
|
||||
Du bearbeitest: <strong>
|
||||
{activeTab === 'hero' && 'Hero Section (Startbereich)'}
|
||||
{activeTab === 'features' && 'Features (Funktionen)'}
|
||||
{activeTab === 'faq' && 'FAQ (Haeufige Fragen)'}
|
||||
{activeTab === 'pricing' && 'Pricing (Preise)'}
|
||||
{activeTab === 'other' && 'Trust & Testimonial'}
|
||||
</strong>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -122,31 +122,31 @@ const ADMIN_SCREENS: ScreenDefinition[] = [
|
||||
{ id: 'admin-backlog', name: 'Production Backlog', description: 'Go-Live Checkliste', category: 'overview', icon: '📝', url: '/backlog' },
|
||||
{ id: 'admin-rbac', name: 'RBAC', description: 'Rollen & Berechtigungen', category: 'overview', icon: '👥', url: '/rbac' },
|
||||
|
||||
// === DSGVO (Violet #7c3aed) ===
|
||||
{ id: 'admin-consent', name: 'Consent Verwaltung', description: 'Rechtliche Dokumente & Versionen', category: 'dsgvo', icon: '📄', url: '/dsgvo/consent' },
|
||||
{ id: 'admin-dsr', name: 'Datenschutzanfragen', description: 'DSGVO Art. 15-21', category: 'dsgvo', icon: '🔒', url: '/dsgvo/dsr' },
|
||||
{ id: 'admin-einwilligungen', name: 'Einwilligungen', description: 'Nutzer-Consent Uebersicht', category: 'dsgvo', icon: '✅', url: '/dsgvo/einwilligungen' },
|
||||
{ id: 'admin-vvt', name: 'VVT', description: 'Verarbeitungsverzeichnis Art. 30', category: 'dsgvo', icon: '📋', url: '/dsgvo/vvt' },
|
||||
{ id: 'admin-dsfa', name: 'DSFA', description: 'Datenschutz-Folgenabschaetzung', category: 'dsgvo', icon: '⚖️', url: '/dsgvo/dsfa' },
|
||||
{ id: 'admin-tom', name: 'TOMs', description: 'Technische & Org. Massnahmen', category: 'dsgvo', icon: '🛡️', url: '/dsgvo/tom' },
|
||||
{ id: 'admin-loeschfristen', name: 'Loeschfristen', description: 'Aufbewahrung & Deadlines', category: 'dsgvo', icon: '🗑️', url: '/dsgvo/loeschfristen' },
|
||||
{ id: 'admin-advisory-board', name: 'Advisory Board', description: 'KI-Use-Case Pruefung', category: 'dsgvo', icon: '🧑⚖️', url: '/dsgvo/advisory-board' },
|
||||
{ id: 'admin-escalations', name: 'Eskalations-Queue', description: 'DSB Review & Freigabe', category: 'dsgvo', icon: '🚨', url: '/dsgvo/escalations' },
|
||||
|
||||
// === COMPLIANCE (Purple #9333ea) ===
|
||||
{ id: 'admin-compliance-hub', name: 'Compliance Hub', description: 'Zentrales GRC Dashboard', category: 'compliance', icon: '✅', url: '/compliance/hub' },
|
||||
{ id: 'admin-audit-checklist', name: 'Audit Checkliste', description: '476 Anforderungen pruefen', category: 'compliance', icon: '📋', url: '/compliance/audit-checklist' },
|
||||
{ id: 'admin-requirements', name: 'Requirements', description: '558+ aus 19 Verordnungen', category: 'compliance', icon: '📜', url: '/compliance/requirements' },
|
||||
{ id: 'admin-controls', name: 'Controls', description: '474 Control-Mappings', category: 'compliance', icon: '🎛️', url: '/compliance/controls' },
|
||||
{ id: 'admin-evidence', name: 'Evidence', description: 'Nachweise & Dokumentation', category: 'compliance', icon: '📎', url: '/compliance/evidence' },
|
||||
{ id: 'admin-risks', name: 'Risiken', description: 'Risk Matrix & Register', category: 'compliance', icon: '⚠️', url: '/compliance/risks' },
|
||||
{ id: 'admin-audit-report', name: 'Audit Report', description: 'PDF Audit-Berichte', category: 'compliance', icon: '📊', url: '/compliance/audit-report' },
|
||||
{ id: 'admin-modules', name: 'Service Registry', description: '30+ Service-Module', category: 'compliance', icon: '🔧', url: '/compliance/modules' },
|
||||
{ id: 'admin-dsms', name: 'DSMS', description: 'Datenschutz-Management-System', category: 'compliance', icon: '🏛️', url: '/compliance/dsms' },
|
||||
{ id: 'admin-compliance-workflow', name: 'Workflow', description: 'Freigabe-Workflows', category: 'compliance', icon: '🔄', url: '/compliance/workflow' },
|
||||
{ id: 'admin-source-policy', name: 'Quellen-Policy', description: 'Datenquellen & Compliance', category: 'compliance', icon: '📚', url: '/compliance/source-policy' },
|
||||
{ id: 'admin-ai-act', name: 'EU-AI-Act', description: 'KI-Risikoklassifizierung', category: 'compliance', icon: '🤖', url: '/compliance/ai-act' },
|
||||
{ id: 'admin-obligations', name: 'Pflichten', description: 'NIS2, DSGVO, AI Act', category: 'compliance', icon: '⚡', url: '/compliance/obligations' },
|
||||
// === COMPLIANCE SDK (Violet #8b5cf6) ===
|
||||
// DSGVO - Datenschutz & Betroffenenrechte
|
||||
{ id: 'admin-consent', name: 'Consent Verwaltung', description: 'Rechtliche Dokumente & Versionen', category: 'sdk', icon: '📄', url: '/sdk/consent-management' },
|
||||
{ id: 'admin-dsr', name: 'Datenschutzanfragen', description: 'DSGVO Art. 15-21', category: 'sdk', icon: '🔒', url: '/sdk/dsr' },
|
||||
{ id: 'admin-einwilligungen', name: 'Einwilligungen', description: 'Nutzer-Consent Uebersicht', category: 'sdk', icon: '✅', url: '/sdk/einwilligungen' },
|
||||
{ id: 'admin-vvt', name: 'VVT', description: 'Verarbeitungsverzeichnis Art. 30', category: 'sdk', icon: '📋', url: '/sdk/vvt' },
|
||||
{ id: 'admin-dsfa', name: 'DSFA', description: 'Datenschutz-Folgenabschaetzung', category: 'sdk', icon: '⚖️', url: '/sdk/dsfa' },
|
||||
{ id: 'admin-tom', name: 'TOMs', description: 'Technische & Org. Massnahmen', category: 'sdk', icon: '🛡️', url: '/sdk/tom' },
|
||||
{ id: 'admin-loeschfristen', name: 'Loeschfristen', description: 'Aufbewahrung & Deadlines', category: 'sdk', icon: '🗑️', url: '/sdk/loeschfristen' },
|
||||
{ id: 'admin-advisory-board', name: 'Advisory Board', description: 'KI-Use-Case Pruefung', category: 'sdk', icon: '🧑⚖️', url: '/sdk/advisory-board' },
|
||||
{ id: 'admin-escalations', name: 'Eskalations-Queue', description: 'DSB Review & Freigabe', category: 'sdk', icon: '🚨', url: '/sdk/escalations' },
|
||||
// Compliance - Audit, GRC & Regulatorik
|
||||
{ id: 'admin-compliance-hub', name: 'Compliance Hub', description: 'Zentrales GRC Dashboard', category: 'sdk', icon: '✅', url: '/sdk/compliance-hub' },
|
||||
{ id: 'admin-audit-checklist', name: 'Audit Checkliste', description: '476 Anforderungen pruefen', category: 'sdk', icon: '📋', url: '/sdk/audit-checklist' },
|
||||
{ id: 'admin-requirements', name: 'Requirements', description: '558+ aus 19 Verordnungen', category: 'sdk', icon: '📜', url: '/sdk/requirements' },
|
||||
{ id: 'admin-controls', name: 'Controls', description: '474 Control-Mappings', category: 'sdk', icon: '🎛️', url: '/sdk/controls' },
|
||||
{ id: 'admin-evidence', name: 'Evidence', description: 'Nachweise & Dokumentation', category: 'sdk', icon: '📎', url: '/sdk/evidence' },
|
||||
{ id: 'admin-risks', name: 'Risiken', description: 'Risk Matrix & Register', category: 'sdk', icon: '⚠️', url: '/sdk/risks' },
|
||||
{ id: 'admin-audit-report', name: 'Audit Report', description: 'PDF Audit-Berichte', category: 'sdk', icon: '📊', url: '/sdk/audit-report' },
|
||||
{ id: 'admin-modules', name: 'Service Registry', description: '30+ Service-Module', category: 'sdk', icon: '🔧', url: '/sdk/modules' },
|
||||
{ id: 'admin-dsms', name: 'DSMS', description: 'Datenschutz-Management-System', category: 'sdk', icon: '🏛️', url: '/sdk/dsms' },
|
||||
{ id: 'admin-compliance-workflow', name: 'Workflow', description: 'Freigabe-Workflows', category: 'sdk', icon: '🔄', url: '/sdk/workflow' },
|
||||
{ id: 'admin-source-policy', name: 'Quellen-Policy', description: 'Datenquellen & Compliance', category: 'sdk', icon: '📚', url: '/sdk/source-policy' },
|
||||
{ id: 'admin-ai-act', name: 'EU-AI-Act', description: 'KI-Risikoklassifizierung', category: 'sdk', icon: '🤖', url: '/sdk/ai-act' },
|
||||
{ id: 'admin-obligations', name: 'Pflichten', description: 'NIS2, DSGVO, AI Act', category: 'sdk', icon: '⚡', url: '/sdk/obligations' },
|
||||
|
||||
// === KI & AUTOMATISIERUNG (Teal #14b8a6) ===
|
||||
{ id: 'admin-llm-compare', name: 'LLM Vergleich', description: 'KI-Provider Vergleich', category: 'ai', icon: '🤖', url: '/ai/llm-compare' },
|
||||
|
||||
Reference in New Issue
Block a user