Compare commits

...

5 Commits

Author SHA1 Message Date
Daniel S
950497311c fix: enforce strict draft vs publish isolation on all editor API endpoints
All checks were successful
Production Build & Deploy / build-and-deploy (push) Successful in 46s
2026-08-09 23:55:16 +02:00
Daniel S
28473bbcdc feat: rebuild admin editor into full Elementor-style 3-tab inspector and responsive canvas 2026-08-09 23:52:51 +02:00
Daniel S
fc2017b210 feat: enable full section editing, background styling, animations and live file persistence 2026-08-09 23:50:33 +02:00
Daniel S
831a0d53f0 refactor: simplify editor actions into single direct save button 2026-08-09 23:48:02 +02:00
Daniel S
9284717a80 fix: update editor bridge syntax and refine demo config content 2026-08-09 23:46:22 +02:00
7 changed files with 519 additions and 305 deletions

View File

@@ -1,10 +1,25 @@
{ {
"updated_at": "2026-08-09T21:48:17.969Z",
"siteName": "N&D IT Solutions", "siteName": "N&D IT Solutions",
"metaDescription": "Webdesign & Digital Solutions", "metaDescription": "High-Performance Webdesign & Digital Solutions - 100% DSGVO-konform.",
"is_demo_mode": false,
"header": {
"brandName": "N&D",
"brandHighlight": "i-t SOLUTIONS",
"ctaText": "Erstgespräch buchen",
"ctaLink": "#contact",
"showThemeToggle": true,
"glassEffect": true
},
"footer": {
"brandDescription": "Ihr Partner für professionelles Webdesign, Website-Entwicklung und lokale SEO für Kleinbetriebe & KMU.",
"copyrightText": "N&D i-t SOLUTIONS. Alle Rechte vorbehalten.",
"showLegalLinks": true
},
"hero": { "hero": {
"badge": "WaaS", "badge": "š Waas Enterprise Baukastensystem",
"title": "Maßgeschneiderte Webseiten. Digital im Griff.", "title": "Maßgeschneiderte Webseiten. Digital im Griff.",
"subtitle": "Wir machen Zeugs", "subtitle": "Wir entwickeln blitzschnelle, DSGVO-konforme High-End Webseiten für Ihr Unternehmen zum monatlichen Festpreis inklusive Wartung & Hosting.",
"ctaPrimaryText": "Projekt anfragen", "ctaPrimaryText": "Projekt anfragen",
"ctaPrimaryLink": "#contact", "ctaPrimaryLink": "#contact",
"ctaSecondaryText": "Mehr erfahren", "ctaSecondaryText": "Mehr erfahren",
@@ -12,11 +27,13 @@
"elements": [ "elements": [
{ {
"id": "el-1", "id": "el-1",
"slotId": "hero-badge-slot",
"type": "badge", "type": "badge",
"content": "š WaaS Engine" "content": "WaaS Engine"
}, },
{ {
"id": "el-2", "id": "el-2",
"slotId": "hero-actions-slot",
"type": "button", "type": "button",
"content": "Neuer Button", "content": "Neuer Button",
"link": "#contact", "link": "#contact",
@@ -48,7 +65,7 @@
{ {
"id": "card-3", "id": "card-3",
"title": "Liquid Glass UI", "title": "Liquid Glass UI",
"description": "Moderne Ästhetik mit matte Boxen, Aurora Glows und physischen Kanten-Effekten.", "description": "Moderne Ästhetik mit matte Boxen, Aurora Glows und physikalischen Kanten-Effekten.",
"colSpan": 1, "colSpan": 1,
"badge": "Design", "badge": "Design",
"icon": "layers" "icon": "layers"
@@ -71,9 +88,9 @@
"address": "Deutschland" "address": "Deutschland"
}, },
"sectionsOrder": [ "sectionsOrder": [
"contact", "hero",
"bento", "bento",
"hero" "contact"
], ],
"sectionsDetails": [ "sectionsDetails": [
{ {
@@ -83,8 +100,9 @@
"elements": [ "elements": [
{ {
"id": "el-1", "id": "el-1",
"slotId": "hero-badge-slot",
"type": "badge", "type": "badge",
"content": "š WaaS Engine" "content": "WaaS Engine"
} }
] ]
}, },
@@ -100,5 +118,53 @@
"title": "Contact Section", "title": "Contact Section",
"elements": [] "elements": []
} }
],
"site_info": {
"title": "N&D IT Solutions",
"homepage_id": "page_home",
"email": "",
"phone": "",
"address": ""
},
"pages": [
{
"id": "page_home",
"slug": "/",
"title": "Startseite (Hauptseite)",
"is_published": true,
"sections": []
},
{
"id": "page_templates",
"slug": "/templates",
"title": "Templates & Design-Muster",
"is_published": true,
"sections": [
{
"id": "hero_templates",
"type": "HeroSection",
"settings": {
"title": "Templates",
"subtitle": "Unsere fertigen Design-Vorlagen"
}
}
]
},
{
"id": "page_privacy",
"slug": "/datenschutz",
"title": "Datenschutzerklärung",
"is_published": true,
"sections": [
{
"id": "privacy_content",
"type": "HeroSection",
"settings": {
"title": "Datenschutz",
"subtitle": "100% DSGVO-konforme Datenverarbeitung."
}
}
]
}
] ]
} }

View File

@@ -1,7 +1,9 @@
{ {
"activePreset": "amberGlow", "activePreset": "midnightBlue",
"customColors": { "customColors": {
"bg": "#0B0F19", "bg": "#0b0f19",
"accent": "#38BDF8" "accent": "#38bdf8",
"text": "#e0e0e0",
"border": ""
} }
} }

View File

@@ -13,7 +13,8 @@
banner.style.cssText = 'position: fixed; top: 0; left: 0; right: 0; z-index: 99999; background: #d97706; color: #0f172a; text-align: center; padding: 6px 12px; font-size: 12px; font-weight: 700; font-family: monospace; letter-spacing: 0.5px; box-shadow: 0 2px 10px rgba(0,0,0,0.3); pointer-events: none; opacity: 0.95;'; banner.style.cssText = 'position: fixed; top: 0; left: 0; right: 0; z-index: 99999; background: #d97706; color: #0f172a; text-align: center; padding: 6px 12px; font-size: 12px; font-weight: 700; font-family: monospace; letter-spacing: 0.5px; box-shadow: 0 2px 10px rgba(0,0,0,0.3); pointer-events: none; opacity: 0.95;';
banner.innerHTML = '🛠️ BEARBEITUNGSMODUS (VORSCHAU / ENTWURF) — Änderungen sind noch nicht live!'; banner.innerHTML = '🛠️ BEARBEITUNGSMODUS (VORSCHAU / ENTWURF) — Änderungen sind noch nicht live!';
document.body.appendChild(banner); document.body.appendChild(banner);
document.body.style.paddingTop = (document.body.style.paddingTop ? parseInt(document.body.style.paddingTop) + 30 : 30) + 'px'; const currentPadding = parseInt(document.body.style.paddingTop || '0', 10);
document.body.style.paddingTop = (currentPadding + 30) + 'px';
}); });
window.addEventListener('message', (event) => { window.addEventListener('message', (event) => {

View File

@@ -5,18 +5,22 @@ interface Props {
config: SiteConfig['bento']; config: SiteConfig['bento'];
} }
const { config } = Astro.props; const props = Astro.props;
const config = props.config || props.settings || props;
const sectionTitle = config.sectionTitle || config.title || 'Unsere Leistungen';
const sectionSubtitle = config.sectionSubtitle || config.subtitle || '';
const cards = config.cards || config.items || [];
--- ---
<section id="services" class={`py-20 px-4 sm:px-6 lg:px-8 max-w-7xl mx-auto relative ${config.animation ? `anim-${config.animation}` : ''}`} style={(config as any).bg_color ? `background-color: ${(config as any).bg_color};` : ''}> <section id="services" class={`py-20 px-4 sm:px-6 lg:px-8 max-w-7xl mx-auto relative ${config.animation ? `anim-${config.animation}` : ''}`} style={(config as any).bg_color ? `background-color: ${(config as any).bg_color};` : ''}>
<div class="text-center max-w-3xl mx-auto mb-16 space-y-3"> <div class="text-center max-w-3xl mx-auto mb-16 space-y-3">
<h2 class="text-3xl font-extrabold text-white tracking-tight uppercase">{config.sectionTitle}</h2> <h2 class="text-3xl font-extrabold text-white tracking-tight uppercase">{sectionTitle}</h2>
<p class="text-slate-400 text-xs sm:text-sm">{config.sectionSubtitle}</p> <p class="text-slate-400 text-xs sm:text-sm">{sectionSubtitle}</p>
</div> </div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-6"> <div class="grid grid-cols-1 md:grid-cols-3 gap-6">
{config.cards.map((card) => ( {cards.map((card: any) => (
<div class={`relative glass-panel glass-panel-hover rounded-2xl p-8 flex flex-col justify-between overflow-hidden group ${card.colSpan === 2 ? 'md:col-span-2' : 'md:col-span-1'} ${card.animation ? `anim-${card.animation}` : ''}`}> <div class={`relative glass-panel glass-panel-hover rounded-2xl p-8 flex flex-col justify-between overflow-hidden group ${card.colSpan === 2 ? 'md:col-span-2' : 'md:col-span-1'} ${card.animation ? `anim-${card.animation}` : ''}`}>
<div class="space-y-4 relative z-10"> <div class="space-y-4 relative z-10">
{card.badge && ( {card.badge && (

View File

@@ -6,15 +6,19 @@ interface Props {
smtpConfig: SmtpConfig; smtpConfig: SmtpConfig;
} }
const { contactConfig, smtpConfig } = Astro.props; const props = Astro.props;
const contactConfig = props.contactConfig || props.settings || props;
const smtpConfig = props.smtpConfig || { isConfigured: false };
const title = contactConfig.title || 'Kontaktieren Sie uns';
const subtitle = contactConfig.subtitle || 'Wir freuen uns auf Ihre Nachricht.';
--- ---
<section id="contact" class="py-20 px-4 sm:px-6 lg:px-8 max-w-4xl mx-auto relative" style={(contactConfig as any).bg_color ? `background-color: ${(contactConfig as any).bg_color};` : ''}> <section id="contact" class="py-20 px-4 sm:px-6 lg:px-8 max-w-4xl mx-auto relative" style={(contactConfig as any).bg_color ? `background-color: ${(contactConfig as any).bg_color};` : ''}>
<div class="glass-panel rounded-3xl p-8 sm:p-12 relative overflow-hidden"> <div class="glass-panel rounded-3xl p-8 sm:p-12 relative overflow-hidden">
<div class="text-center space-y-3 mb-10"> <div class="text-center space-y-3 mb-10">
<h2 class="text-3xl font-extrabold text-white tracking-tight uppercase">{contactConfig.title}</h2> <h2 class="text-3xl font-extrabold text-white tracking-tight uppercase">{title}</h2>
<p class="text-slate-400 text-xs sm:text-sm">{contactConfig.subtitle}</p> <p class="text-slate-400 text-xs sm:text-sm">{subtitle}</p>
</div> </div>
{!smtpConfig.isConfigured && ( {!smtpConfig.isConfigured && (

View File

@@ -7,11 +7,13 @@ interface Props {
contextData?: Record<string, any>; contextData?: Record<string, any>;
} }
const { config, contextData = {} } = Astro.props; const props = Astro.props;
const config = props.config || props.settings || props;
const contextData = props.contextData || {};
const heroTitle = resolveDynamicBinding(config.title, contextData); const heroTitle = resolveDynamicBinding(config.title || config.heroTitle || 'Willkommen', contextData);
const heroSubtitle = resolveDynamicBinding(config.subtitle, contextData); const heroSubtitle = resolveDynamicBinding(config.subtitle || config.heroSubtitle || '', contextData);
const heroBadge = resolveDynamicBinding(config.badge, contextData); const heroBadge = resolveDynamicBinding(config.badge || config.heroBadge || '', contextData);
const elements = config.elements || []; const elements = config.elements || [];
const badgeElements = elements.filter((el: UIComponent) => el.slotId === 'hero-badge-slot'); const badgeElements = elements.filter((el: UIComponent) => el.slotId === 'hero-badge-slot');

View File

@@ -14,359 +14,494 @@ const pageId = searchParams.get('page_id');
const userAgent = Astro.request.headers.get('user-agent') ?? ''; const userAgent = Astro.request.headers.get('user-agent') ?? '';
const { siteConfig: draftConfig } = loadWaasConfigs(userAgent, true); const { siteConfig: draftConfig } = loadWaasConfigs(userAgent, true);
const pages = (draftConfig as any).pages || []; const pages = (draftConfig as any).pages || [];
const targetPage = pages.find((p: any) => p.id === pageId) || pages[0]; const targetPage = pages.find((p: any) => p.id === pageId) || pages[0] || { id: 'page_home', title: 'Startseite', slug: '/', sections: [] };
const previewUrl = `${targetPage?.slug || '/'}?preview=draft`; const previewUrl = `${targetPage?.slug || '/'}?preview=draft`;
--- ---
<Layout title={`Editor: ${targetPage?.title || 'Baukasten'} | N&D Admin`}> <Layout title={`Elementor Editor: ${targetPage?.title || 'Baukasten'} | N&D Admin`}>
<AdminSidebar currentPath="/admin/editor" /> <AdminSidebar currentPath="/admin/editor" />
<div class="h-screen w-screen flex overflow-hidden bg-slate-950 font-sans pl-12">
<!-- Main Container mit Elementor Dark Theme UI (#1e1e1e, #262626, Elementor Pink #e0436b & Sky Blue #38bdf8) -->
<div class="h-screen w-screen flex overflow-hidden bg-[#121212] font-sans pl-12 text-slate-200 selection:bg-[#e0436b] selection:text-white">
<aside class="w-80 border-r border-slate-800 bg-slate-900 flex flex-col justify-between z-20"> <!-- LINKES ELEMENTOR PANEL (360px - 400px) -->
<aside id="elementor-panel" class="w-[380px] min-w-[380px] max-w-[380px] bg-[#1e1e1e] border-r border-[#2d2d2d] flex flex-col justify-between z-30 shadow-2xl relative">
<div class="p-4 space-y-6 overflow-y-auto"> <!-- 1. HEADER LEISTE -->
<div class="flex items-center justify-between border-b border-slate-800 pb-4"> <div class="h-12 bg-[#181818] border-b border-[#2d2d2d] flex items-center justify-between px-3">
<div class="flex items-center gap-2">
<a href="/admin/pages" class="p-1.5 rounded hover:bg-[#2a2a2a] text-slate-400 hover:text-white transition-colors" title="Zurück zum Admin-Dashboard">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"/></svg>
</a>
<button id="btn-open-widgets" class="p-1.5 rounded hover:bg-[#2a2a2a] text-[#e0436b] transition-colors" title="Widget-Bibliothek">
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 24 24"><path d="M4 4h4v4H4V4zm6 0h4v4h-4V4zm6 0h4v4h-4V4zM4 10h4v4H4v-4zm6 0h4v4h-4v-4zm6 0h4v4h-4v-4zM4 16h4v4H4v-4zm6 0h4v4h-4v-4zm6 0h4v4h-4v-4z"/></svg>
</button>
<span class="text-xs font-bold truncate max-w-[170px] text-slate-200" title={targetPage.title}>
{targetPage.title}
</span>
</div>
<div class="flex items-center gap-1">
<span id="inspector-active-badge" class="hidden text-[10px] font-mono font-semibold px-2 py-0.5 rounded bg-[#e0436b]/20 text-[#e0436b] border border-[#e0436b]/30">
Inspector
</span>
</div>
</div>
<!-- 2. HAUPTBEREICH (DYNAMISCHES PANEL) -->
<div class="flex-1 overflow-y-auto custom-scrollbar relative bg-[#1e1e1e]">
<!-- STATE A: WIDGET-DRAWER (Katalog & Sektions-Übersicht) -->
<div id="panel-state-widgets" class="p-4 space-y-6">
<!-- Sektionen verwalten -->
<div> <div>
<a href="/admin/pages" class="text-xs text-slate-400 hover:text-sky-400 transition-colors">← Zurück zur Übersicht</a> <div class="flex items-center justify-between mb-3">
<h1 class="text-base font-bold text-white mt-1">{targetPage?.title || 'Seite bearbeiten'}</h1> <h2 class="text-xs font-bold uppercase tracking-wider text-slate-400">Sektionen auf Seite</h2>
<span class="text-[10px] bg-[#2a2a2a] text-slate-400 px-2 py-0.5 rounded font-mono">{targetPage?.sections?.length || 0}</span>
</div>
<div id="section-list" class="space-y-2">
{targetPage?.sections?.map((sec: any, index: number) => (
<div
class="group p-3 bg-[#262626] hover:bg-[#2d2d2d] border border-[#333] hover:border-[#e0436b]/50 rounded-lg text-xs flex items-center justify-between transition-all cursor-pointer shadow-sm"
data-section-id={sec.id}
data-section-type={sec.type}
data-settings={JSON.stringify(sec.settings || {})}
>
<div class="flex items-center gap-2.5 min-w-0 flex-1">
<span class="text-[10px] font-mono text-[#e0436b] font-bold">#{index + 1}</span>
<span class="font-semibold text-slate-200 truncate">{sec.type}</span>
</div>
<div class="flex items-center gap-1 opacity-80 group-hover:opacity-100">
<button class="btn-inspect-section p-1 hover:text-[#38bdf8] transition-colors" title="Im 3-Tab-Inspector öffnen">✏️</button>
<button class="btn-move-up p-1 hover:text-[#38bdf8] transition-colors" title="Nach oben">⬆️</button>
<button class="btn-move-down p-1 hover:text-[#38bdf8] transition-colors" title="Nach unten">⬇️</button>
<button class="btn-delete-section p-1 hover:text-red-400 transition-colors" title="Sektion löschen">🗑️</button>
</div>
</div>
))}
</div>
</div> </div>
</div>
<div class="space-y-2"> <!-- Sektion / Widget hinzufügen -->
<label class="text-xs font-semibold text-slate-400 uppercase tracking-wider block">Farb-Auswahl</label> <div class="pt-4 border-t border-[#2d2d2d] space-y-3">
<select id="preset-selector" class="w-full bg-slate-800 border border-slate-700 rounded-lg p-2.5 text-xs text-white focus:outline-none focus:border-sky-500"> <h2 class="text-xs font-bold uppercase tracking-wider text-slate-400">Widget-Bibliothek (Neue Sektion)</h2>
{Object.entries(THEME_PRESETS).map(([key, preset]) => ( <div class="grid grid-cols-2 gap-2.5">
<option value={key}>{preset.name}</option> <button data-widget-type="HeroSection" class="btn-quick-add-widget p-3 bg-[#262626] hover:bg-[#2d2d2d] border border-[#333] hover:border-[#38bdf8] rounded-xl flex flex-col items-center gap-2 text-center transition-all group">
))} <span class="text-xl group-hover:scale-110 transition-transform">🚀</span>
</select> <span class="text-xs font-bold text-slate-200">Hero Section</span>
</div> </button>
<button data-widget-type="BentoGrid" class="btn-quick-add-widget p-3 bg-[#262626] hover:bg-[#2d2d2d] border border-[#333] hover:border-[#38bdf8] rounded-xl flex flex-col items-center gap-2 text-center transition-all group">
<span class="text-xl group-hover:scale-110 transition-transform">🍱</span>
<div class="space-y-2"> <span class="text-xs font-bold text-slate-200">Bento Grid</span>
<label class="text-xs font-semibold text-slate-400 uppercase tracking-wider block">Farbschema</label> </button>
<div class="grid grid-cols-2 gap-2"> <button data-widget-type="ContactSection" class="btn-quick-add-widget p-3 bg-[#262626] hover:bg-[#2d2d2d] border border-[#333] hover:border-[#38bdf8] rounded-xl flex flex-col items-center gap-2 text-center transition-all group">
<button id="btn-mode-dark" class="px-3 py-2 bg-slate-800 border border-slate-700 rounded-lg text-xs font-bold text-white hover:border-sky-500 transition-colors">🌙 Dark Mode</button> <span class="text-xl group-hover:scale-110 transition-transform">📬</span>
<button id="btn-mode-light" class="px-3 py-2 bg-slate-800 border border-slate-700 rounded-lg text-xs font-bold text-slate-300 hover:border-sky-500 transition-colors">☀️ Light Mode</button> <span class="text-xs font-bold text-slate-200">Kontakt Formular</span>
</button>
</div>
</div> </div>
</div> </div>
<div class="space-y-2 pt-4 border-t border-slate-800"> <!-- STATE B: 3-TAB-INSPEKTORMENÜ (Elementor Inspector) -->
<label class="text-xs font-semibold text-slate-400 uppercase tracking-wider block">Sektion hinzufügen</label> <div id="panel-state-inspector" class="hidden flex flex-col h-full">
<div class="flex gap-2">
<select id="select-new-section-type" class="flex-1 bg-slate-800 border border-slate-700 rounded-lg p-2 text-xs text-white focus:outline-none focus:border-sky-500"> <!-- TAB HEADER -->
<option value="HeroSection">Hero Section</option> <div class="flex border-b border-[#2d2d2d] bg-[#181818]">
<option value="BentoGrid">Bento Grid</option> <button id="tab-btn-content" class="flex-1 py-3 text-xs font-bold text-[#e0436b] border-b-2 border-[#e0436b] transition-all flex items-center justify-center gap-1.5">
<option value="ContactSection">Kontakt Formular</option> <span>📝</span> Inhalt
</select> </button>
<button id="btn-add-section" class="bg-sky-500 hover:bg-sky-400 text-slate-950 font-bold px-3 py-2 rounded-lg text-xs transition-colors"> <button id="tab-btn-style" class="flex-1 py-3 text-xs font-bold text-slate-400 hover:text-slate-200 border-b-2 border-transparent transition-all flex items-center justify-center gap-1.5">
+ Hinzufügen <span>🎨</span> Stil
</button>
<button id="tab-btn-advanced" class="flex-1 py-3 text-xs font-bold text-slate-400 hover:text-slate-200 border-b-2 border-transparent transition-all flex items-center justify-center gap-1.5">
<span>⚙️</span> Erweitert
</button> </button>
</div> </div>
</div>
<div class="space-y-2 pt-4 border-t border-slate-800"> <!-- TAB INHALTE -->
<label class="text-xs font-semibold text-slate-400 uppercase tracking-wider block">Sektionen verwalten</label> <form id="form-elementor-inspector" class="p-4 flex-1 space-y-6 overflow-y-auto">
<div id="section-list" class="space-y-2"> <input type="hidden" id="active-section-id" name="section_id" />
{targetPage?.sections?.map((sec: any, index: number) => (
<div <!-- TAB 1: INHALT -->
class="p-3 bg-slate-800/80 border border-slate-700/60 rounded-lg text-xs space-y-2 text-slate-200" <div id="tab-panel-content" class="space-y-4">
data-section-id={sec.id} <div class="flex items-center justify-between pb-2 border-b border-[#2d2d2d]">
data-section-type={sec.type} <span class="text-xs font-bold text-slate-300">Text- & Medienfelder</span>
data-settings={JSON.stringify(sec.settings || {})} <button type="button" id="btn-[#dynamic-picker-trigger]" class="text-[10px] font-mono text-[#38bdf8] bg-[#38bdf8]/10 hover:bg-[#38bdf8]/20 px-2 py-0.5 rounded border border-[#38bdf8]/30 flex items-center gap-1">
> 🛢️ Dynamic Data
<div class="flex items-center justify-between font-semibold"> </button>
<span class="text-sky-400">#{index + 1} {sec.type}</span> </div>
<div class="flex items-center gap-1">
<button class="btn-edit-section hover:text-sky-400 px-1" title="Inhalte/Einstellungen bearbeiten">✏️</button> <div>
<button class="btn-move-up hover:text-sky-400 px-1" title="Nach oben">⬆️</button> <label class="block text-[11px] font-bold text-slate-400 uppercase tracking-wider mb-1">Überschrift (Title)</label>
<button class="btn-move-down hover:text-sky-400 px-1" title="Nach unten">⬇️</button> <div class="relative">
<button class="btn-delete-section hover:text-red-400 px-1" title="Sektion löschen">🗑️</button> <input type="text" id="field-title" name="title" class="w-full bg-[#262626] border border-[#333] focus:border-[#e0436b] rounded-lg p-2.5 text-xs text-white outline-none transition-colors" />
</div>
</div>
<div>
<label class="block text-[11px] font-bold text-slate-400 uppercase tracking-wider mb-1">Untertitel (Subtitle)</label>
<textarea id="field-subtitle" name="subtitle" rows="3" class="w-full bg-[#262626] border border-[#333] focus:border-[#e0436b] rounded-lg p-2.5 text-xs text-white outline-none transition-colors resize-none"></textarea>
</div>
<div>
<label class="block text-[11px] font-bold text-slate-400 uppercase tracking-wider mb-1">Button Text / CTA</label>
<input type="text" id="field-cta-text" name="cta_text" class="w-full bg-[#262626] border border-[#333] focus:border-[#e0436b] rounded-lg p-2.5 text-xs text-white outline-none transition-colors" />
</div>
<div>
<label class="block text-[11px] font-bold text-slate-400 uppercase tracking-wider mb-1">Bild-URL / Medien</label>
<input type="text" id="field-image-url" name="image_url" placeholder="https://..." class="w-full bg-[#262626] border border-[#333] focus:border-[#e0436b] rounded-lg p-2.5 text-xs text-white outline-none transition-colors" />
</div>
</div>
<!-- TAB 2: STIL -->
<div id="tab-panel-style" class="hidden space-y-5">
<div class="pb-2 border-b border-[#2d2d2d]">
<span class="text-xs font-bold text-slate-300">Farben & Effekte</span>
</div>
<div>
<label class="block text-[11px] font-bold text-slate-400 uppercase tracking-wider mb-1">Hintergrundfarbe (bg_color)</label>
<div class="flex gap-2">
<input type="color" id="field-bg-color-picker" class="h-9 w-12 rounded bg-[#262626] border border-[#333] cursor-pointer p-0.5" />
<input type="text" id="field-bg-color" name="bg_color" placeholder="#0b0f19" class="flex-1 bg-[#262626] border border-[#333] focus:border-[#e0436b] rounded-lg p-2 text-xs text-white outline-none" />
</div>
</div>
<div>
<label class="block text-[11px] font-bold text-slate-400 uppercase tracking-wider mb-1">Akzentfarbe (accent_color)</label>
<div class="flex gap-2">
<input type="color" id="field-accent-color-picker" class="h-9 w-12 rounded bg-[#262626] border border-[#333] cursor-pointer p-0.5" />
<input type="text" id="field-accent-color" name="accent_color" placeholder="#38bdf8" class="flex-1 bg-[#262626] border border-[#333] focus:border-[#e0436b] rounded-lg p-2 text-xs text-white outline-none" />
</div>
</div>
<div>
<div class="flex items-center justify-between mb-1">
<label class="text-[11px] font-bold text-slate-400 uppercase tracking-wider">Backdrop Blur</label>
<span id="blur-val" class="text-xs font-mono text-[#38bdf8]">12px</span>
</div>
<input type="range" id="field-backdrop-blur" name="backdrop_blur" min="0" max="24" value="12" class="w-full accent-[#e0436b]" />
</div>
<div class="flex items-center justify-between p-3 bg-[#262626] rounded-xl border border-[#333]">
<span class="text-xs font-semibold text-slate-300">1px Liquid Edge Border</span>
<input type="checkbox" id="field-liquid-edge" name="liquid_edge" class="w-4 h-4 accent-[#e0436b] cursor-pointer" />
</div>
</div>
<!-- TAB 3: ERWEITERT -->
<div id="tab-panel-advanced" class="hidden space-y-5">
<div class="pb-2 border-b border-[#2d2d2d]">
<span class="text-xs font-bold text-slate-300">Abstände & Animationen</span>
</div>
<div>
<label class="block text-[11px] font-bold text-slate-400 uppercase tracking-wider mb-2">Innenabstände (Padding)</label>
<div class="grid grid-cols-4 gap-2 text-center">
<div>
<span class="text-[10px] text-slate-500 block mb-1">Oben</span>
<input type="text" id="field-padding-top" name="padding_top" placeholder="24px" class="w-full bg-[#262626] border border-[#333] rounded-lg p-1.5 text-xs text-white text-center outline-none" />
</div>
<div>
<span class="text-[10px] text-slate-500 block mb-1">Rechts</span>
<input type="text" id="field-padding-right" name="padding_right" placeholder="16px" class="w-full bg-[#262626] border border-[#333] rounded-lg p-1.5 text-xs text-white text-center outline-none" />
</div>
<div>
<span class="text-[10px] text-slate-500 block mb-1">Unten</span>
<input type="text" id="field-padding-bottom" name="padding_bottom" placeholder="24px" class="w-full bg-[#262626] border border-[#333] rounded-lg p-1.5 text-xs text-white text-center outline-none" />
</div>
<div>
<span class="text-[10px] text-slate-500 block mb-1">Links</span>
<input type="text" id="field-padding-left" name="padding_left" placeholder="16px" class="w-full bg-[#262626] border border-[#333] rounded-lg p-1.5 text-xs text-white text-center outline-none" />
</div> </div>
</div> </div>
</div> </div>
))}
</div> <div>
<label class="block text-[11px] font-bold text-slate-400 uppercase tracking-wider mb-1">Motion.dev Animation</label>
<select id="field-animation" name="animation" class="w-full bg-[#262626] border border-[#333] focus:border-[#e0436b] rounded-lg p-2.5 text-xs text-white outline-none">
<option value="">Keine Animation</option>
<option value="stagger-fade">stagger-fade (Schrittweise)</option>
<option value="spring-scale">spring-scale (Federnder Zoom)</option>
<option value="hover-pulse">hover-pulse (Interaktiv Puls)</option>
<option value="slide-up">slide-up (Von unten)</option>
</select>
</div>
<div class="space-y-2 pt-2">
<label class="block text-[11px] font-bold text-slate-400 uppercase tracking-wider">Sichtbarkeit (Responsive)</label>
<div class="flex items-center justify-between p-2.5 bg-[#262626] rounded-lg border border-[#333]">
<span class="text-xs text-slate-300">Auf Mobile ausblenden</span>
<input type="checkbox" id="field-hide-mobile" name="hide_mobile" class="accent-[#e0436b]" />
</div>
<div class="flex items-center justify-between p-2.5 bg-[#262626] rounded-lg border border-[#333]">
<span class="text-xs text-slate-300">Auf Desktop ausblenden</span>
<input type="checkbox" id="field-hide-desktop" name="hide_desktop" class="accent-[#e0436b]" />
</div>
</div>
</div>
</form>
</div> </div>
</div> </div>
<div class="p-4 border-t border-slate-800 bg-slate-950/50 space-y-2"> <!-- 3. BOTTOM-BAR (PRO-STEUERUNGSLEISTE) -->
<button id="btn-save-draft" class="w-full bg-slate-800 hover:bg-slate-700 text-slate-200 font-semibold py-2.5 rounded-lg text-xs transition-colors border border-slate-700"> <div class="h-14 bg-[#181818] border-t border-[#2d2d2d] px-3 flex items-center justify-between gap-2">
💾 Entwurf speichern <div class="flex items-center gap-1">
</button> <!-- ⚙️ Zahnrad: Seiteneinstellungen -->
<button id="btn-publish" class="w-full bg-sky-500 hover:bg-sky-400 text-slate-950 font-bold py-2.5 rounded-lg text-xs transition-colors shadow-lg shadow-sky-500/20"> <button id="btn-open-settings" class="p-2 rounded hover:bg-[#2a2a2a] text-slate-400 hover:text-white transition-colors" title="Seiteneinstellungen (SEO, Slug)">
🚀 Live Veröffentlichen ⚙️
</button> </button>
<!-- 👁️ Auge: Vorschau im neuen Tab -->
<a href={targetPage.slug || '/'} target="_blank" class="p-2 rounded hover:bg-[#2a2a2a] text-slate-400 hover:text-white transition-colors" title="Live Vorschau im Tab">
👁️
</a>
</div>
<!-- 📱 Responsive-Switcher -->
<div class="flex items-center bg-[#262626] rounded-lg p-1 border border-[#333]">
<button id="res-desktop" class="p-1 rounded text-[#38bdf8] hover:bg-[#333] transition-colors" title="Desktop View (100%)">🖥️</button>
<button id="res-tablet" class="p-1 rounded text-slate-400 hover:text-white hover:bg-[#333] transition-colors" title="Tablet View (768px)">💻</button>
<button id="res-mobile" class="p-1 rounded text-slate-400 hover:text-white hover:bg-[#333] transition-colors" title="Mobile View (375px)">📱</button>
</div>
<!-- SPEICHERN & VERÖFFENTLICHEN -->
<div class="flex items-center gap-1.5">
<button id="btn-save-draft" class="px-2.5 py-1.5 bg-[#262626] hover:bg-[#333] text-slate-200 font-bold rounded-lg text-xs transition-colors border border-[#333]" title="Entwurf speichern">
💾
</button>
<button id="btn-publish" class="px-3 py-1.5 bg-[#e0436b] hover:bg-[#c9365c] text-white font-bold rounded-lg text-xs transition-colors shadow-lg shadow-[#e0436b]/20 flex items-center gap-1">
🚀 Veröffentlichen
</button>
</div>
</div> </div>
</aside> </aside>
<main class="flex-1 bg-slate-950 flex flex-col relative"> <!-- RECHTER CANVAS-BEREICH (LIVE-IFRAME WORKSPACE) -->
<div class="h-10 bg-slate-900 border-b border-slate-800 flex items-center justify-between px-4 text-xs text-slate-400"> <main class="flex-1 bg-[#121212] flex flex-col relative items-center justify-start overflow-hidden p-4">
<span>Vorschau-Route: <code class="text-sky-400 font-mono">{previewUrl}</code></span>
<span id="editor-status-badge" class="text-amber-400 bg-amber-950/60 border border-amber-800/60 px-2 py-0.5 rounded text-[10px]">Entwurf aktiv</span> <div class="w-full h-8 bg-[#181818] border border-[#2d2d2d] rounded-t-xl flex items-center justify-between px-4 text-xs text-slate-400 mb-0">
<div class="flex items-center gap-2">
<span class="w-2.5 h-2.5 rounded-full bg-emerald-500"></span>
<span>Elementor Canvas: <code class="text-[#38bdf8] font-mono">{previewUrl}</code></span>
</div>
<span id="editor-status-badge" class="text-[#38bdf8] bg-[#38bdf8]/10 border border-[#38bdf8]/30 px-2 py-0.5 rounded text-[10px] font-mono">
Ready
</span>
</div> </div>
<iframe
id="preview-iframe" <!-- Live Canvas Iframe Wrapper -->
src={previewUrl} <div id="iframe-wrapper" class="w-full h-[calc(100%-2rem)] transition-all duration-300 flex justify-center items-center shadow-2xl rounded-b-xl overflow-hidden bg-slate-950 border border-[#2d2d2d]">
class="w-full h-full border-none bg-slate-950" <iframe
title="Live Website Preview" id="preview-iframe"
></iframe> src={previewUrl}
class="w-full h-full border-none transition-all duration-300"
title="Elementor Live Workspace"
></iframe>
</div>
</main> </main>
</div> </div>
<dialog id="modal-edit-section" class="bg-slate-900 text-white border border-slate-800 rounded-xl p-6 backdrop:bg-slate-950/80 max-w-lg w-full"> <!-- GLOBALE SEITENEINSTELLUNGEN MODAL -->
<form id="form-edit-section-settings" class="space-y-4"> <dialog id="modal-page-settings" class="bg-[#1e1e1e] text-white border border-[#333] rounded-2xl p-6 backdrop:bg-black/80 max-w-md w-full shadow-2xl">
<h2 class="text-lg font-bold text-white flex items-center justify-between"> <form id="form-page-settings" class="space-y-4">
<span>Sektion bearbeiten</span> <h2 class="text-base font-bold text-white flex items-center justify-between border-b border-[#2d2d2d] pb-3">
<span id="modal-section-type-badge" class="text-xs font-mono bg-sky-950 text-sky-400 border border-sky-800 px-2 py-0.5 rounded"></span> <span>⚙️ Seiteneinstellungen</span>
</h2> </h2>
<input type="hidden" id="edit-section-id" name="section_id" /> <div>
<label class="block text-xs font-bold text-slate-400 mb-1">Seitentitel</label>
<div id="dynamic-settings-fields" class="space-y-3"> <input type="text" name="title" value={targetPage.title} class="w-full bg-[#262626] border border-[#333] rounded-lg p-2 text-xs text-white focus:outline-none focus:border-[#e0436b]" />
</div> </div>
<div>
<div class="flex justify-end gap-2 pt-4 border-t border-slate-800"> <label class="block text-xs font-bold text-slate-400 mb-1">Slug (URL)</label>
<button type="button" id="btn-close-edit-modal" class="px-4 py-2 bg-slate-800 text-slate-300 rounded-lg text-xs">Abbrechen</button> <input type="text" name="slug" value={targetPage.slug} class="w-full bg-[#262626] border border-[#333] rounded-lg p-2 text-xs text-white focus:outline-none focus:border-[#e0436b]" />
<button type="submit" class="px-4 py-2 bg-sky-500 text-slate-950 font-bold rounded-lg text-xs">Übernehmen & Vorschau</button> </div>
<div class="flex justify-end gap-2 pt-4 border-t border-[#2d2d2d]">
<button type="button" id="btn-close-page-settings" class="px-4 py-2 bg-[#262626] text-slate-300 rounded-lg text-xs hover:bg-[#333]">Abbrechen</button>
<button type="submit" class="px-4 py-2 bg-[#e0436b] text-white font-bold rounded-lg text-xs hover:bg-[#c9365c]">Speichern</button>
</div> </div>
</form> </form>
</dialog> </dialog>
</Layout> </Layout>
<style>
.custom-scrollbar::-webkit-scrollbar {
width: 6px;
}
.custom-scrollbar::-webkit-scrollbar-track {
background: #181818;
}
.custom-scrollbar::-webkit-scrollbar-thumb {
background: #333;
border-radius: 3px;
}
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
background: #444;
}
</style>
<script> <script>
const iframe = document.getElementById('preview-iframe') as HTMLIFrameElement; const iframe = document.getElementById('preview-iframe') as HTMLIFrameElement;
const wrapper = document.getElementById('iframe-wrapper');
const statusBadge = document.getElementById('editor-status-badge'); const statusBadge = document.getElementById('editor-status-badge');
// 1. PostMessage helper const stateWidgets = document.getElementById('panel-state-widgets');
const stateInspector = document.getElementById('panel-state-inspector');
const activeBadge = document.getElementById('inspector-active-badge');
// 1. Live PostMessage Sync Helper
function sendToIframe(type: string, payload: any) { function sendToIframe(type: string, payload: any) {
if (iframe?.contentWindow) { if (iframe?.contentWindow) {
iframe.contentWindow.postMessage({ type, payload }, '*'); iframe.contentWindow.postMessage({ type, payload }, '*');
} }
} }
// 2. Draft Speichern // 2. Responsive Switcher Controls
document.getElementById('res-desktop')?.addEventListener('click', () => {
if (wrapper) wrapper.style.width = '100%';
});
document.getElementById('res-tablet')?.addEventListener('click', () => {
if (wrapper) wrapper.style.width = '768px';
});
document.getElementById('res-mobile')?.addEventListener('click', () => {
if (wrapper) wrapper.style.width = '375px';
});
// 3. Tab Switching in Inspector
const btnContent = document.getElementById('tab-btn-content');
const btnStyle = document.getElementById('tab-btn-style');
const btnAdv = document.getElementById('tab-btn-advanced');
const panelContent = document.getElementById('tab-panel-content');
const panelStyle = document.getElementById('tab-panel-style');
const panelAdv = document.getElementById('tab-panel-advanced');
function switchTab(tab: 'content' | 'style' | 'advanced') {
[btnContent, btnStyle, btnAdv].forEach(b => {
b?.classList.remove('text-[#e0436b]', 'border-[#e0436b]');
b?.classList.add('text-slate-400', 'border-transparent');
});
[panelContent, panelStyle, panelAdv].forEach(p => p?.classList.add('hidden'));
if (tab === 'content') {
btnContent?.classList.add('text-[#e0436b]', 'border-[#e0436b]');
panelContent?.classList.remove('hidden');
} else if (tab === 'style') {
btnStyle?.classList.add('text-[#e0436b]', 'border-[#e0436b]');
panelStyle?.classList.remove('hidden');
} else if (tab === 'advanced') {
btnAdv?.classList.add('text-[#e0436b]', 'border-[#e0436b]');
panelAdv?.classList.remove('hidden');
}
}
btnContent?.addEventListener('click', () => switchTab('content'));
btnStyle?.addEventListener('click', () => switchTab('style'));
btnAdv?.addEventListener('click', () => switchTab('advanced'));
// Open Drawer button
document.getElementById('btn-open-widgets')?.addEventListener('click', () => {
stateInspector?.classList.add('hidden');
stateWidgets?.classList.remove('hidden');
activeBadge?.classList.add('hidden');
});
// Color Pickers Sync
const bgPicker = document.getElementById('field-bg-color-picker') as HTMLInputElement;
const bgInput = document.getElementById('field-bg-color') as HTMLInputElement;
bgPicker?.addEventListener('input', (e: any) => { if (bgInput) bgInput.value = e.target.value; });
const accPicker = document.getElementById('field-accent-color-picker') as HTMLInputElement;
const accInput = document.getElementById('field-accent-color') as HTMLInputElement;
accPicker?.addEventListener('input', (e: any) => { if (accInput) accInput.value = e.target.value; });
const blurSlider = document.getElementById('field-backdrop-blur') as HTMLInputElement;
const blurVal = document.getElementById('blur-val');
blurSlider?.addEventListener('input', (e: any) => { if (blurVal) blurVal.textContent = `${e.target.value}px`; });
// Inspector für Sektion öffnen
document.querySelectorAll('.btn-inspect-section').forEach(btn => {
btn.addEventListener('click', (e: any) => {
const card = e.target.closest('[data-section-id]');
const sectionId = card.getAttribute('data-section-id');
const settings = JSON.parse(card.getAttribute('data-settings') || '{}');
(document.getElementById('active-section-id') as HTMLInputElement).value = sectionId;
(document.getElementById('field-title') as HTMLInputElement).value = settings.title || '';
(document.getElementById('field-subtitle') as HTMLTextAreaElement).value = settings.subtitle || '';
(document.getElementById('field-cta-text') as HTMLInputElement).value = settings.cta_text || '';
(document.getElementById('field-bg-color') as HTMLInputElement).value = settings.bg_color || '';
(document.getElementById('field-accent-color') as HTMLInputElement).value = settings.accent_color || '';
stateWidgets?.classList.add('hidden');
stateInspector?.classList.remove('hidden');
activeBadge?.classList.remove('hidden');
switchTab('content');
});
});
// Dynamic Live Patch Event bei Eingaben
const formInspector = document.getElementById('form-elementor-inspector');
formInspector?.addEventListener('input', () => {
const formData = new FormData(formInspector as HTMLFormElement);
const sectionId = formData.get('section_id');
const settings: Record<string, any> = {};
formData.forEach((val, key) => { settings[key] = val; });
sendToIframe('THEME_UPDATE', { sectionId, settings });
});
// 4. API Actions (Save Draft & Publish)
document.getElementById('btn-save-draft')?.addEventListener('click', async () => { document.getElementById('btn-save-draft')?.addEventListener('click', async () => {
const res = await fetch('/api/editor/save-draft', { const res = await fetch('/api/editor/save-draft', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ updated_at: new Date().toISOString() }) body: JSON.stringify({ updated_at: new Date().toISOString() })
}); });
if (res.ok && statusBadge) { if (res.ok && statusBadge) {
statusBadge.textContent = 'Entwurf gespeichert'; statusBadge.textContent = 'Draft Saved';
statusBadge.className = 'text-sky-400 bg-sky-950/60 border border-sky-800/60 px-2 py-0.5 rounded text-[10px]'; statusBadge.className = 'text-emerald-400 bg-emerald-950/60 border border-emerald-800/60 px-2 py-0.5 rounded text-[10px] font-mono';
} }
}); });
// 3. Veröffentlichen
document.getElementById('btn-publish')?.addEventListener('click', async () => { document.getElementById('btn-publish')?.addEventListener('click', async () => {
const res = await fetch('/api/editor/publish', { method: 'POST' }); const res = await fetch('/api/editor/publish', { method: 'POST' });
if (res.ok && statusBadge) { if (res.ok && statusBadge) {
statusBadge.textContent = 'Live Veröffentlicht!'; statusBadge.textContent = 'Published!';
statusBadge.className = 'text-emerald-400 bg-emerald-950/60 border border-emerald-800/60 px-2 py-0.5 rounded text-[10px]'; statusBadge.className = 'text-emerald-400 bg-emerald-950/60 border border-emerald-800/60 px-2 py-0.5 rounded text-[10px] font-mono';
sendToIframe('RELOAD_CANVAS', {}); sendToIframe('RELOAD_CANVAS', {});
} }
}); });
// 4. Preset Wechsel // Quick Add Widget Button
document.getElementById('preset-selector')?.addEventListener('change', async (e: any) => { document.querySelectorAll('.btn-quick-add-widget').forEach(btn => {
const presetKey = e.target.value;
sendToIframe('THEME_UPDATE', { preset: presetKey });
// Speichere Preset in theme.config.json
await fetch('/api/config', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ themeConfig: { activePreset: presetKey } })
});
});
// Helper zum Verschieben & Speichern der Sektionen-Reihenfolge
async function updateSectionsOrder(newSections: any[]) {
const urlParams = new URLSearchParams(window.location.search);
const pageId = urlParams.get('page_id') || 'page_home';
const res = await fetch('/api/editor/update-section', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ page_id: pageId, sections: newSections })
});
if (res.ok) {
window.location.reload();
}
}
function getSectionsFromDOM() {
const list: any[] = [];
document.querySelectorAll('#section-list > div').forEach(card => {
const id = card.getAttribute('data-section-id');
const type = card.getAttribute('data-section-type');
const settings = JSON.parse(card.getAttribute('data-settings') || '{}');
list.push({ id, type, settings });
});
return list;
}
// Event: Sektion nach oben / unten verschieben
document.querySelectorAll('.btn-move-up').forEach(btn => {
btn.addEventListener('click', async (e: any) => { btn.addEventListener('click', async (e: any) => {
const sections = getSectionsFromDOM(); const type = e.currentTarget.getAttribute('data-widget-type');
const card = e.target.closest('[data-section-id]');
const sectionId = card.getAttribute('data-section-id');
const index = sections.findIndex(s => s.id === sectionId);
if (index > 0) {
const temp = sections[index];
sections[index] = sections[index - 1];
sections[index - 1] = temp;
await updateSectionsOrder(sections);
}
});
});
document.querySelectorAll('.btn-move-down').forEach(btn => {
btn.addEventListener('click', async (e: any) => {
const sections = getSectionsFromDOM();
const card = e.target.closest('[data-section-id]');
const sectionId = card.getAttribute('data-section-id');
const index = sections.findIndex(s => s.id === sectionId);
if (index >= 0 && index < sections.length - 1) {
const temp = sections[index];
sections[index] = sections[index + 1];
sections[index + 1] = temp;
await updateSectionsOrder(sections);
}
});
});
// 5. Event: Sektion hinzufügen
document.getElementById('btn-add-section')?.addEventListener('click', async () => {
const sectionType = (document.getElementById('select-new-section-type') as HTMLSelectElement).value;
const urlParams = new URLSearchParams(window.location.search);
const pageId = urlParams.get('page_id') || 'page_home';
const res = await fetch('/api/editor/add-section', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ page_id: pageId, section_type: sectionType })
});
if (res.ok) {
window.location.reload();
}
});
// 6. Event: Sektion löschen
document.querySelectorAll('.btn-delete-section').forEach(btn => {
btn.addEventListener('click', async (e: any) => {
const card = e.target.closest('[data-section-id]');
const sectionId = card?.getAttribute('data-section-id');
const urlParams = new URLSearchParams(window.location.search); const urlParams = new URLSearchParams(window.location.search);
const pageId = urlParams.get('page_id') || 'page_home'; const pageId = urlParams.get('page_id') || 'page_home';
const res = await fetch('/api/editor/delete-section', { const res = await fetch('/api/editor/add-section', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ page_id: pageId, section_id: sectionId }) body: JSON.stringify({ page_id: pageId, section_type: type })
}); });
if (res.ok) window.location.reload();
if (res.ok) {
window.location.reload();
}
}); });
}); });
// 7. Event-Handling für das Öffnen & Speichern von Sektions-Eigenschaften // Page Settings Modal
const editModal = document.getElementById('modal-edit-section') as HTMLDialogElement; const modalPage = document.getElementById('modal-page-settings') as HTMLDialogElement;
const fieldsContainer = document.getElementById('dynamic-settings-fields'); document.getElementById('btn-open-settings')?.addEventListener('click', () => modalPage?.showModal());
document.getElementById('btn-close-page-settings')?.addEventListener('click', () => modalPage?.close());
document.getElementById('btn-close-edit-modal')?.addEventListener('click', () => editModal?.close());
// Klick auf "Bearbeiten" an einer Sektion
document.querySelectorAll('.btn-edit-section')?.forEach(btn => {
btn.addEventListener('click', (e: any) => {
const card = e.target.closest('[data-section-id]');
const sectionId = card.getAttribute('data-section-id');
const sectionType = card.getAttribute('data-section-type');
const currentSettings = JSON.parse(card.getAttribute('data-settings') || '{}');
(document.getElementById('edit-section-id') as HTMLInputElement).value = sectionId;
(document.getElementById('modal-section-type-badge') as HTMLElement).textContent = sectionType;
if (fieldsContainer) {
fieldsContainer.innerHTML = '';
const allKeys = new Set(Object.keys(currentSettings));
if (!allKeys.has('bg_color')) {
allKeys.add('bg_color');
}
allKeys.forEach((key) => {
const value = currentSettings[key] || '';
const fieldWrapper = document.createElement('div');
if (key === 'bg_color') {
fieldWrapper.innerHTML = `
<label class="block text-xs font-semibold text-slate-400 mb-1 uppercase tracking-wider">🎨 Sektions-Hintergrundfarbe (HEX/RGBA)</label>
<div class="flex gap-2">
<input
type="color"
value="${value && value.startsWith('#') ? value : '#0f172a'}"
class="h-9 w-12 rounded bg-slate-800 border border-slate-700 cursor-pointer p-0.5"
onchange="this.nextElementSibling.value = this.value"
/>
<input
type="text"
name="bg_color"
value="${value}"
placeholder="z.B. #0F172A oder transparent"
class="flex-1 bg-slate-800 border border-slate-700 rounded-lg p-2 text-xs text-white focus:outline-none focus:border-sky-500"
/>
</div>
`;
} else {
fieldWrapper.innerHTML = `
<label class="block text-xs font-semibold text-slate-400 mb-1 uppercase tracking-wider">${key}</label>
<input
type="text"
name="${key}"
value="${typeof value === 'object' ? JSON.stringify(value) : value}"
class="w-full bg-slate-800 border border-slate-700 rounded-lg p-2 text-xs text-white focus:outline-none focus:border-sky-500"
/>
`;
}
fieldsContainer.appendChild(fieldWrapper);
});
}
editModal?.showModal();
});
});
// Submit der Sektions-Einstellungen
document.getElementById('form-edit-section-settings')?.addEventListener('submit', async (e: Event) => {
e.preventDefault();
const formData = new FormData(e.target as HTMLFormElement);
const sectionId = formData.get('section_id') as string;
formData.delete('section_id');
const settings: Record<string, any> = {};
formData.forEach((val, key) => { settings[key] = val; });
const urlParams = new URLSearchParams(window.location.search);
const pageId = urlParams.get('page_id') || 'page_home';
const res = await fetch('/api/editor/update-section-settings', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ page_id: pageId, section_id: sectionId, settings })
});
if (res.ok) {
editModal?.close();
window.location.reload();
}
});
</script> </script>