feat: rebuild admin editor into full Elementor-style 3-tab inspector and responsive canvas
This commit is contained in:
@@ -14,358 +14,494 @@ const pageId = searchParams.get('page_id');
|
||||
const userAgent = Astro.request.headers.get('user-agent') ?? '';
|
||||
const { siteConfig: draftConfig } = loadWaasConfigs(userAgent, true);
|
||||
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`;
|
||||
---
|
||||
|
||||
<Layout title={`Editor: ${targetPage?.title || 'Baukasten'} | N&D Admin`}>
|
||||
<Layout title={`Elementor Editor: ${targetPage?.title || 'Baukasten'} | N&D Admin`}>
|
||||
<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">
|
||||
<div class="flex items-center justify-between border-b border-slate-800 pb-4">
|
||||
<!-- 1. HEADER LEISTE -->
|
||||
<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>
|
||||
<a href="/admin/pages" class="text-xs text-slate-400 hover:text-sky-400 transition-colors">← Zurück zur Übersicht</a>
|
||||
<h1 class="text-base font-bold text-white mt-1">{targetPage?.title || 'Seite bearbeiten'}</h1>
|
||||
<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">
|
||||
{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 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>
|
||||
<!-- Sektion / Widget hinzufügen -->
|
||||
<div class="pt-4 border-t border-[#2d2d2d] space-y-3">
|
||||
<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>
|
||||
</div>
|
||||
</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
|
||||
<!-- 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>
|
||||
</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">Sektionen verwalten</label>
|
||||
<div id="section-list" class="space-y-2">
|
||||
{targetPage?.sections?.map((sec: any, index: number) => (
|
||||
<div
|
||||
class="p-3 bg-slate-800/80 border border-slate-700/60 rounded-lg text-xs space-y-2 text-slate-200"
|
||||
data-section-id={sec.id}
|
||||
data-section-type={sec.type}
|
||||
data-settings={JSON.stringify(sec.settings || {})}
|
||||
>
|
||||
<div class="flex items-center justify-between font-semibold">
|
||||
<span class="text-sky-400">#{index + 1} {sec.type}</span>
|
||||
<div class="flex items-center gap-1">
|
||||
<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>
|
||||
<!-- 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>
|
||||
|
||||
<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 class="p-4 border-t border-slate-800 bg-slate-950/50">
|
||||
<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">
|
||||
💾 Speichern
|
||||
</button>
|
||||
<!-- 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>
|
||||
|
||||
<main class="flex-1 bg-slate-950 flex flex-col relative">
|
||||
<div class="h-10 bg-slate-900 border-b border-slate-800 flex items-center justify-between px-4 text-xs text-slate-400">
|
||||
<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>
|
||||
<!-- RECHTER CANVAS-BEREICH (LIVE-IFRAME WORKSPACE) -->
|
||||
<main class="flex-1 bg-[#121212] flex flex-col relative items-center justify-start overflow-hidden p-4">
|
||||
|
||||
<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>
|
||||
<iframe
|
||||
id="preview-iframe"
|
||||
src={previewUrl}
|
||||
class="w-full h-full border-none bg-slate-950"
|
||||
title="Live Website Preview"
|
||||
></iframe>
|
||||
|
||||
<!-- 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
|
||||
id="preview-iframe"
|
||||
src={previewUrl}
|
||||
class="w-full h-full border-none transition-all duration-300"
|
||||
title="Elementor Live Workspace"
|
||||
></iframe>
|
||||
</div>
|
||||
|
||||
</main>
|
||||
|
||||
</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">
|
||||
<form id="form-edit-section-settings" class="space-y-4">
|
||||
<h2 class="text-lg font-bold text-white flex items-center justify-between">
|
||||
<span>Sektion bearbeiten</span>
|
||||
<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>
|
||||
<!-- GLOBALE SEITENEINSTELLUNGEN MODAL -->
|
||||
<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">
|
||||
<form id="form-page-settings" class="space-y-4">
|
||||
<h2 class="text-base font-bold text-white flex items-center justify-between border-b border-[#2d2d2d] pb-3">
|
||||
<span>⚙️ Seiteneinstellungen</span>
|
||||
</h2>
|
||||
<input type="hidden" id="edit-section-id" name="section_id" />
|
||||
|
||||
<div id="dynamic-settings-fields" class="space-y-3">
|
||||
<div>
|
||||
<label class="block text-xs font-bold text-slate-400 mb-1">Seitentitel</label>
|
||||
<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 class="flex justify-end gap-2 pt-4 border-t border-slate-800">
|
||||
<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>
|
||||
<button type="submit" class="px-4 py-2 bg-sky-500 text-slate-950 font-bold rounded-lg text-xs">Übernehmen & Vorschau</button>
|
||||
<div>
|
||||
<label class="block text-xs font-bold text-slate-400 mb-1">Slug (URL)</label>
|
||||
<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]" />
|
||||
</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>
|
||||
</form>
|
||||
</dialog>
|
||||
|
||||
</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>
|
||||
const iframe = document.getElementById('preview-iframe') as HTMLIFrameElement;
|
||||
const wrapper = document.getElementById('iframe-wrapper');
|
||||
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) {
|
||||
if (iframe?.contentWindow) {
|
||||
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 () => {
|
||||
const res = await fetch('/api/editor/save-draft', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ updated_at: new Date().toISOString() })
|
||||
});
|
||||
|
||||
if (res.ok && statusBadge) {
|
||||
statusBadge.textContent = 'Gespeichert';
|
||||
statusBadge.className = 'text-emerald-400 bg-emerald-950/60 border border-emerald-800/60 px-2 py-0.5 rounded text-[10px]';
|
||||
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] 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', {});
|
||||
}
|
||||
});
|
||||
|
||||
// 4. Preset Wechsel
|
||||
document.getElementById('preset-selector')?.addEventListener('change', async (e: any) => {
|
||||
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 => {
|
||||
// Quick Add Widget Button
|
||||
document.querySelectorAll('.btn-quick-add-widget').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) {
|
||||
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 type = e.currentTarget.getAttribute('data-widget-type');
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
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',
|
||||
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
|
||||
const editModal = document.getElementById('modal-edit-section') as HTMLDialogElement;
|
||||
const fieldsContainer = document.getElementById('dynamic-settings-fields');
|
||||
|
||||
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();
|
||||
}
|
||||
});
|
||||
|
||||
// Page Settings Modal
|
||||
const modalPage = document.getElementById('modal-page-settings') as HTMLDialogElement;
|
||||
document.getElementById('btn-open-settings')?.addEventListener('click', () => modalPage?.showModal());
|
||||
document.getElementById('btn-close-page-settings')?.addEventListener('click', () => modalPage?.close());
|
||||
</script>
|
||||
Reference in New Issue
Block a user