[split-required] Split 500-850 LOC files (batch 2)

backend-lehrer (10 files):
- game/database.py (785 → 5), correction_api.py (683 → 4)
- classroom_engine/antizipation.py (676 → 5)
- llm_gateway schools/edu_search already done in prior batch

klausur-service (12 files):
- orientation_crop_api.py (694 → 5), pdf_export.py (677 → 4)
- zeugnis_crawler.py (676 → 5), grid_editor_api.py (671 → 5)
- eh_templates.py (658 → 5), mail/api.py (651 → 5)
- qdrant_service.py (638 → 5), training_api.py (625 → 4)

website (6 pages):
- middleware (696 → 8), mail (733 → 6), consent (628 → 8)
- compliance/risks (622 → 5), export (502 → 5), brandbook (629 → 7)

studio-v2 (3 components):
- B2BMigrationWizard (848 → 3), CleanupPanel (765 → 2)
- dashboard-experimental (739 → 2)

admin-lehrer (4 files):
- uebersetzungen (769 → 4), manager (670 → 2)
- ChunkBrowserQA (675 → 6), dsfa/page (674 → 5)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-04-25 08:24:01 +02:00
parent 34da9f4cda
commit b4613e26f3
118 changed files with 15258 additions and 14680 deletions

View File

@@ -0,0 +1,189 @@
'use client'
import type {
WebsiteContent,
HeroContent,
FeatureContent,
} from '@/lib/content-types'
// Shared styles
export const inputCls = 'w-full px-3 py-2 border border-slate-300 rounded-lg text-sm focus:ring-2 focus:ring-sky-500 focus:border-sky-500 transition-colors'
export const labelCls = 'block text-xs font-medium text-slate-600 mb-1'
export interface EditorProps {
content: WebsiteContent
setContent: React.Dispatch<React.SetStateAction<WebsiteContent | null>>
}
// =============================================================================
// Hero Editor
// =============================================================================
export function HeroEditor({ content, setContent }: EditorProps) {
function update(field: keyof HeroContent, value: string) {
setContent(c => c ? { ...c, hero: { ...c.hero, [field]: value } } : c)
}
return (
<div className="grid gap-3">
<div><label className={labelCls}>Badge</label><input className={inputCls} value={content.hero.badge} onChange={e => update('badge', e.target.value)} /></div>
<div><label className={labelCls}>Titel</label><input className={inputCls} value={content.hero.title} onChange={e => update('title', e.target.value)} /></div>
<div className="grid grid-cols-2 gap-3">
<div><label className={labelCls}>Highlight 1</label><input className={inputCls} value={content.hero.titleHighlight1} onChange={e => update('titleHighlight1', e.target.value)} /></div>
<div><label className={labelCls}>Highlight 2</label><input className={inputCls} value={content.hero.titleHighlight2} onChange={e => update('titleHighlight2', e.target.value)} /></div>
</div>
<div><label className={labelCls}>Untertitel</label><textarea className={inputCls} rows={2} value={content.hero.subtitle} onChange={e => update('subtitle', e.target.value)} /></div>
<div className="grid grid-cols-3 gap-3">
<div><label className={labelCls}>CTA Primaer</label><input className={inputCls} value={content.hero.ctaPrimary} onChange={e => update('ctaPrimary', e.target.value)} /></div>
<div><label className={labelCls}>CTA Sekundaer</label><input className={inputCls} value={content.hero.ctaSecondary} onChange={e => update('ctaSecondary', e.target.value)} /></div>
<div><label className={labelCls}>CTA Hinweis</label><input className={inputCls} value={content.hero.ctaHint} onChange={e => update('ctaHint', e.target.value)} /></div>
</div>
</div>
)
}
// =============================================================================
// Features Editor
// =============================================================================
export function FeaturesEditor({ content, setContent }: EditorProps) {
function update(index: number, field: keyof FeatureContent, value: string) {
setContent(c => {
if (!c) return c
const features = [...c.features]
features[index] = { ...features[index], [field]: value }
return { ...c, features }
})
}
return (
<div className="space-y-3">
{content.features.map((feature, i) => (
<div key={feature.id} className="bg-slate-50 rounded-lg p-3 space-y-2">
<div className="grid grid-cols-6 gap-2">
<div><label className={labelCls}>Icon</label><input className={`${inputCls} text-center text-lg`} value={feature.icon} onChange={e => update(i, 'icon', e.target.value)} /></div>
<div className="col-span-5"><label className={labelCls}>Titel</label><input className={inputCls} value={feature.title} onChange={e => update(i, 'title', e.target.value)} /></div>
</div>
<div><label className={labelCls}>Beschreibung</label><textarea className={inputCls} rows={2} value={feature.description} onChange={e => update(i, 'description', e.target.value)} /></div>
</div>
))}
</div>
)
}
// =============================================================================
// FAQ Editor
// =============================================================================
export function FAQEditor({ content, setContent }: EditorProps) {
function updateItem(index: number, field: 'question' | 'answer', value: string) {
setContent(c => {
if (!c) return c
const faq = [...c.faq]
if (field === 'answer') { faq[index] = { ...faq[index], answer: value.split('\n') } }
else { faq[index] = { ...faq[index], question: value } }
return { ...c, faq }
})
}
function addItem() { setContent(c => c ? { ...c, faq: [...c.faq, { question: 'Neue Frage?', answer: ['Antwort hier...'] }] } : c) }
function removeItem(index: number) { setContent(c => c ? { ...c, faq: c.faq.filter((_, i) => i !== index) } : c) }
return (
<div className="space-y-3">
{content.faq.map((item, i) => (
<div key={i} className="bg-slate-50 rounded-lg p-3 space-y-2 relative group">
<button onClick={() => removeItem(i)} className="absolute top-2 right-2 p-1 text-red-400 hover:text-red-600 opacity-0 group-hover:opacity-100 transition-opacity" title="Entfernen">
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" /></svg>
</button>
<div><label className={labelCls}>Frage {i + 1}</label><input className={inputCls} value={item.question} onChange={e => updateItem(i, 'question', e.target.value)} /></div>
<div><label className={labelCls}>Antwort</label><textarea className={`${inputCls} font-mono`} rows={3} value={item.answer.join('\n')} onChange={e => updateItem(i, 'answer', e.target.value)} /></div>
</div>
))}
<button onClick={addItem} className="w-full py-2 border-2 border-dashed border-slate-300 rounded-lg text-sm text-slate-500 hover:border-sky-400 hover:text-sky-600 transition-colors">
+ Frage hinzufuegen
</button>
</div>
)
}
// =============================================================================
// Pricing Editor
// =============================================================================
export function PricingEditor({ content, setContent }: EditorProps) {
function update(index: number, field: string, value: string | number | boolean) {
setContent(c => {
if (!c) return c
const pricing = [...c.pricing]
if (field === 'price') { pricing[index] = { ...pricing[index], price: Number(value) } }
else if (field === 'popular') { pricing[index] = { ...pricing[index], popular: Boolean(value) } }
else if (field.startsWith('features.')) {
const sub = field.replace('features.', '')
if (sub === 'included' && typeof value === 'string') { pricing[index] = { ...pricing[index], features: { ...pricing[index].features, included: value.split('\n') } } }
else { pricing[index] = { ...pricing[index], features: { ...pricing[index].features, [sub]: value } } }
} else { pricing[index] = { ...pricing[index], [field]: value } }
return { ...c, pricing }
})
}
return (
<div className="space-y-4">
{content.pricing.map((plan, i) => (
<div key={plan.id} className="bg-slate-50 rounded-lg p-3 space-y-2">
<div className="flex items-center gap-2 mb-1">
<span className="text-sm font-semibold text-slate-800">{plan.name}</span>
{plan.popular && <span className="text-xs bg-sky-100 text-sky-700 px-1.5 py-0.5 rounded">Beliebt</span>}
</div>
<div className="grid grid-cols-4 gap-2">
<div><label className={labelCls}>Name</label><input className={inputCls} value={plan.name} onChange={e => update(i, 'name', e.target.value)} /></div>
<div><label className={labelCls}>Preis (EUR)</label><input className={inputCls} type="number" step="0.01" value={plan.price} onChange={e => update(i, 'price', e.target.value)} /></div>
<div><label className={labelCls}>Intervall</label><input className={inputCls} value={plan.interval} onChange={e => update(i, 'interval', e.target.value)} /></div>
<div className="flex items-end pb-1"><label className="flex items-center gap-2 cursor-pointer"><input type="checkbox" checked={plan.popular || false} onChange={e => update(i, 'popular', e.target.checked)} className="w-4 h-4 text-sky-600 rounded" /><span className="text-xs text-slate-600">Beliebt</span></label></div>
</div>
<div><label className={labelCls}>Beschreibung</label><input className={inputCls} value={plan.description} onChange={e => update(i, 'description', e.target.value)} /></div>
<div className="grid grid-cols-2 gap-2">
<div><label className={labelCls}>Aufgaben</label><input className={inputCls} value={plan.features.tasks} onChange={e => update(i, 'features.tasks', e.target.value)} /></div>
<div><label className={labelCls}>Aufgaben-Beschreibung</label><input className={inputCls} value={plan.features.taskDescription} onChange={e => update(i, 'features.taskDescription', e.target.value)} /></div>
</div>
<div><label className={labelCls}>Features (eine pro Zeile)</label><textarea className={`${inputCls} font-mono`} rows={3} value={plan.features.included.join('\n')} onChange={e => update(i, 'features.included', e.target.value)} /></div>
</div>
))}
</div>
)
}
// =============================================================================
// Trust Editor
// =============================================================================
export function TrustEditor({ content, setContent }: EditorProps) {
function update(key: 'item1' | 'item2' | 'item3', field: 'value' | 'label', val: string) {
setContent(c => c ? { ...c, trust: { ...c.trust, [key]: { ...c.trust[key], [field]: val } } } : c)
}
return (
<div className="grid grid-cols-3 gap-3">
{(['item1', 'item2', 'item3'] as const).map((key, i) => (
<div key={key} className="bg-slate-50 rounded-lg p-3 space-y-2">
<div><label className={labelCls}>Wert {i + 1}</label><input className={inputCls} value={content.trust[key].value} onChange={e => update(key, 'value', e.target.value)} /></div>
<div><label className={labelCls}>Label {i + 1}</label><input className={inputCls} value={content.trust[key].label} onChange={e => update(key, 'label', e.target.value)} /></div>
</div>
))}
</div>
)
}
// =============================================================================
// Testimonial Editor
// =============================================================================
export function TestimonialEditor({ content, setContent }: EditorProps) {
function update(field: 'quote' | 'author' | 'role', value: string) {
setContent(c => c ? { ...c, testimonial: { ...c.testimonial, [field]: value } } : c)
}
return (
<div className="space-y-3">
<div><label className={labelCls}>Zitat</label><textarea className={inputCls} rows={3} value={content.testimonial.quote} onChange={e => update('quote', e.target.value)} /></div>
<div className="grid grid-cols-2 gap-3">
<div><label className={labelCls}>Autor</label><input className={inputCls} value={content.testimonial.author} onChange={e => update('author', e.target.value)} /></div>
<div><label className={labelCls}>Rolle</label><input className={inputCls} value={content.testimonial.role} onChange={e => update('role', e.target.value)} /></div>
</div>
</div>
)
}

View File

@@ -9,17 +9,14 @@
*/
import { useState, useEffect, useRef, useCallback } from 'react'
import type {
WebsiteContent,
HeroContent,
FeatureContent,
FAQItem,
PricingPlan,
} from '@/lib/content-types'
import type { WebsiteContent } from '@/lib/content-types'
import {
HeroEditor, FeaturesEditor, FAQEditor, PricingEditor,
TrustEditor, TestimonialEditor,
} from './_components/SectionEditors'
const ADMIN_KEY = 'breakpilot-admin-2024'
// Section metadata for cards
const SECTIONS = [
{ key: 'hero', name: 'Hero Section', icon: '🎯', scrollTo: 'hero' },
{ key: 'features', name: 'Features', icon: '⚡', scrollTo: 'features' },
@@ -35,52 +32,34 @@ type SectionKey = (typeof SECTIONS)[number]['key']
function countWords(content: WebsiteContent): number {
const texts: string[] = []
// Hero
texts.push(content.hero.badge, content.hero.title, content.hero.titleHighlight1, content.hero.titleHighlight2, content.hero.subtitle, content.hero.ctaPrimary, content.hero.ctaSecondary, content.hero.ctaHint)
// Features
content.features.forEach(f => { texts.push(f.title, f.description) })
// FAQ
content.faq.forEach(f => { texts.push(f.question, ...f.answer) })
// Pricing
content.pricing.forEach(p => { texts.push(p.name, p.description, p.features.tasks, p.features.taskDescription, ...p.features.included) })
// Trust
texts.push(content.trust.item1.value, content.trust.item1.label, content.trust.item2.value, content.trust.item2.label, content.trust.item3.value, content.trust.item3.label)
// Testimonial
texts.push(content.testimonial.quote, content.testimonial.author, content.testimonial.role)
return texts.filter(Boolean).join(' ').split(/\s+/).filter(Boolean).length
}
function sectionComplete(content: WebsiteContent, section: SectionKey): boolean {
switch (section) {
case 'hero':
return !!(content.hero.title && content.hero.subtitle && content.hero.ctaPrimary)
case 'features':
return content.features.length > 0 && content.features.every(f => f.title && f.description)
case 'faq':
return content.faq.length > 0 && content.faq.every(f => f.question && f.answer.length > 0)
case 'pricing':
return content.pricing.length > 0 && content.pricing.every(p => p.name && p.price > 0)
case 'trust':
return !!(content.trust.item1.value && content.trust.item2.value && content.trust.item3.value)
case 'testimonial':
return !!(content.testimonial.quote && content.testimonial.author)
case 'hero': return !!(content.hero.title && content.hero.subtitle && content.hero.ctaPrimary)
case 'features': return content.features.length > 0 && content.features.every(f => f.title && f.description)
case 'faq': return content.faq.length > 0 && content.faq.every(f => f.question && f.answer.length > 0)
case 'pricing': return content.pricing.length > 0 && content.pricing.every(p => p.name && p.price > 0)
case 'trust': return !!(content.trust.item1.value && content.trust.item2.value && content.trust.item3.value)
case 'testimonial': return !!(content.testimonial.quote && content.testimonial.author)
}
}
function sectionSummary(content: WebsiteContent, section: SectionKey): string {
switch (section) {
case 'hero':
return `"${content.hero.title} ${content.hero.titleHighlight1}"`.slice(0, 50)
case 'features':
return `${content.features.length} Features`
case 'faq':
return `${content.faq.length} Fragen`
case 'pricing':
return `${content.pricing.length} Plaene`
case 'trust':
return `${content.trust.item1.value}, ${content.trust.item2.value}, ${content.trust.item3.value}`
case 'testimonial':
return `"${content.testimonial.quote.slice(0, 40)}..."`
case 'hero': return `"${content.hero.title} ${content.hero.titleHighlight1}"`.slice(0, 50)
case 'features': return `${content.features.length} Features`
case 'faq': return `${content.faq.length} Fragen`
case 'pricing': return `${content.pricing.length} Plaene`
case 'trust': return `${content.trust.item1.value}, ${content.trust.item2.value}, ${content.trust.item3.value}`
case 'testimonial': return `"${content.testimonial.quote.slice(0, 40)}..."`
}
}
@@ -96,108 +75,49 @@ export default function WebsiteManagerPage() {
const [websiteStatus, setWebsiteStatus] = useState<{ online: boolean; responseTime: number } | null>(null)
const iframeRef = useRef<HTMLIFrameElement>(null)
// Load content
useEffect(() => {
loadContent()
checkWebsiteStatus()
}, [])
// Auto-dismiss messages
useEffect(() => {
if (message) {
const t = setTimeout(() => setMessage(null), 4000)
return () => clearTimeout(t)
}
}, [message])
useEffect(() => { loadContent(); checkWebsiteStatus() }, [])
useEffect(() => { if (message) { const t = setTimeout(() => setMessage(null), 4000); return () => clearTimeout(t) } }, [message])
async function loadContent() {
try {
const res = await fetch('/api/website/content')
if (res.ok) {
const data = await res.json()
setContent(data)
setOriginalContent(JSON.parse(JSON.stringify(data)))
} else {
setMessage({ type: 'error', text: 'Fehler beim Laden des Contents' })
}
} catch {
setMessage({ type: 'error', text: 'Verbindungsfehler beim Laden' })
} finally {
setLoading(false)
}
if (res.ok) { const data = await res.json(); setContent(data); setOriginalContent(JSON.parse(JSON.stringify(data))) }
else { setMessage({ type: 'error', text: 'Fehler beim Laden des Contents' }) }
} catch { setMessage({ type: 'error', text: 'Verbindungsfehler beim Laden' }) }
finally { setLoading(false) }
}
async function checkWebsiteStatus() {
try {
const res = await fetch('/api/website/status')
if (res.ok) {
const data = await res.json()
setWebsiteStatus(data)
}
} catch {
setWebsiteStatus({ online: false, responseTime: 0 })
}
try { const res = await fetch('/api/website/status'); if (res.ok) { setWebsiteStatus(await res.json()) } }
catch { setWebsiteStatus({ online: false, responseTime: 0 }) }
}
async function saveChanges() {
if (!content) return
setSaving(true)
setMessage(null)
setSaving(true); setMessage(null)
try {
const res = await fetch('/api/website/content', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'x-admin-key': ADMIN_KEY },
body: JSON.stringify(content),
})
if (res.ok) {
setMessage({ type: 'success', text: 'Erfolgreich gespeichert!' })
setOriginalContent(JSON.parse(JSON.stringify(content)))
// Reload iframe to reflect changes
if (iframeRef.current) {
iframeRef.current.src = iframeRef.current.src
}
} else {
const err = await res.json()
setMessage({ type: 'error', text: err.error || 'Fehler beim Speichern' })
}
} catch {
setMessage({ type: 'error', text: 'Verbindungsfehler beim Speichern' })
} finally {
setSaving(false)
}
const res = await fetch('/api/website/content', { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-admin-key': ADMIN_KEY }, body: JSON.stringify(content) })
if (res.ok) { setMessage({ type: 'success', text: 'Erfolgreich gespeichert!' }); setOriginalContent(JSON.parse(JSON.stringify(content))); if (iframeRef.current) { iframeRef.current.src = iframeRef.current.src } }
else { const err = await res.json(); setMessage({ type: 'error', text: err.error || 'Fehler beim Speichern' }) }
} catch { setMessage({ type: 'error', text: 'Verbindungsfehler beim Speichern' }) }
finally { setSaving(false) }
}
function resetContent() {
if (originalContent) {
setContent(JSON.parse(JSON.stringify(originalContent)))
setMessage({ type: 'success', text: 'Zurueckgesetzt auf letzten gespeicherten Stand' })
}
if (originalContent) { setContent(JSON.parse(JSON.stringify(originalContent))); setMessage({ type: 'success', text: 'Zurueckgesetzt auf letzten gespeicherten Stand' }) }
}
// Scroll iframe to section
const scrollPreview = useCallback((scrollTo: string) => {
if (!iframeRef.current?.contentWindow) return
try {
iframeRef.current.contentWindow.postMessage(
{ type: 'scrollTo', section: scrollTo },
'*'
)
} catch {
// cross-origin fallback
}
try { iframeRef.current.contentWindow.postMessage({ type: 'scrollTo', section: scrollTo }, '*') } catch { /* cross-origin fallback */ }
}, [])
function toggleSection(key: SectionKey) {
const newExpanded = expandedSection === key ? null : key
setExpandedSection(newExpanded)
if (newExpanded) {
const section = SECTIONS.find(s => s.key === newExpanded)
if (section) scrollPreview(section.scrollTo)
}
if (newExpanded) { const section = SECTIONS.find(s => s.key === newExpanded); if (section) scrollPreview(section.scrollTo) }
}
// ─── Render ────────────────────────────────────────────────────────────────
if (loading) {
return (
<div className="flex items-center justify-center py-20">
@@ -213,11 +133,7 @@ export default function WebsiteManagerPage() {
}
if (!content) {
return (
<div className="flex items-center justify-center py-20">
<div className="text-red-600">Content konnte nicht geladen werden.</div>
</div>
)
return <div className="flex items-center justify-center py-20"><div className="text-red-600">Content konnte nicht geladen werden.</div></div>
}
const wordCount = countWords(content)
@@ -227,58 +143,29 @@ export default function WebsiteManagerPage() {
return (
<div className="space-y-4">
{/* ── Status Bar ───────────────────────────────────────────────────── */}
{/* Status Bar */}
<div className="bg-white rounded-xl border border-slate-200 px-5 py-3 flex items-center justify-between">
<div className="flex items-center gap-4">
{/* Website status */}
<div className="flex items-center gap-2">
<span className={`w-2.5 h-2.5 rounded-full ${websiteStatus?.online ? 'bg-green-500' : 'bg-red-500'}`} />
<span className="text-sm text-slate-700">
Website {websiteStatus?.online ? 'online' : 'offline'}
{websiteStatus?.online && websiteStatus.responseTime > 0 && (
<span className="text-slate-400 ml-1">({websiteStatus.responseTime}ms)</span>
)}
{websiteStatus?.online && websiteStatus.responseTime > 0 && <span className="text-slate-400 ml-1">({websiteStatus.responseTime}ms)</span>}
</span>
</div>
{/* Link */}
<a
href="https://macmini:3000"
target="_blank"
rel="noopener noreferrer"
className="text-sm text-sky-600 hover:text-sky-700 flex items-center gap-1"
>
<a href="https://macmini:3000" target="_blank" rel="noopener noreferrer" className="text-sm text-sky-600 hover:text-sky-700 flex items-center gap-1">
Zur Website
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
</svg>
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" /></svg>
</a>
</div>
<div className="flex items-center gap-3">
{message && (
<span className={`px-3 py-1 rounded-lg text-sm font-medium ${
message.type === 'success' ? 'bg-green-50 text-green-700' : 'bg-red-50 text-red-700'
}`}>
{message.text}
</span>
)}
<button
onClick={resetContent}
disabled={!hasChanges}
className="px-4 py-2 text-sm font-medium text-slate-600 bg-slate-100 rounded-lg hover:bg-slate-200 disabled:opacity-40 disabled:cursor-not-allowed transition-colors"
>
Reset
</button>
<button
onClick={saveChanges}
disabled={saving || !hasChanges}
className="px-5 py-2 text-sm font-medium text-white bg-sky-600 rounded-lg hover:bg-sky-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
{saving ? 'Speichern...' : 'Speichern'}
</button>
{message && <span className={`px-3 py-1 rounded-lg text-sm font-medium ${message.type === 'success' ? 'bg-green-50 text-green-700' : 'bg-red-50 text-red-700'}`}>{message.text}</span>}
<button onClick={resetContent} disabled={!hasChanges} className="px-4 py-2 text-sm font-medium text-slate-600 bg-slate-100 rounded-lg hover:bg-slate-200 disabled:opacity-40 disabled:cursor-not-allowed transition-colors">Reset</button>
<button onClick={saveChanges} disabled={saving || !hasChanges} className="px-5 py-2 text-sm font-medium text-white bg-sky-600 rounded-lg hover:bg-sky-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors">{saving ? 'Speichern...' : 'Speichern'}</button>
</div>
</div>
{/* ── Stats Bar ────────────────────────────────────────────────────── */}
{/* Stats Bar */}
<div className="grid grid-cols-4 gap-3">
{[
{ label: 'Sektionen', value: `${SECTIONS.length}`, icon: '📄' },
@@ -288,33 +175,21 @@ export default function WebsiteManagerPage() {
].map((stat) => (
<div key={stat.label} className="bg-white rounded-xl border border-slate-200 px-4 py-3 flex items-center gap-3">
<span className="text-xl">{stat.icon}</span>
<div>
<div className="text-sm font-semibold text-slate-900">{stat.value}</div>
<div className="text-xs text-slate-500">{stat.label}</div>
</div>
<div><div className="text-sm font-semibold text-slate-900">{stat.value}</div><div className="text-xs text-slate-500">{stat.label}</div></div>
</div>
))}
</div>
{/* ── Main Layout: 60/40 ───────────────────────────────────────────── */}
{/* Main Layout: 60/40 */}
<div className="grid grid-cols-5 gap-4" style={{ height: 'calc(100vh - 300px)' }}>
{/* ── Left: Section Cards (3/5 = 60%) ──────────────────────────── */}
{/* Left: Section Cards */}
<div className="col-span-3 overflow-y-auto pr-1 space-y-3">
{SECTIONS.map((section) => {
const isExpanded = expandedSection === section.key
const isComplete = sectionComplete(content, section.key)
return (
<div
key={section.key}
className={`bg-white rounded-xl border transition-all ${
isExpanded ? 'border-sky-300 shadow-md' : 'border-slate-200 hover:border-slate-300'
}`}
>
{/* Card Header */}
<button
onClick={() => toggleSection(section.key)}
className="w-full px-5 py-4 flex items-center justify-between text-left"
>
<div key={section.key} className={`bg-white rounded-xl border transition-all ${isExpanded ? 'border-sky-300 shadow-md' : 'border-slate-200 hover:border-slate-300'}`}>
<button onClick={() => toggleSection(section.key)} className="w-full px-5 py-4 flex items-center justify-between text-left">
<div className="flex items-center gap-3">
<span className="text-xl">{section.icon}</span>
<div>
@@ -330,16 +205,11 @@ export default function WebsiteManagerPage() {
) : (
<span className="w-6 h-6 rounded-full bg-amber-100 text-amber-600 flex items-center justify-center text-xs">!</span>
)}
<svg
className={`w-5 h-5 text-slate-400 transition-transform ${isExpanded ? 'rotate-180' : ''}`}
fill="none" viewBox="0 0 24 24" stroke="currentColor"
>
<svg className={`w-5 h-5 text-slate-400 transition-transform ${isExpanded ? 'rotate-180' : ''}`} fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
</div>
</button>
{/* Inline Editor */}
{isExpanded && (
<div className="px-5 pb-5 border-t border-slate-100 pt-4">
{section.key === 'hero' && <HeroEditor content={content} setContent={setContent} />}
@@ -355,316 +225,22 @@ export default function WebsiteManagerPage() {
})}
</div>
{/* ── Right: Live Preview (2/5 = 40%) ──────────────────────────── */}
{/* Right: Live Preview */}
<div className="col-span-2 bg-white rounded-xl border border-slate-200 overflow-hidden flex flex-col">
{/* Preview Header */}
<div className="bg-slate-50 border-b border-slate-200 px-4 py-2.5 flex items-center justify-between flex-shrink-0">
<div className="flex items-center gap-2">
<div className="flex gap-1.5">
<div className="w-2.5 h-2.5 rounded-full bg-red-400" />
<div className="w-2.5 h-2.5 rounded-full bg-yellow-400" />
<div className="w-2.5 h-2.5 rounded-full bg-green-400" />
</div>
<div className="flex gap-1.5"><div className="w-2.5 h-2.5 rounded-full bg-red-400" /><div className="w-2.5 h-2.5 rounded-full bg-yellow-400" /><div className="w-2.5 h-2.5 rounded-full bg-green-400" /></div>
<span className="text-xs text-slate-500 ml-2">macmini:3000</span>
</div>
<button
onClick={() => { if (iframeRef.current) iframeRef.current.src = iframeRef.current.src }}
className="p-1 text-slate-400 hover:text-slate-600 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 onClick={() => { if (iframeRef.current) iframeRef.current.src = iframeRef.current.src }} className="p-1 text-slate-400 hover:text-slate-600 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>
{/* iframe */}
<div className="flex-1 relative bg-slate-100">
<iframe
ref={iframeRef}
src="https://macmini:3000/?preview=true"
className="absolute inset-0 w-full h-full border-0"
style={{
width: '166.67%',
height: '166.67%',
transform: 'scale(0.6)',
transformOrigin: 'top left',
}}
title="Website Preview"
sandbox="allow-same-origin allow-scripts"
/>
<iframe ref={iframeRef} src="https://macmini:3000/?preview=true" className="absolute inset-0 w-full h-full border-0" style={{ width: '166.67%', height: '166.67%', transform: 'scale(0.6)', transformOrigin: 'top left' }} title="Website Preview" sandbox="allow-same-origin allow-scripts" />
</div>
</div>
</div>
</div>
)
}
// ─── Section Editors ─────────────────────────────────────────────────────────
interface EditorProps {
content: WebsiteContent
setContent: React.Dispatch<React.SetStateAction<WebsiteContent | null>>
}
const inputCls = 'w-full px-3 py-2 border border-slate-300 rounded-lg text-sm focus:ring-2 focus:ring-sky-500 focus:border-sky-500 transition-colors'
const labelCls = 'block text-xs font-medium text-slate-600 mb-1'
// ─── Hero Editor ─────────────────────────────────────────────────────────────
function HeroEditor({ content, setContent }: EditorProps) {
function update(field: keyof HeroContent, value: string) {
setContent(c => c ? { ...c, hero: { ...c.hero, [field]: value } } : c)
}
return (
<div className="grid gap-3">
<div>
<label className={labelCls}>Badge</label>
<input className={inputCls} value={content.hero.badge} onChange={e => update('badge', e.target.value)} />
</div>
<div>
<label className={labelCls}>Titel</label>
<input className={inputCls} value={content.hero.title} onChange={e => update('title', e.target.value)} />
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<label className={labelCls}>Highlight 1</label>
<input className={inputCls} value={content.hero.titleHighlight1} onChange={e => update('titleHighlight1', e.target.value)} />
</div>
<div>
<label className={labelCls}>Highlight 2</label>
<input className={inputCls} value={content.hero.titleHighlight2} onChange={e => update('titleHighlight2', e.target.value)} />
</div>
</div>
<div>
<label className={labelCls}>Untertitel</label>
<textarea className={inputCls} rows={2} value={content.hero.subtitle} onChange={e => update('subtitle', e.target.value)} />
</div>
<div className="grid grid-cols-3 gap-3">
<div>
<label className={labelCls}>CTA Primaer</label>
<input className={inputCls} value={content.hero.ctaPrimary} onChange={e => update('ctaPrimary', e.target.value)} />
</div>
<div>
<label className={labelCls}>CTA Sekundaer</label>
<input className={inputCls} value={content.hero.ctaSecondary} onChange={e => update('ctaSecondary', e.target.value)} />
</div>
<div>
<label className={labelCls}>CTA Hinweis</label>
<input className={inputCls} value={content.hero.ctaHint} onChange={e => update('ctaHint', e.target.value)} />
</div>
</div>
</div>
)
}
// ─── Features Editor ─────────────────────────────────────────────────────────
function FeaturesEditor({ content, setContent }: EditorProps) {
function update(index: number, field: keyof FeatureContent, value: string) {
setContent(c => {
if (!c) return c
const features = [...c.features]
features[index] = { ...features[index], [field]: value }
return { ...c, features }
})
}
return (
<div className="space-y-3">
{content.features.map((feature, i) => (
<div key={feature.id} className="bg-slate-50 rounded-lg p-3 space-y-2">
<div className="grid grid-cols-6 gap-2">
<div>
<label className={labelCls}>Icon</label>
<input className={`${inputCls} text-center text-lg`} value={feature.icon} onChange={e => update(i, 'icon', e.target.value)} />
</div>
<div className="col-span-5">
<label className={labelCls}>Titel</label>
<input className={inputCls} value={feature.title} onChange={e => update(i, 'title', e.target.value)} />
</div>
</div>
<div>
<label className={labelCls}>Beschreibung</label>
<textarea className={inputCls} rows={2} value={feature.description} onChange={e => update(i, 'description', e.target.value)} />
</div>
</div>
))}
</div>
)
}
// ─── FAQ Editor ──────────────────────────────────────────────────────────────
function FAQEditor({ content, setContent }: EditorProps) {
function updateItem(index: number, field: 'question' | 'answer', value: string) {
setContent(c => {
if (!c) return c
const faq = [...c.faq]
if (field === 'answer') {
faq[index] = { ...faq[index], answer: value.split('\n') }
} else {
faq[index] = { ...faq[index], question: value }
}
return { ...c, faq }
})
}
function addItem() {
setContent(c => c ? { ...c, faq: [...c.faq, { question: 'Neue Frage?', answer: ['Antwort hier...'] }] } : c)
}
function removeItem(index: number) {
setContent(c => c ? { ...c, faq: c.faq.filter((_, i) => i !== index) } : c)
}
return (
<div className="space-y-3">
{content.faq.map((item, i) => (
<div key={i} className="bg-slate-50 rounded-lg p-3 space-y-2 relative group">
<button
onClick={() => removeItem(i)}
className="absolute top-2 right-2 p-1 text-red-400 hover:text-red-600 opacity-0 group-hover:opacity-100 transition-opacity"
title="Entfernen"
>
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
<div>
<label className={labelCls}>Frage {i + 1}</label>
<input className={inputCls} value={item.question} onChange={e => updateItem(i, 'question', e.target.value)} />
</div>
<div>
<label className={labelCls}>Antwort</label>
<textarea className={`${inputCls} font-mono`} rows={3} value={item.answer.join('\n')} onChange={e => updateItem(i, 'answer', e.target.value)} />
</div>
</div>
))}
<button onClick={addItem} className="w-full py-2 border-2 border-dashed border-slate-300 rounded-lg text-sm text-slate-500 hover:border-sky-400 hover:text-sky-600 transition-colors">
+ Frage hinzufuegen
</button>
</div>
)
}
// ─── Pricing Editor ──────────────────────────────────────────────────────────
function PricingEditor({ content, setContent }: EditorProps) {
function update(index: number, field: string, value: string | number | boolean) {
setContent(c => {
if (!c) return c
const pricing = [...c.pricing]
if (field === 'price') {
pricing[index] = { ...pricing[index], price: Number(value) }
} else if (field === 'popular') {
pricing[index] = { ...pricing[index], popular: Boolean(value) }
} else if (field.startsWith('features.')) {
const sub = field.replace('features.', '')
if (sub === 'included' && typeof value === 'string') {
pricing[index] = { ...pricing[index], features: { ...pricing[index].features, included: value.split('\n') } }
} else {
pricing[index] = { ...pricing[index], features: { ...pricing[index].features, [sub]: value } }
}
} else {
pricing[index] = { ...pricing[index], [field]: value }
}
return { ...c, pricing }
})
}
return (
<div className="space-y-4">
{content.pricing.map((plan, i) => (
<div key={plan.id} className="bg-slate-50 rounded-lg p-3 space-y-2">
<div className="flex items-center gap-2 mb-1">
<span className="text-sm font-semibold text-slate-800">{plan.name}</span>
{plan.popular && <span className="text-xs bg-sky-100 text-sky-700 px-1.5 py-0.5 rounded">Beliebt</span>}
</div>
<div className="grid grid-cols-4 gap-2">
<div>
<label className={labelCls}>Name</label>
<input className={inputCls} value={plan.name} onChange={e => update(i, 'name', e.target.value)} />
</div>
<div>
<label className={labelCls}>Preis (EUR)</label>
<input className={inputCls} type="number" step="0.01" value={plan.price} onChange={e => update(i, 'price', e.target.value)} />
</div>
<div>
<label className={labelCls}>Intervall</label>
<input className={inputCls} value={plan.interval} onChange={e => update(i, 'interval', e.target.value)} />
</div>
<div className="flex items-end pb-1">
<label className="flex items-center gap-2 cursor-pointer">
<input type="checkbox" checked={plan.popular || false} onChange={e => update(i, 'popular', e.target.checked)} className="w-4 h-4 text-sky-600 rounded" />
<span className="text-xs text-slate-600">Beliebt</span>
</label>
</div>
</div>
<div>
<label className={labelCls}>Beschreibung</label>
<input className={inputCls} value={plan.description} onChange={e => update(i, 'description', e.target.value)} />
</div>
<div className="grid grid-cols-2 gap-2">
<div>
<label className={labelCls}>Aufgaben</label>
<input className={inputCls} value={plan.features.tasks} onChange={e => update(i, 'features.tasks', e.target.value)} />
</div>
<div>
<label className={labelCls}>Aufgaben-Beschreibung</label>
<input className={inputCls} value={plan.features.taskDescription} onChange={e => update(i, 'features.taskDescription', e.target.value)} />
</div>
</div>
<div>
<label className={labelCls}>Features (eine pro Zeile)</label>
<textarea className={`${inputCls} font-mono`} rows={3} value={plan.features.included.join('\n')} onChange={e => update(i, 'features.included', e.target.value)} />
</div>
</div>
))}
</div>
)
}
// ─── Trust Editor ────────────────────────────────────────────────────────────
function TrustEditor({ content, setContent }: EditorProps) {
function update(key: 'item1' | 'item2' | 'item3', field: 'value' | 'label', val: string) {
setContent(c => c ? { ...c, trust: { ...c.trust, [key]: { ...c.trust[key], [field]: val } } } : c)
}
return (
<div className="grid grid-cols-3 gap-3">
{(['item1', 'item2', 'item3'] as const).map((key, i) => (
<div key={key} className="bg-slate-50 rounded-lg p-3 space-y-2">
<div>
<label className={labelCls}>Wert {i + 1}</label>
<input className={inputCls} value={content.trust[key].value} onChange={e => update(key, 'value', e.target.value)} />
</div>
<div>
<label className={labelCls}>Label {i + 1}</label>
<input className={inputCls} value={content.trust[key].label} onChange={e => update(key, 'label', e.target.value)} />
</div>
</div>
))}
</div>
)
}
// ─── Testimonial Editor ──────────────────────────────────────────────────────
function TestimonialEditor({ content, setContent }: EditorProps) {
function update(field: 'quote' | 'author' | 'role', value: string) {
setContent(c => c ? { ...c, testimonial: { ...c.testimonial, [field]: value } } : c)
}
return (
<div className="space-y-3">
<div>
<label className={labelCls}>Zitat</label>
<textarea className={inputCls} rows={3} value={content.testimonial.quote} onChange={e => update('quote', e.target.value)} />
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<label className={labelCls}>Autor</label>
<input className={inputCls} value={content.testimonial.author} onChange={e => update('author', e.target.value)} />
</div>
<div>
<label className={labelCls}>Rolle</label>
<input className={inputCls} value={content.testimonial.role} onChange={e => update('role', e.target.value)} />
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,367 @@
'use client'
import type { WebsiteContent, FeatureContent } from '@/lib/content-types'
// =============================================================================
// Features Editor
// =============================================================================
interface FeaturesEditorProps {
content: WebsiteContent
updateFeature: (index: number, field: keyof FeatureContent, value: string) => void
}
export function FeaturesEditor({ content, updateFeature }: FeaturesEditorProps) {
return (
<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 Editor
// =============================================================================
interface FAQEditorProps {
content: WebsiteContent
updateFAQ: (index: number, field: 'question' | 'answer', value: string | string[]) => void
addFAQ: () => void
removeFAQ: (index: number) => void
}
export function FAQEditor({ content, updateFAQ, addFAQ, removeFAQ }: FAQEditorProps) {
return (
<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 Editor
// =============================================================================
interface PricingEditorProps {
content: WebsiteContent
updatePricing: (index: number, field: string, value: string | number | boolean) => void
}
export function PricingEditor({ content, updatePricing }: PricingEditorProps) {
return (
<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 Section Editor (Trust + Testimonial)
// =============================================================================
interface OtherEditorProps {
content: WebsiteContent
setContent: (content: WebsiteContent) => void
}
export function OtherEditor({ content, setContent }: OtherEditorProps) {
return (
<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>
)
}

View File

@@ -0,0 +1,109 @@
'use client'
import type { WebsiteContent, HeroContent } from '@/lib/content-types'
interface HeroEditorProps {
content: WebsiteContent
updateHero: (field: keyof HeroContent, value: string) => void
}
export function HeroEditor({ content, updateHero }: HeroEditorProps) {
return (
<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>
)
}

View File

@@ -0,0 +1,75 @@
'use client'
import { RefObject } from 'react'
interface PreviewPanelProps {
activeTab: string
iframeRef: RefObject<HTMLIFrameElement | null>
}
export function PreviewPanel({ activeTab, iframeRef }: PreviewPanelProps) {
return (
<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&section=${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>
)
}

View File

@@ -16,6 +16,9 @@
import { useState, useEffect, useRef, useCallback } from 'react'
import { WebsiteContent, HeroContent, FeatureContent } from '@/lib/content-types'
import { HeroEditor } from './_components/HeroEditor'
import { FeaturesEditor, FAQEditor, PricingEditor, OtherEditor } from './_components/ContentEditors'
import { PreviewPanel } from './_components/PreviewPanel'
// Admin Key (in production via login)
const ADMIN_KEY = 'breakpilot-admin-2024'
@@ -54,15 +57,8 @@ export default function UebersetzungenPage() {
}
}, [])
// Scroll to section on tab change
useEffect(() => {
scrollToSection(activeTab)
}, [activeTab, scrollToSection])
// Load content
useEffect(() => {
loadContent()
}, [])
useEffect(() => { scrollToSection(activeTab) }, [activeTab, scrollToSection])
useEffect(() => { loadContent() }, [])
async function loadContent() {
try {
@@ -82,20 +78,14 @@ export default function UebersetzungenPage() {
async function saveChanges() {
if (!content) return
setSaving(true)
setMessage(null)
try {
const res = await fetch('/api/website/content', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-admin-key': ADMIN_KEY,
},
headers: { 'Content-Type': 'application/json', 'x-admin-key': ADMIN_KEY },
body: JSON.stringify(content),
})
if (res.ok) {
setMessage({ type: 'success', text: 'Gespeichert!' })
} else {
@@ -109,16 +99,11 @@ export default function UebersetzungenPage() {
}
}
// Hero Section update
function updateHero(field: keyof HeroContent, value: string) {
if (!content) return
setContent({
...content,
hero: { ...content.hero, [field]: value },
})
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]
@@ -126,7 +111,6 @@ export default function UebersetzungenPage() {
setContent({ ...content, features: newFeatures })
}
// FAQ update
function updateFAQ(index: number, field: 'question' | 'answer', value: string | string[]) {
if (!content) return
const newFAQ = [...content.faq]
@@ -138,23 +122,16 @@ export default function UebersetzungenPage() {
setContent({ ...content, faq: newFAQ })
}
// Add FAQ
function addFAQ() {
if (!content) return
setContent({
...content,
faq: [...content.faq, { question: 'Neue Frage?', answer: ['Antwort hier...'] }],
})
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 })
setContent({ ...content, faq: content.faq.filter((_, i) => i !== index) })
}
// Pricing update
function updatePricing(index: number, field: string, value: string | number | boolean) {
if (!content) return
const newPricing = [...content.pricing]
@@ -165,21 +142,9 @@ export default function UebersetzungenPage() {
} 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'),
},
}
newPricing[index] = { ...newPricing[index], features: { ...newPricing[index].features, included: value.split('\n') } }
} else {
newPricing[index] = {
...newPricing[index],
features: {
...newPricing[index].features,
[subField]: value,
},
}
newPricing[index] = { ...newPricing[index], features: { ...newPricing[index].features, [subField]: value } }
}
} else {
newPricing[index] = { ...newPricing[index], [field]: value }
@@ -209,13 +174,10 @@ export default function UebersetzungenPage() {
<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">Uebersetzungen</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'
showPreview ? 'bg-blue-100 text-blue-700' : 'bg-slate-100 text-slate-600 hover:bg-slate-200'
}`}
title={showPreview ? 'Preview ausblenden' : 'Preview einblenden'}
>
@@ -228,13 +190,9 @@ export default function UebersetzungenPage() {
</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'
}`}
>
<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>
)}
@@ -256,9 +214,7 @@ export default function UebersetzungenPage() {
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'
activeTab === tab ? 'bg-white text-slate-900 shadow-sm' : 'text-slate-600 hover:text-slate-900'
}`}
>
{tab === 'hero' && 'Hero'}
@@ -275,494 +231,15 @@ export default function UebersetzungenPage() {
<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>
)}
{activeTab === 'hero' && <HeroEditor content={content} updateHero={updateHero} />}
{activeTab === 'features' && <FeaturesEditor content={content} updateFeature={updateFeature} />}
{activeTab === 'faq' && <FAQEditor content={content} updateFAQ={updateFAQ} addFAQ={addFAQ} removeFAQ={removeFAQ} />}
{activeTab === 'pricing' && <PricingEditor content={content} updatePricing={updatePricing} />}
{activeTab === 'other' && <OtherEditor content={content} setContent={setContent} />}
</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&section=${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>
)}
{showPreview && <PreviewPanel activeTab={activeTab} iframeRef={iframeRef} />}
</div>
</div>
)