diff --git a/admin-v2/app/(admin)/website/manager/page.tsx b/admin-v2/app/(admin)/website/manager/page.tsx new file mode 100644 index 0000000..db784ab --- /dev/null +++ b/admin-v2/app/(admin)/website/manager/page.tsx @@ -0,0 +1,670 @@ +'use client' + +/** + * Website Manager - CMS Dashboard + * + * Visual CMS dashboard for the BreakPilot website (macmini:3000). + * 60/40 split: Section cards with inline editors | Live iframe preview. + * Status bar, content stats, reset, save. + */ + +import { useState, useEffect, useRef, useCallback } from 'react' +import type { + WebsiteContent, + HeroContent, + FeatureContent, + FAQItem, + PricingPlan, +} from '@/lib/content-types' + +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' }, + { key: 'faq', name: 'FAQ', icon: '❓', scrollTo: 'faq' }, + { key: 'pricing', name: 'Pricing', icon: '💰', scrollTo: 'pricing' }, + { key: 'trust', name: 'Trust Indicators', icon: '🛡️', scrollTo: 'trust' }, + { key: 'testimonial', name: 'Testimonial', icon: '💬', scrollTo: 'trust' }, +] as const + +type SectionKey = (typeof SECTIONS)[number]['key'] + +// ─── Helpers ─────────────────────────────────────────────────────────────── + +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) + } +} + +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)}..."` + } +} + +// ─── Main Component ──────────────────────────────────────────────────────── + +export default function WebsiteManagerPage() { + const [content, setContent] = useState(null) + const [originalContent, setOriginalContent] = useState(null) + const [loading, setLoading] = useState(true) + const [saving, setSaving] = useState(false) + const [message, setMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null) + const [expandedSection, setExpandedSection] = useState(null) + const [websiteStatus, setWebsiteStatus] = useState<{ online: boolean; responseTime: number } | null>(null) + const iframeRef = useRef(null) + + // Load content + useEffect(() => { + loadContent() + checkWebsiteStatus() + }, []) + + // Auto-dismiss messages + 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) + } + } + + 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 }) + } + } + + 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: '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) + } + } + + function resetContent() { + 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 + } + }, []) + + 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) + } + } + + // ─── Render ──────────────────────────────────────────────────────────────── + + if (loading) { + return ( +
+
+ + + + + Lade Website-Content... +
+
+ ) + } + + if (!content) { + return ( +
+
Content konnte nicht geladen werden.
+
+ ) + } + + const wordCount = countWords(content) + const completeSections = SECTIONS.filter(s => sectionComplete(content, s.key)).length + const completionPct = Math.round((completeSections / SECTIONS.length) * 100) + const hasChanges = JSON.stringify(content) !== JSON.stringify(originalContent) + + return ( +
+ {/* ── Status Bar ───────────────────────────────────────────────────── */} +
+
+ {/* Website status */} +
+ + + Website {websiteStatus?.online ? 'online' : 'offline'} + {websiteStatus?.online && websiteStatus.responseTime > 0 && ( + ({websiteStatus.responseTime}ms) + )} + +
+ {/* Link */} + + Zur Website + + + + +
+
+ {message && ( + + {message.text} + + )} + + +
+
+ + {/* ── Stats Bar ────────────────────────────────────────────────────── */} +
+ {[ + { label: 'Sektionen', value: `${SECTIONS.length}`, icon: '📄' }, + { label: 'Woerter', value: wordCount.toLocaleString('de-DE'), icon: '📝' }, + { label: 'Vollstaendig', value: `${completionPct}%`, icon: completionPct === 100 ? '✅' : '🔧' }, + { label: 'Aenderungen', value: hasChanges ? 'Ungespeichert' : 'Aktuell', icon: hasChanges ? '🟡' : '🟢' }, + ].map((stat) => ( +
+ {stat.icon} +
+
{stat.value}
+
{stat.label}
+
+
+ ))} +
+ + {/* ── Main Layout: 60/40 ───────────────────────────────────────────── */} +
+ {/* ── Left: Section Cards (3/5 = 60%) ──────────────────────────── */} +
+ {SECTIONS.map((section) => { + const isExpanded = expandedSection === section.key + const isComplete = sectionComplete(content, section.key) + return ( +
+ {/* Card Header */} + + + {/* Inline Editor */} + {isExpanded && ( +
+ {section.key === 'hero' && } + {section.key === 'features' && } + {section.key === 'faq' && } + {section.key === 'pricing' && } + {section.key === 'trust' && } + {section.key === 'testimonial' && } +
+ )} +
+ ) + })} +
+ + {/* ── Right: Live Preview (2/5 = 40%) ──────────────────────────── */} +
+ {/* Preview Header */} +
+
+
+
+
+
+
+ macmini:3000 +
+ +
+ {/* iframe */} +
+