Add public Foerderantrag (funding application) pages to the website. Add website management section to admin-v2 with content and status APIs. Extend Header, LandingContent, and i18n with new navigation entries. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
770 lines
32 KiB
TypeScript
770 lines
32 KiB
TypeScript
'use client'
|
|
|
|
/**
|
|
* Uebersetzungen - Website Content Editor
|
|
*
|
|
* 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 UebersetzungenPage() {
|
|
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/website/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/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: '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">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'
|
|
}`}
|
|
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>
|
|
)
|
|
}
|