feat: rebuild admin editor into full Elementor-style 3-tab inspector and responsive canvas

This commit is contained in:
Daniel S
2026-08-09 23:52:51 +02:00
parent fc2017b210
commit 28473bbcdc

View File

@@ -14,358 +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">
<aside class="w-80 border-r border-slate-800 bg-slate-900 flex flex-col justify-between z-20"> <!-- 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">
<!-- 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> <div class="flex items-center gap-2">
<a href="/admin/pages" class="text-xs text-slate-400 hover:text-sky-400 transition-colors">← Zurück zur Übersicht</a> <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">
<h1 class="text-base font-bold text-white mt-1">{targetPage?.title || 'Seite bearbeiten'}</h1> <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>
</div> </a>
</div> <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>
<div class="space-y-2">
<label class="text-xs font-semibold text-slate-400 uppercase tracking-wider block">Farb-Auswahl</label>
<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">
{Object.entries(THEME_PRESETS).map(([key, preset]) => (
<option value={key}>{preset.name}</option>
))}
</select>
</div>
<div class="space-y-2">
<label class="text-xs font-semibold text-slate-400 uppercase tracking-wider block">Farbschema</label>
<div class="grid grid-cols-2 gap-2">
<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>
<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>
</div>
</div>
<div class="space-y-2 pt-4 border-t border-slate-800">
<label class="text-xs font-semibold text-slate-400 uppercase tracking-wider block">Sektion hinzufügen</label>
<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">
<option value="HeroSection">Hero Section</option>
<option value="BentoGrid">Bento Grid</option>
<option value="ContactSection">Kontakt Formular</option>
</select>
<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">
+ Hinzufügen
</button> </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>
</div> </div>
<div class="space-y-2 pt-4 border-t border-slate-800"> <!-- 2. HAUPTBEREICH (DYNAMISCHES PANEL) -->
<label class="text-xs font-semibold text-slate-400 uppercase tracking-wider block">Sektionen verwalten</label> <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 class="flex items-center justify-between mb-3">
<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"> <div id="section-list" class="space-y-2">
{targetPage?.sections?.map((sec: any, index: number) => ( {targetPage?.sections?.map((sec: any, index: number) => (
<div <div
class="p-3 bg-slate-800/80 border border-slate-700/60 rounded-lg text-xs space-y-2 text-slate-200" 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-id={sec.id}
data-section-type={sec.type} data-section-type={sec.type}
data-settings={JSON.stringify(sec.settings || {})} data-settings={JSON.stringify(sec.settings || {})}
> >
<div class="flex items-center justify-between font-semibold"> <div class="flex items-center gap-2.5 min-w-0 flex-1">
<span class="text-sky-400">#{index + 1} {sec.type}</span> <span class="text-[10px] font-mono text-[#e0436b] font-bold">#{index + 1}</span>
<div class="flex items-center gap-1"> <span class="font-semibold text-slate-200 truncate">{sec.type}</span>
<button class="btn-edit-section hover:text-sky-400 px-1" title="Inhalte/Einstellungen bearbeiten">✏️</button>
<button class="btn-move-up hover:text-sky-400 px-1" title="Nach oben">⬆️</button>
<button class="btn-move-down hover:text-sky-400 px-1" title="Nach unten">⬇️</button>
<button class="btn-delete-section hover:text-red-400 px-1" title="Sektion löschen">🗑️</button>
</div> </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> </div>
</div>
<div class="p-4 border-t border-slate-800 bg-slate-950/50"> <!-- Sektion / Widget hinzufügen -->
<button id="btn-save-draft" 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 flex items-center justify-center gap-2"> <div class="pt-4 border-t border-[#2d2d2d] space-y-3">
💾 Speichern <h2 class="text-xs font-bold uppercase tracking-wider text-slate-400">Widget-Bibliothek (Neue Sektion)</h2>
<div class="grid grid-cols-2 gap-2.5">
<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>
<span class="text-xs font-bold text-slate-200">Hero Section</span>
</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>
<span class="text-xs font-bold text-slate-200">Bento Grid</span>
</button>
<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">
<span class="text-xl group-hover:scale-110 transition-transform">📬</span>
<span class="text-xs font-bold text-slate-200">Kontakt Formular</span>
</button> </button>
</div> </div>
</div>
</div>
<!-- STATE B: 3-TAB-INSPEKTORMENÜ (Elementor Inspector) -->
<div id="panel-state-inspector" class="hidden flex flex-col h-full">
<!-- TAB HEADER -->
<div class="flex border-b border-[#2d2d2d] bg-[#181818]">
<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">
<span>📝</span> Inhalt
</button>
<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">
<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>
</div>
<!-- TAB INHALTE -->
<form id="form-elementor-inspector" class="p-4 flex-1 space-y-6 overflow-y-auto">
<input type="hidden" id="active-section-id" name="section_id" />
<!-- TAB 1: INHALT -->
<div id="tab-panel-content" class="space-y-4">
<div class="flex items-center justify-between pb-2 border-b border-[#2d2d2d]">
<span class="text-xs font-bold text-slate-300">Text- & Medienfelder</span>
<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
</button>
</div>
<div>
<label class="block text-[11px] font-bold text-slate-400 uppercase tracking-wider mb-1">Überschrift (Title)</label>
<div class="relative">
<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>
<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>
<!-- 3. BOTTOM-BAR (PRO-STEUERUNGSLEISTE) -->
<div class="h-14 bg-[#181818] border-t border-[#2d2d2d] px-3 flex items-center justify-between gap-2">
<div class="flex items-center gap-1">
<!-- ⚙️ Zahnrad: Seiteneinstellungen -->
<button id="btn-open-settings" class="p-2 rounded hover:bg-[#2a2a2a] text-slate-400 hover:text-white transition-colors" title="Seiteneinstellungen (SEO, Slug)">
⚙️
</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>
</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> </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>
<!-- Live Canvas Iframe Wrapper -->
<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]">
<iframe <iframe
id="preview-iframe" id="preview-iframe"
src={previewUrl} src={previewUrl}
class="w-full h-full border-none bg-slate-950" class="w-full h-full border-none transition-all duration-300"
title="Live Website Preview" title="Elementor Live Workspace"
></iframe> ></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. 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 = 'Gespeichert'; statusBadge.textContent = 'Draft Saved';
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';
}
});
document.getElementById('btn-publish')?.addEventListener('click', async () => {
const res = await fetch('/api/editor/publish', { method: 'POST' });
if (res.ok && statusBadge) {
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] 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 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/add-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_type: sectionType }) body: JSON.stringify({ page_id: pageId, section_type: type })
}); });
if (res.ok) window.location.reload();
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 pageId = urlParams.get('page_id') || 'page_home';
const res = await fetch('/api/editor/delete-section', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ page_id: pageId, section_id: sectionId })
});
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('title')) allKeys.add('title');
if (!allKeys.has('subtitle')) allKeys.add('subtitle');
if (!allKeys.has('bg_color')) allKeys.add('bg_color');
if (!allKeys.has('animation')) allKeys.add('animation');
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 if (key === 'animation') {
fieldWrapper.innerHTML = `
<label class="block text-xs font-semibold text-slate-400 mb-1 uppercase tracking-wider">✨ Animation / Effekt</label>
<select name="animation" 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">
<option value="" ${!value ? 'selected' : ''}>Keine Animation</option>
<option value="fade-in" ${value === 'fade-in' ? 'selected' : ''}>Fade In (Einblenden)</option>
<option value="slide-up" ${value === 'slide-up' ? 'selected' : ''}>Slide Up (Von unten)</option>
<option value="zoom-in" ${value === 'zoom-in' ? 'selected' : ''}>Zoom In (Heranzoomen)</option>
</select>
`;
} 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>