Files
ndsolutionswebsite/src/pages/admin/editor.astro

522 lines
26 KiB
Plaintext

---
import Layout from '../../layouts/Layout.astro';
import { loadWaasConfigs } from '../../utils/configLoader';
import { THEME_PRESETS } from '../../utils/themePresets';
const { siteConfig, themeConfig } = loadWaasConfigs();
---
<Layout title="Live Visual Editor | WaaS Engine">
<div class="h-screen w-screen flex flex-col bg-[#0B0F19] text-white font-sans overflow-hidden">
<!-- Top Bar -->
<header class="h-14 bg-slate-900/80 border-b border-white/10 px-6 flex items-center justify-between backdrop-blur-md shrink-0 z-20">
<div class="flex items-center gap-3">
<span class="text-xs font-mono text-sky-400 font-bold uppercase tracking-widest">Waas Elementor Live Editor</span>
<span class="text-xs px-2.5 py-0.5 rounded-full bg-emerald-500/10 text-emerald-400 border border-emerald-500/20 font-mono">SSR Live Active</span>
</div>
<!-- Device Switcher (PC | Tablet | Mobile) -->
<div class="flex items-center gap-1 bg-slate-950/70 p-1 rounded-xl border border-white/10">
<button type="button" id="device-desktop" class="device-btn px-3 py-1 rounded-lg text-xs font-semibold transition-all bg-sky-400/20 text-sky-400 border border-sky-400/30" data-device="desktop">
Desktop (100%)
</button>
<button type="button" id="device-tablet" class="device-btn px-3 py-1 rounded-lg text-xs font-semibold transition-all text-slate-400 hover:text-white" data-device="tablet">
Tablet (768px)
</button>
<button type="button" id="device-mobile" class="device-btn px-3 py-1 rounded-lg text-xs font-semibold transition-all text-slate-400 hover:text-white" data-device="mobile">
Mobile (375px)
</button>
</div>
<div class="flex items-center gap-3">
<a href="/admin" class="px-3.5 py-1.5 rounded-lg glass-panel text-xs hover:border-sky-400 transition-all">
Classic Admin
</a>
<button id="save-editor-btn" class="px-5 py-1.5 rounded-lg bg-sky-400 text-slate-950 font-bold text-xs uppercase tracking-wider hover:bg-white transition-all shadow-lg shadow-sky-400/20 active:scale-95">
Live Speichern
</button>
</div>
</header>
<div id="editor-alert" class="hidden px-4 py-2 bg-emerald-500/10 border-b border-emerald-500/20 text-emerald-400 text-xs font-mono text-center"></div>
<!-- Split Screen Main -->
<div class="flex-1 flex overflow-hidden relative">
<!-- Left Panel: Controls & Drag-and-Drop Sektionen -->
<aside class="w-90 sm:w-96 bg-slate-950/70 border-r border-white/10 p-5 overflow-y-auto space-y-6 shrink-0 z-10 custom-scroll">
<!-- 3-Teiler Inspector Tabs (Inhalt, Stil, Erweitert) -->
<div class="flex bg-slate-900/80 p-1 rounded-xl border border-white/10">
<button type="button" id="tab-btn-content" class="inspector-tab-btn flex-1 py-1.5 rounded-lg text-xs font-bold transition-all bg-sky-400/20 text-sky-400 border border-sky-400/30" data-tab="content">
Inhalt
</button>
<button type="button" id="tab-btn-style" class="inspector-tab-btn flex-1 py-1.5 rounded-lg text-xs font-bold transition-all text-slate-400 hover:text-white" data-tab="style">
Stil
</button>
<button type="button" id="tab-btn-advanced" class="inspector-tab-btn flex-1 py-1.5 rounded-lg text-xs font-bold transition-all text-slate-400 hover:text-white" data-tab="advanced">
Erweitert
</button>
</div>
<!-- TAB 1: INHALT (Sektionen, Layout & Widget Hinzufuegen) -->
<div id="tab-panel-content" class="space-y-6">
<!-- Neue Sektion Hinzufuegen -->
<div class="glass-panel rounded-xl p-4 space-y-3">
<h3 class="text-xs font-bold text-sky-400 uppercase tracking-wider">
+ Neue Sektion Hinzufuegen
</h3>
<div class="flex gap-2">
<select id="new-section-select" class="flex-1 glass-panel bg-slate-900 rounded-lg p-2 text-xs text-white focus:outline-none focus:border-sky-400">
<option value="hero">Hero Section</option>
<option value="bento">Bento Grid Section</option>
<option value="services">Leistungen / Services</option>
<option value="contact">Kontaktformular Section</option>
<option value="faq">FAQ Akkordeon Section</option>
<option value="testimonials">Kundenbewertungen Section</option>
<option value="cta-banner">CTA Banner Section</option>
</select>
<button type="button" id="add-section-btn" class="px-3 py-2 rounded-lg bg-sky-400/20 text-sky-400 border border-sky-400/30 font-bold text-xs hover:bg-sky-400 hover:text-slate-950 transition-all">
+ Hinzufuegen
</button>
</div>
</div>
<!-- Sektionen Re-Order -->
<div class="glass-panel rounded-xl p-4 space-y-3">
<h3 class="text-xs font-bold text-sky-400 uppercase tracking-wider flex items-center justify-between">
<span>Sektionen Reihenfolge (Layout)</span>
<span class="text-[10px] text-slate-500 font-mono">Drag & Drop</span>
</h3>
<div id="sections-list" class="space-y-2">
{siteConfig.sectionsOrder.map((sec, idx) => (
<div data-section={sec} draggable="true" class="section-item flex items-center justify-between p-3 rounded-lg bg-slate-900/60 border border-white/5 hover:border-white/20 transition-all group cursor-grab">
<div class="flex items-center gap-2">
<span class="text-slate-500 font-mono text-xs">#{idx + 1}</span>
<span class="text-xs font-semibold capitalize text-white">{sec}</span>
</div>
<div class="flex items-center gap-1">
<button type="button" onclick={`moveSection('${sec}', -1)`} class="p-1 hover:text-sky-400 text-xs font-bold">Up</button>
<button type="button" onclick={`moveSection('${sec}', 1)`} class="p-1 hover:text-sky-400 text-xs font-bold">Down</button>
<button type="button" onclick={`deleteSection('${sec}')`} class="p-1 text-red-400/70 hover:text-red-400 text-xs font-bold ml-1">Trash</button>
</div>
</div>
))}
</div>
</div>
<!-- Dynamische UIKomponente Hinzufuegen per Sektion -->
<div class="glass-panel rounded-xl p-4 space-y-4">
<h3 class="text-xs font-bold text-sky-400 uppercase tracking-wider">
UI-Komponente Hinzufuegen &amp; Verwaltungsverzeichnis
</h3>
<div class="space-y-3">
<div>
<label class="block text-[10px] font-bold text-slate-400 uppercase mb-1">Ziel-Sektion</label>
<select id="new-el-section" class="w-full glass-panel bg-slate-900 rounded-lg p-2 text-xs text-white focus:outline-none focus:border-sky-400 capitalize">
{siteConfig.sectionsOrder.map(sec => (
<option value={sec}>{sec} Sektion</option>
))}
</select>
</div>
<div>
<label class="block text-[10px] font-bold text-slate-400 uppercase mb-1">Platz (Slot ID)</label>
<select id="new-el-slot" class="w-full glass-panel bg-slate-900 rounded-lg p-2 text-xs text-white focus:outline-none focus:border-sky-400">
<option value="hero-badge-slot">Hero Badge Slot</option>
<option value="hero-actions-slot">Hero Buttons Slot</option>
<option value="hero-custom-slot">Hero Custom Elements Slot</option>
<option value="bento-header-slot">Bento Header Slot</option>
<option value="bento-grid-slot">Bento Grid Elements Slot</option>
<option value="contact-form-slot">Contact Form Slot</option>
<option value="section-custom-slot">Allgemeiner Sektions-Slot</option>
</select>
</div>
<div>
<label class="block text-[10px] font-bold text-slate-400 uppercase mb-1">Typ</label>
<select id="new-el-type" class="w-full glass-panel bg-slate-900 rounded-lg p-2 text-xs text-white focus:outline-none focus:border-sky-400">
<option value="button">Button (CTA)</option>
<option value="badge">Badge / Utility Tag</option>
<option value="text">Textblock / Paragraph</option>
<option value="card">Feature Bento Card</option>
<option value="accordion">FAQ Akkordeon Block</option>
<option value="stat-box">Statistiken / Zahlen-Box</option>
<option value="testimonial">Kundenbewertung / Testimonial</option>
<option value="cta-box">Banner / Call-to-Action Box</option>
</select>
</div>
<div>
<label class="block text-[10px] font-bold text-slate-400 uppercase mb-1">Inhalt / Label</label>
<input type="text" id="new-el-content" placeholder="z.B. Jetzt Anfragen" class="w-full glass-panel bg-slate-900 rounded-lg p-2 text-xs text-white focus:outline-none focus:border-sky-400" />
</div>
<button type="button" id="add-el-btn" class="w-full py-2.5 rounded-lg bg-sky-400/20 text-sky-400 border border-sky-400/30 font-bold text-xs uppercase tracking-wider hover:bg-sky-400 hover:text-slate-950 transition-all">
+ Komponente in Sektion platzieren
</button>
</div>
</div>
<!-- Component Manager per Section (Positionieren, Bearbeiten, Loeschen) -->
<div class="glass-panel rounded-xl p-4 space-y-3">
<h3 class="text-xs font-bold text-sky-400 uppercase tracking-wider flex items-center justify-between">
<span>Sektions-Komponenten (<span id="elements-count">{(siteConfig.hero.elements || []).length}</span>)</span>
</h3>
<!-- Dynamic Filter per Section -->
<div class="flex items-center gap-2 mb-2">
<label class="text-[10px] text-slate-400 uppercase font-bold">Filter:</label>
<select id="filter-section-select" class="flex-1 glass-panel bg-slate-900 rounded p-1 text-xs text-white capitalize">
<option value="all">Alle Sektionen</option>
{siteConfig.sectionsOrder.map(sec => (
<option value={sec}>{sec}</option>
))}
</select>
</div>
<div id="elements-container" class="space-y-2 max-h-80 overflow-y-auto pr-1 custom-scroll">
{((siteConfig.hero.elements || [])).map((el, idx) => (
<div data-id={el.id} class="p-3 rounded-lg bg-slate-900/70 border border-white/10 space-y-2 text-xs group">
<div class="flex items-center justify-between">
<div>
<span class="font-bold text-sky-400 uppercase text-[10px] block">{el.type} (Slot: {el.slotId})</span>
<span id={`el-label-${el.id}`} class="text-slate-200 font-semibold">{el.content}</span>
</div>
<div class="flex items-center gap-1">
<button type="button" onclick={`moveElement('${el.id}', -1)`} class="p-1 hover:text-sky-400 text-xs font-bold" title="Nach oben verschieben">Up</button>
<button type="button" onclick={`moveElement('${el.id}', 1)`} class="p-1 hover:text-sky-400 text-xs font-bold" title="Nach unten verschieben">Down</button>
<button type="button" onclick={`deleteElement('${el.id}')`} class="p-1 text-red-400/80 hover:text-red-400 text-xs font-bold ml-1" title="Komponente loeschen">Trash</button>
</div>
</div>
<!-- Inline Edit Form -->
<div class="pt-2 border-t border-white/5 flex gap-2">
<input type="text" id={`edit-input-${el.id}`} value={el.content} class="flex-1 glass-panel bg-slate-950 rounded px-2 py-1 text-xs text-white border border-white/10 focus:border-sky-400" />
<button type="button" onclick={`updateElementContent('${el.id}')`} class="px-2 py-1 rounded bg-sky-400/20 text-sky-400 border border-sky-400/30 text-[10px] font-bold hover:bg-sky-400 hover:text-slate-950 transition-all">Save</button>
</div>
</div>
))}
</div>
</div>
</div>
<!-- TAB 2: STIL (Layout, Typographie, Farbung & Glows) -->
<div id="tab-panel-style" class="hidden space-y-6">
<div class="glass-panel rounded-xl p-4 space-y-4">
<h3 class="text-xs font-bold text-sky-400 uppercase tracking-wider">
Globales Farbtheme &amp; Styling
</h3>
<div class="space-y-3">
<div>
<label class="block text-[10px] font-bold text-slate-400 uppercase mb-1">Theme Preset</label>
<select id="style-preset-select" class="w-full glass-panel bg-slate-900 rounded-lg p-2 text-xs text-white focus:outline-none focus:border-sky-400">
{Object.entries(THEME_PRESETS).map(([id, preset]) => (
<option value={id} selected={id === themeConfig.activePreset}>{preset.name}</option>
))}
</select>
</div>
<div>
<label class="block text-[10px] font-bold text-slate-400 uppercase mb-1">Glass Border Radius</label>
<select id="style-radius-select" class="w-full glass-panel bg-slate-900 rounded-lg p-2 text-xs text-white focus:outline-none focus:border-sky-400">
<option value="rounded-lg">Medium (12px)</option>
<option value="rounded-xl">Large (16px)</option>
<option value="rounded-2xl">Extra Large (24px)</option>
</select>
</div>
</div>
</div>
</div>
<!-- TAB 3: ERWEITERT (Animationen, Custom CSS & Z_index) -->
<div id="tab-panel-advanced" class="hidden space-y-6">
<div class="glass-panel rounded-xl p-4 space-y-4">
<h3 class="text-xs font-bold text-sky-400 uppercase tracking-wider">
Erweiterte Effekte &amp; CSS
</h3>
<div class="space-y-3">
<div>
<label class="block text-[10px] font-bold text-slate-400 uppercase mb-1">Globale Animation GPU Acceleration</label>
<select id="advanced-gpu-select" class="w-full glass-panel bg-slate-900 rounded-lg p-2 text-xs text-white focus:outline-none focus:border-sky-400">
<option value="enabled">Activated (Hardware XL)</option>
<option value="disabled">Disabled (Save Battery)</option>
</select>
</div>
<div>
<label class="block text-[10px] font-bold text-slate-400 uppercase mb-1">Custom CSS Overrides</label>
<textarea id="advanced-custom-css" placeholder=".custom-glass { backdrop-blur: 20px; }" class="w-full h-20 glass-panel bg-slate-900 rounded-lg p-2 text-xs text-white font-mono focus:outline-none focus:border-sky-400"></textarea>
</div>
</div>
</div>
</div>
</aside>
<!-- Right Panel: Live Website Preview iframe with Responsive Viewport Container -->
<main class="flex-1 bg-slate-900/90 relative flex items-center justify-center p-4 overflow-hidden">
<div id="iframe-container" class="w-full h-full transition-all duration-300 flex items-center justify-center">
<iframe id="preview-iframe" src="/" class="w-full h-full transition-all duration-300 border-none shadow-2xl rounded-xl"></iframe>
</div>
</main>
</div>
</div>
</Layout>
<script is:inline define:vars={{ siteConfigJSON: JSON.stringify(siteConfig), themeConfigJSON: JSON.stringify(themeConfig) }}>
let siteConfigState = JSON.parse(siteConfigJSON);
let themeConfigState = JSON.parse(themeConfigJSON);
if (!siteConfigState.hero.elements) siteConfigState.hero.elements = [];
function reloadIframe() {
const iframe = document.getElementById('preview-iframe');
if (iframe) iframe.src = iframe.src;
}
const inspectorTabs = document.querySelectorAll('.inspector-tab-btn');
inspectorTabs.forEach(btn => {
btn.addEventListener('click', () => {
inspectorTabs.forEach(b => {
b.classList.remove('bg-sky-400/20', 'text-sky-400', 'border', 'border-sky-400/30');
b.classList.add('text-slate-400');
});
btn.classList.remove('text-slate-400');
btn.classList.add('bg-sky-400/20', 'text-sky-400', 'border', 'border-sky-400/30');
const tab = btn.getAttribute('data-tab');
document.getElementById('tab-panel-content').classList.add('hidden');
document.getElementById('tab-panel-style').classList.add('hidden');
document.getElementById('tab-panel-advanced').classList.add('hidden');
document.getElementById('tab-panel-' + tab).classList.remove('hidden');
});
});
const deviceBtns = document.querySelectorAll('.device-btn');
const iframeContainer = document.getElementById('iframe-container');
deviceBtns.forEach(btn => {
btn.addEventListener('click', () => {
deviceBtns.forEach(b => {
b.classList.remove('bg-sky-400/20', 'text-sky-400', 'border', 'border-sky-400/30');
b.classList.add('text-slate-400');
});
btn.classList.remove('text-slate-400');
btn.classList.add('bg-sky-400/20', 'text-sky-400', 'border', 'border-sky-400/30');
const device = btn.getAttribute('data-device');
if (device === 'mobile') {
iframeContainer.className = 'w-[375px] h-[667px] mx-auto transition-all duration-300 shadow-2xl rounded-2xl border border-white/10 overflow-hidden shrink-0';
} else if (device === 'tablet') {
iframeContainer.className = 'w-[768px] h-[1024px] mx-auto transition-all duration-300 shadow-2xl rounded-2xl border border-white/10 overflow-hidden shrink-0';
} else {
iframeContainer.className = 'w-full h-full transition-all duration-300 flex items-center justify-center';
}
});
});
const addSectionBtn = document.getElementById('add-section-btn');
if (addSectionBtn) {
addSectionBtn.addEventListener('click', () => {
const newSec = document.getElementById('new-section-select').value;
if (!siteConfigState.sectionsOrder.includes(newSec)) {
siteConfigState.sectionsOrder.push(newSec);
renderSectionsList();
updateSectionSelectors();
} else {
alert('Sektion ist bereits im Layout enthalten!');
}
});
}
window.deleteSection = function(secName) {
siteConfigState.sectionsOrder = siteConfigState.sectionsOrder.filter(s => s !== secName);
renderSectionsList();
updateSectionSelectors();
};
window.moveSection = function(secName, dir) {
const arr = siteConfigState.sectionsOrder;
const idx = arr.indexOf(secName);
if (idx === -1) return;
const targetIdx = idx + dir;
if (targetIdx < 0 || targetIdx >= arr.length) return;
const temp = arr[idx];
arr[idx] = arr[targetIdx];
arr[targetIdx] = temp;
renderSectionsList();
};
function renderSectionsList() {
const listContainer = document.getElementById('sections-list');
if (!listContainer) return;
listContainer.innerHTML = siteConfigState.sectionsOrder.map((sec, idx) => (
'<div data-section="' + sec + '" draggable="true" class="section-item flex items-center justify-between p-3 rounded-lg bg-slate-900/60 border border-white/5 hover:border-white/20 transition-all group cursor-grab">' +
'<div class="flex items-center gap-2">' +
'<span class="text-slate-500 font-mono text-xs">#' + (idx + 1) + '</span>' +
'<span class="text-xs font-semibold capitalize text-white">' + sec + '</span>' +
'</div>' +
'<div class="flex items-center gap-1">' +
'<button type="button" onclick="moveSection(\'' + sec + '\', -1)" class="p-1 hover:text-sky-400 text-xs font-bold">Up</button>' +
'<button type="button" onclick="moveSection(\'' + sec + '\', 1)" class="p-1 hover:text-sky-400 text-xs font-bold">Down</button>' +
'<button type="button" onclick="deleteSection(\'' + sec + '\')" class="p-1 text-red-400/70 hover:text-red-400 text-xs font-bold ml-1">Trash</button>' +
'</div>' +
'</div>'
)).join('');
}
function updateSectionSelectors() {
const newElSec = document.getElementById('new-el-section');
const filterSec = document.getElementById('filter-section-select');
if (newElSec) {
newElSec.innerHTML = siteConfigState.sectionsOrder.map(s => '<option value="' + s + '">' + s + ' Sektion</option>').join('');
}
if (filterSec) {
const current = filterSec.value;
filterSec.innerHTML = '<option value="all">Alle Sektionen</option>' + siteConfigState.sectionsOrder.map(s => '<option value="' + s + '">' + s + '</option>').join('');
filterSec.value = current;
}
}
// --- UIComponent Management: Add, Edit, Delete, Position ---
const addBtn = document.getElementById('add-el-btn');
if (addBtn) {
addBtn.addEventListener('click', () => {
const section = document.getElementById('new-el-section').value;
const slotId = document.getElementById('new-el-slot').value;
const type = document.getElementById('new-el-type').value;
const content = document.getElementById('new-el-content').value || 'Neues Element';
const newComp = {
id: 'el-' + Date.now(),
slotId: section + '-' + slotId,
type,
content,
style: 'primary'
};
siteConfigState.hero.elements.push(newComp);
document.getElementById('new-el-content').value = '';
renderElementsContainer();
});
}
window.deleteElement = function(elId) {
siteConfigState.hero.elements = siteConfigState.hero.elements.filter(e => e.id !== elId);
renderElementsContainer();
};
window.moveElement = function(elId, dir) {
const arr = siteConfigState.hero.elements;
const idx = arr.findIndex(e => e.id === elId);
if (idx === -1) return;
const targetIdx = idx + dir;
if (targetIdx < 0 || targetIdx >= arr.length) return;
const temp = arr[idx];
arr[idx] = arr[targetIdx];
arr[targetIdx] = temp;
renderElementsContainer();
};
window.updateElementContent = function(elId) {
const input = document.getElementById('edit-input-' + elId);
if (!input) return;
const el = siteConfigState.hero.elements.find(e => e.id === elId);
if (el) {
el.content = input.value;
const label = document.getElementById('el-label-' + elId);
if (label) label.textContent = input.value;
}
};
const filterSecSelect = document.getElementById('filter-section-select');
if (filterSecSelect) {
filterSecSelect.addEventListener('change', renderElementsContainer);
}
function renderElementsContainer() {
const container = document.getElementById('elements-container');
const countEl = document.getElementById('elements-count');
if (!container) return;
const filterSec = filterSecSelect ? filterSecSelect.value : 'all';
let els = siteConfigState.hero.elements || [];
if (filterSec !== 'all') {
els = els.filter(e => e.slotId && e.slotId.startsWith(filterSec));
}
if (countEl) countEl.textContent = els.length;
container.innerHTML = els.map((el, idx) => (
'<div data-id="' + el.id + '" class="p-3 rounded-lg bg-slate-900/70 border border-white/10 space-y-2 text-xs group">' +
'<div class="flex items-center justify-between">' +
'<div>' +
'<span class="font-bold text-sky-400 uppercase text-[10px] block">' + el.type + ' (Slot: ' + el.slotId + ')</span>' +
'<span id="el-label-' + el.id + '" class="text-slate-200 font-semibold">' + el.content + '</span>' +
'</div>' +
'<div class="flex items-center gap-1">' +
'<button type="button" onclick="moveElement(\'' + el.id + '\', -1)" class="p-1 hover:text-sky-400 text-xs font-bold" title="Nach oben">Up</button>' +
'<button type="button" onclick="moveElement(\'' + el.id + '\', 1)" class="p-1 hover:text-sky-400 text-xs font-bold" title="Nach unten">Down</button>' +
'<button type="button" onclick="deleteElement(\'' + el.id + '\')" class="p-1 text-red-400/80 hover:text-red-400 text-xs font-bold ml-1" title="Loeschen">Trash</button>' +
'</div>' +
'</div>' +
'<div class="pt-2 border-t border-white/5 flex gap-2">' +
'<input type="text" id="edit-input-' + el.id + '" value="' + el.content.replace(/"/g, '&quot;') + '" class="flex-1 glass-panel bg-slate-950 rounded px-2 py-1 text-xs text-white border border-white/10 focus:border-sky-400" />' +
'<button type="button" onclick="updateElementContent(\'' + el.id + '\')" class="px-2 py-1 rounded bg-sky-400/20 text-sky-400 border border-sky-400/30 text-[10px] font-bold hover:bg-sky-400 hover:text-slate-950 transition-all">Save</button>' +
'</div>' +
'</div>'
)).join('');
}
const stylePresetSelect = document.getElementById('style-preset-select');
if (stylePresetSelect) {
stylePresetSelect.addEventListener('change', () => {
themeConfigState.activePreset = stylePresetSelect.value;
});
}
const saveBtn = document.getElementById('save-editor-btn');
const alertBox = document.getElementById('editor-alert');
if (saveBtn) {
saveBtn.addEventListener('click', async () => {
saveBtn.disabled = true;
saveBtn.textContent = 'Wird gespeichert...';
try {
await fetch('/api/update-config', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(siteConfigState)
});
await fetch('/api/config', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(themeConfigState)
});
alertBox.classList.remove('hidden');
alertBox.textContent = 'Speichern erfolgreich! Live-Preview aktualisiert.';
reloadIframe();
setTimeout(() => alertBox.classList.add('hidden'), 3000);
} catch (err) {
alert('Fehler beim Speichern.');
} finally {
saveBtn.disabled = false;
saveBtn.textContent = 'Live Speichern';
}
});
}
</script>