feat(admin-editor): add elementor-style visual editor with drag-and-drop section manager and device switcher
This commit is contained in:
346
src/pages/admin/editor.astro
Normal file
346
src/pages/admin/editor.astro
Normal file
@@ -0,0 +1,346 @@
|
||||
---
|
||||
import Layout from '../../layouts/Layout.astro';
|
||||
import { loadWaasConfigs } from '../../utils/configLoader';
|
||||
|
||||
const { siteConfig } = 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-ng 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-80 sm:wm96 bg-slate-950/70 border-r border-white/10 p-5 overflow-y-auto space-y-6 shrink-0 z-10 custom-scroll">
|
||||
|
||||
<!-- 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 Sektion</option>
|
||||
<option value="bento">Bento Grid Sektion</option>
|
||||
<option value="services">Leistungen / Services</option>
|
||||
<option value="contact">Kontaktformular Sektion</option>
|
||||
<option value="faq">FAQ Akkordeon Sektion</option>
|
||||
<option value="testimonials">Kundenbewertungen Sektion</option>
|
||||
<option value="cta-banner">CTA Banner Sektion</option>
|
||||
</select>
|
||||
<button type="button" id="add-section-btn" class="px-3 py-2 rounded-lg space-x-1 bg-sky-400/t20 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>
|
||||
|
||||
<!-- Dynamic UI Elements Adder -->
|
||||
<div class="glass-panel rounded-xl p-4 space-y-4">
|
||||
<h3 class="text-xs font-bold text-sky-400 uppercase tracking-wider">
|
||||
UIKomponente Hinzufuegen
|
||||
</h3>
|
||||
|
||||
<div class="space-y-3">
|
||||
<div>
|
||||
<label class="block text-[10px] font-bold text-slate-400 uppercase mb-1">Platz (Slot IM)</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>
|
||||
</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 / Zimmer-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>
|
||||
|
||||
<div>
|
||||
<label class="block text-[10px] font-bold text-slate-400 uppercase mb-1">Animation FX</label>
|
||||
<select id="new-el-anim" 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="none">Keine</option>
|
||||
<option value="spring-fade">spring-fade</option>
|
||||
<option value="hover-bounce">hover-bounce</option>
|
||||
<option value="pulse-glow">pulse-glow</option>
|
||||
</select>
|
||||
</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 auf Platz platzieren
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Elements List in Active Config -->
|
||||
<div class="glass-panel rounded-xl p-4 space-y-3">
|
||||
<h3 class="text-xs font-bold text-sky-400 uppercase tracking-wider">
|
||||
Element-Container ({siteConfig.hero.elements?.length || 0})
|
||||
</h3>
|
||||
<div id="elements-container" class="space-y-2 max-h-60 overflow-y-auto pr-1">
|
||||
{(siteConfig.hero.elements || []).map((el) => (
|
||||
<div class="p-2.5 rounded-lg bg-slate-900/60 border border-white/5 flex items-center justify-between text-xs">
|
||||
<div>
|
||||
<span class="font-bold text-sky-400 uppercase text-[10px] block">{el.type} (Platz: {el.slotId})</span>
|
||||
<span class="text-slate-300">{el.content}</span>
|
||||
</div>
|
||||
<span class="text-[9px] font-mono text-slate-500 bg-white/5 px-2 py-0.5 rounded">ID: {el.id}</span>
|
||||
</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-fulh 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) }}>
|
||||
let siteConfigState = JSON.parse(siteConfigJSON);
|
||||
|
||||
function reloadIframe() {
|
||||
const iframe = document.getElementById('preview-iframe');
|
||||
if (iframe) iframe.src = iframe.src;
|
||||
}
|
||||
|
||||
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';
|
||||
} 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';
|
||||
} 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();
|
||||
} else {
|
||||
alert('Sektion ist bereits im Layout enthalten!');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
window.deleteSection = function(secName) {
|
||||
siteConfigState.sectionsOrder = siteConfigState.sectionsOrder.filter(s => s !== secName);
|
||||
renderSectionsList();
|
||||
};
|
||||
|
||||
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('');
|
||||
initDragAndDrop();
|
||||
}
|
||||
|
||||
function initDragAndDrop() {
|
||||
const container = document.getElementById('sections-list');
|
||||
if (!container) return;
|
||||
let draggedItem = null;
|
||||
|
||||
const items = container.querySelectorAll('.section-item');
|
||||
items.forEach(item => {
|
||||
item.addEventListener('dragstart', (e) => {
|
||||
draggedItem = item;
|
||||
e.dataTransfer.effectAllowed = 'move';
|
||||
item.classList.add('opacity-40');
|
||||
});
|
||||
|
||||
item.addEventListener('dragend', () => {
|
||||
item.classList.remove('opacity-40');
|
||||
draggedItem = null;
|
||||
});
|
||||
|
||||
item.addEventListener('dragover', (e) => {
|
||||
e.preventDefault();
|
||||
e.dataTransfer.dropEffect = 'move';
|
||||
});
|
||||
|
||||
item.addEventListener('drop', (e) => {
|
||||
e.preventDefault();
|
||||
if (draggedItem && draggedItem !== item) {
|
||||
const fromSec = draggedItem.getAttribute('data-section');
|
||||
const toSec = item.getAttribute('data-section');
|
||||
const arr = siteConfigState.sectionsOrder;
|
||||
const fromIdx = arr;<3B>[<5B>\ي<19><><EFBFBD>T<EFBFBD>X<EFBFBD>N<18>ۜ<EFBFBD><1B>YH\<5C><><EFBFBD>[<5B>^ي<1B><>X<EFBFBD>NY<>
|
||||
<19><><EFBFBD>RYOOHLH <09><><1B>YOOHLJH\<5C><><EFBFBD><EFBFBD>X<>J<19><><EFBFBD>RYJN\<5C><><EFBFBD><EFBFBD>X<>J<1B>Y<19><><EFBFBD>T<EFBFBD>X<EFBFBD>N<1C>[<5B>\<5C><>X<EFBFBD>[ۜ<>\<5C>
|
||||
|
||||
NB<>B<>JNJNB<><42>[<5B>]<1C>Y<EFBFBD>[<5B><1C><>
|
||||
|
||||
N<EFBFBD><18>ۜ<EFBFBD>Y<10><1B>H<1B><>[Y[<5B><0B><>][[Y[<5B><10>RY
|
||||
<09>YY[X<><1B><>NY<>
|
||||
Y<10><1B>HY<10><1B><>Y]<5D>[<5B>\<5C>[<5B>\<5C> <09><>X<><58><EFBFBD>
|
||||
|
||||
HO<><18>ۜ<EFBFBD><1C><1B>YH<1B><>[Y[<5B><0B><>][[Y[<5B><10>RY
|
||||
ۙ]<5D>Y[\<5C><1B> <09>K<EFBFBD><4B>[YN<18>ۜ<EFBFBD>\HH<1B><>[Y[<5B><0B><>][[Y[<5B><10>RY
|
||||
ۙ]<5D>Y[]\I<>K<EFBFBD><4B>[YN<18>ۜ<EFBFBD><18>[<5B>H<1B><>[Y[<5B><0B><>][[Y[<5B><10>RY
|
||||
ۙ]<5D>Y[X<>[<5B> <09>K<EFBFBD><4B>[YH ә]Y\<5C>[[Y[<5B> <09><18>ۜ<EFBFBD>[<5B>[HH<1B><>[Y[<5B><0B><>][[Y[<5B><10>RY
|
||||
ۙ]<5D>Y[X[<5B>[I<>K<EFBFBD><4B>[YN<4E>Y<>
|
||||
\<5C>]P<>ۙ<EFBFBD>Y<EFBFBD><59>]K<>\<5C>˙[[Y[<5B><1C>H<1C>]P<>ۙ<EFBFBD>Y<EFBFBD><59>]K<>\<5C>˙[[Y[<5B><1C>H<16>NB<><42><1C>]P<>ۙ<EFBFBD>Y<EFBFBD><59>]K<>\<5C>˙[[Y[<5B>˜\<5C>
|
||||
Y<0E> <09>[I<>
|
||||
<EFBFBD>]K<><4B><EFBFBD><EFBFBD>
|
||||
K<02><1C><1B>Y<02>\K<02><18>[<5B><02>[<5B>[X][ی<0E>[<5B>[K<02><1C>[N<> <09><1C>[X\<5C>IJN<4E><1B><>[Y[<5B><0B><>][[Y[<5B><10>RY
|
||||
ۙ]<5D>Y[X<>[<5B> <09>K<EFBFBD><4B>[YHH <09><><1C>[<5B>\<5C>[[Y[<5B><1C><>Z[<5B>\<5C>
|
||||
NJNB<><42><19>[<5B><>[ۈ<1C>[<5B>\<5C>[[Y[<5B><1C><>Z[<5B>\<5C>
|
||||
H<18>ۜ<EFBFBD><18>Z[<5B>\<5C>H<1B><>[Y[<5B><0B><>][[Y[<5B><10>RY
|
||||
<09>[[Y[<5B><1C>X<EFBFBD>Z[<5B>\<5C><>NY<>
|
||||
X<>Z[<5B>\<5C>H<1C>]\<5C><><18>ۜ<EFBFBD>[<1C>H<1C>]P<>ۙ<EFBFBD>Y<EFBFBD><59>]K<>\<5C>˙[[Y[<5B><1C><16>N<18>ۜ<EFBFBD>Z[<5B>\<5C><>[<5B><>\<5C>SH[˛X\
|
||||
[O<>
|
||||
<02> <09>]<5D><18>\<5C><>H<EFBFBD>L<><4C>H<1C><>[<5B>Y[<19><18><>\<5C>]KNL͌<18>ܙ\<5C><18>ܙ\<5C>]<5D>]K<>H<19>^][\<5C>X<EFBFBD>[<5B>\<5C><1A>\<5C>Y<>KX<4B>]<1D>Y[<5B>^^ȏ<><C88F>
|
||||
<09>]<5D><><EFBFBD>
|
||||
<09><1C>[<5B><18>\<5C><>H<EFBFBD><48>X<><58>^\<5C><>KM\\<5C><>\<5C>H^V<>LH<18><1B><>ȏ<EFBFBD><C88F>
|
||||
<EFBFBD>[<0B>\H
|
||||
<EFBFBD> <09>
|
||||
]<1E><> <09>
|
||||
<EFBFBD>[<0B><><1B>Y
|
||||
<EFBFBD> <09>O<0B><>[<5B><><EFBFBD>
|
||||
<09><1C>[<5B><18>\<5C><>H<EFBFBD>^\<5C>]KL<4B><08><><EFBFBD>
|
||||
<EFBFBD>[<0B><>[<5B>
|
||||
<EFBFBD> <09><0B><>[<5B><><EFBFBD>
|
||||
<09><0B>]<5D><><EFBFBD>
|
||||
<09><1C>[<5B><18>\<5C><>H<EFBFBD>^V<>\H<19>[[ۛ<>^\<5C>]KML<18><>]<5D>]K<>HL<>KL<0B>H<1C><>[<5B>Y<08><><EFBFBD>
|
||||
<EFBFBD> <09>Q<0E> <09>
|
||||
<EFBFBD>[<0B>Y
|
||||
<EFBFBD> <09><0B><>[<5B><><EFBFBD>
|
||||
<09><0B>]<5D><>
|
||||
JK<EFBFBD><EFBFBD><EFBFBD>[<5B> <09><>NB<><42><18>ۜ<EFBFBD><1C>]<5D>P<EFBFBD><1B>H<1B><>[Y[<5B><0B><>][[Y[<5B><10>RY
|
||||
<09><>]<5D>KYY]܋X<DC8B><1B><>N<18>ۜ<EFBFBD>[\<5C><10><>H<1B><>[Y[<5B><0B><>][[Y[<5B><10>RY
|
||||
<09>Y]܋X[\<5C> <09>N<4E>Y<>
|
||||
<1C>]<5D>P<EFBFBD><1B>H<1C>]<5D>P<EFBFBD><1B><>Y]<5D>[<5B>\<5C>[<5B>\<5C> <09><>X<><58><EFBFBD>\<5C>[<5B><>
|
||||
|
||||
HO<><1C>]<5D>P<EFBFBD><1B><>\<5C>X<EFBFBD>YH<1C>YN<1C>]<5D>P<EFBFBD><1B><>^<10>[<5B>H <09><>\<5C><19>\<5C>ZX<5A>\<5C><0B><><EFBFBD><EFBFBD><EFBFBD><1C>H<18>ۜ<EFBFBD><1C>\<5C>H]<5D>Z]<19>]<18>
|
||||
<09><>\K<>\]KX<4B>ۙ<EFBFBD>Y<EFBFBD><59>Y]<1B><0E> <09><13><> <09><02>XY\<5C>Έ<1E> <09><>[<5B>U\IΈ <09>\X<>][ۋڜ<DB8B>ۉ<EFBFBD>K<02><18><>N<><12><>Ӌ<EFBFBD><D38B><1C>[<5B><>Y<EFBFBD>J<1C>]P<>ۙ<EFBFBD>Y<EFBFBD><59>]JB<4A>JN<18>ۜ<EFBFBD>]HH]<5D>Z]<1C>\˚<><CB9A>ۊ
|
||||
N<EFBFBD>Y<>
|
||||
]K<><4B>X<EFBFBD><58>\<5C><>H[\<5C><10><><0B><>\<5C><>\<5C><0B><>[[ݙJ <09>Y[<5B><>N[\<5C><10><><0B>^<10>[<5B>H <09><>ZX<5A>\<5C><>\<5C><><EFBFBD>ܙZX<5A>H]<5D>KT<1C>]<5D>Y]<5D>Z<>X[\<5C>Y\<5C><0B><><1C>[<1B>YY<><59>[YJ
|
||||
N<1C>][Y[<5B>]
|
||||
|
||||
|
||||
HO<>[\<5C><10><><0B><>\<5C><>\<5C><0B>Y
|
||||
<09>Y[<5B><>K<0C>
|
||||
NH[<1C>H[\<5C>
|
||||
љZ\<5C><18>Z[H<14>ZX<5A>\<5C><><EFBFBD> <09>
|
||||
<EFBFBD>]K<>Y\<5C><>Y<EFBFBD>JNB<>H<18>]<18>
|
||||
\<5C><>H[\<5C>
|
||||
љZ\<5C><18>Z[H<14>ZX<5A>\<5C><><EFBFBD><EFBFBD>NH<19>[<5B>[H<1C>]<5D>P<EFBFBD><1B><>\<5C>X<EFBFBD>YH<19>[<1C>N<1C>]<5D>P<EFBFBD><1B><>^<10>[<5B>H <09>]<5D>H<14>ZX<5A>\<5C><><EFBFBD>B<>JNB<><0B><>ܚ\<0F>
|
||||
224
src/pages/admin/index.astro
Normal file
224
src/pages/admin/index.astro
Normal file
@@ -0,0 +1,224 @@
|
||||
---
|
||||
import Layout from '../../layouts/Layout.astro';
|
||||
import { loadWaasConfigs } from '../../utils/configLoader';
|
||||
import { THEME_PRESETS } from '../../utils/themePresets';
|
||||
|
||||
const { siteConfig, themeConfig, smtpConfig } = loadWaasConfigs();
|
||||
const presets = Object.values(THEME_PRESETS);
|
||||
---
|
||||
|
||||
<Layout title="Waas Admin Dashboard | Layout and Config Management">
|
||||
<div class="min-h-screen bg-[#0B0F19] text-white p-6 sm:p-10 font-sans">
|
||||
<div class="max-w-6xl mx-auto space-y-10">
|
||||
|
||||
<!-- Admin Header -->
|
||||
<div class="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4 border-b border-white/10 pb-6">
|
||||
<div>
|
||||
<span class="text-xs font-mono text-sky-400 uppercase tracking-widest">N& D Waas Engine</span>
|
||||
<h1 class="text-3xl font-extrabold tracking-tight mt-1">Layout & Config Dashboard</h1>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<a href="/" target="_blank" class="px-4 py-2 rounded-xl glass-panel text-xs font-semibold hover:border-sky-400 transition-all flex items-center gap-2">
|
||||
Live Seite Ansehen
|
||||
</a>
|
||||
<button id="save-all-btn" class="px-6 py-2.5 rounded-xl bg-sky-400 text-slate-95 font-bold text-xs uppercase tracking-wider hover:bg-white transition-all shadow-lg shadow-sky-400/20 active:scale-95">
|
||||
Einstellungen Speichern
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="admin-alert" class="hidden p-4 rounded-xl text-xs font-mono text-center transition-all"></div>
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||
|
||||
<!-- Main Form Column -->
|
||||
<div class="lg:col-span-2 space-y-8">
|
||||
|
||||
<!-- Site Info -->
|
||||
<div class="glass-panel rounded-2xl p-6 space-y-4">
|
||||
<h2 class="text-lg font-bold text-sky-400 uppercase tracking-wider flex items-center gap-2">
|
||||
Allgemeine Informationen
|
||||
</h2>
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label class="block text-xs font-bold text-slate-400 mb-1">Webseiten-Name</label>
|
||||
<input type="text" id="siteName" value={siteConfig.siteName} class="w-full glass-panel bg-slate-950/60 rounded-xl p-3 text-xs text-white focus:outline-none focus:border-sky-400" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-bold text-slate-400 mb-1">Meta Beschreibung</label>
|
||||
<textarea id="metaDescription" rows="2" class="w-full glass-panel bg-slate-950/60 rounded-xl p-3 text-xs text-white focus:outline-none focus:border-sky-400 resize-none">{siteConfig.metaDescription}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Hero Config -->
|
||||
<div class="glass-panel rounded-3xl p-6 space-y-4">
|
||||
<h2 class="text-lg font-bold text-sky-400 uppercase tracking-wider flex items-center gap-2">
|
||||
Hero Sektion
|
||||
</h2>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-xs font-bold text-slate-400 mb-1">Badge</label>
|
||||
<input type="text" id="heroBadge" value={siteConfig.hero.badge} class="w-full glass-panel bg-slate-950/60 rounded-xl p-3 text-xs text-white focus:outline-none focus:border-sky-400" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-bold text-slate-400 mb-1">Hauptueberschrift (Title)</label>
|
||||
<input type="text" id="heroTitle" value={siteConfig.hero.title} class="w-full glass-panel bg-slate-950/60 rounded-xl p-3 text-xs text-white focus:outline-none focus:border-sky-400" />
|
||||
</div>
|
||||
<div class="sm:col-span-2">
|
||||
<label class="block text-xs font-bold text-slate-400 mb-1">Unterueberschrift (Subtitle)</label>
|
||||
<textarea id="heroSubtitle" rows="2" class="w-full glass-panel bg-slate-950/60 rounded-xl p-3 text-xs text-white focus:outline-none focus:border-sky-400 resize-none">{siteConfig.hero.subtitle}</textarea>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-bold text-slate-400 mb-1">CTA2 Text</label>
|
||||
<input type="text" id="ctaPrimaryText" value={siteConfig.hero.ctaPrimaryText} class="w-full glass-panel bg-slate-950-60 rounded-xl p-3 text-xs text-white focus:outline-none focus:border-sky-400" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-bold text-slate-400 mb-1">CTA2 Link</label>
|
||||
<input type="text" id="ctaPrimaryLink" value={siteConfig.hero.ctaPrimaryLink} class="w-full glass-panel bg-slate-950-60 rounded-xl p-3 text-xs text-white focus:outline-none focus:border-sky-400" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Section Order Management -->
|
||||
<div class="glass-panel rounded-2xl p-6 space-y-4">
|
||||
<h2 class="text-lg font-bold text-sky-400 uppercase tracking-wider flex items-center gap-2">
|
||||
Sektionen Reihenfolge (Layout)
|
||||
</h2>
|
||||
<p class="text-xs text-slate-400">Kommagetrennte Liste der aktiv gerenderten Sektionen:</p>
|
||||
<input type="text" id="sectionsOrder" value={siteConfig.sectionsOrder.join(', ')} class="w-full glass-panel bg-slate-950/60 rounded-xl p-3 text-xs text-white font-mono focus:outline-none focus:border-sky-400" />
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Sidebar Column -->
|
||||
<div class="space-y-8">
|
||||
|
||||
<!-- Theme Preset Picker -->
|
||||
<div class="glass-panel rounded-2xl p-6 space-y-4">
|
||||
<h2 class="text-lg font-bold text-sky-400 uppercase tracking-wider flex items-center gap-2">
|
||||
Theme Preset (10 Styles)
|
||||
</h2>
|
||||
<div class="space-y-2 max-h-[320px] overflow-y-auto pr-1">
|
||||
{presets.map((p) => (
|
||||
<label class={`flex items-center justify-between p-3 rounded-xl border transition-all cursor-pointer ${themeConfig.activePreset === p.id ? 'border-sky-400 bg-sky-400/10' : 'border-white/5 hover:border-white/20 bg-slate-950/40'}`}>
|
||||
<div class="flex items-center gap-3">
|
||||
<input type="radio" name="activePreset" value={p.id} checked={themeConfig.activePreset === p.id} class="text-sky-400 focus:ring-0" />
|
||||
<span class="text-xs font-semibold">{p.name}</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-1">
|
||||
<span class="w-3 h-3 rounded-full inline-block border border-white/20" style={`background-color: ${p.colors.bg};`}></span>
|
||||
<span class="w-3 h-3 rounded-full inline-block" style={`background-color: ${p.colors.accent};`}></span>
|
||||
</div>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- SMTP Config -->
|
||||
<div class="glass-panel rounded-3xl p-6 space-y-4">
|
||||
<h2 class="text-lg font-bold text-sky-400 uppercase tracking-wider flex items-center gap-2">
|
||||
SMTP E-Mail Einstellungen
|
||||
</h2>
|
||||
<div class="space-y-3">
|
||||
<div>
|
||||
<label class="block text-[10px] font-bold text-slate-400 uppercase mb-1">SMTP Host</label>
|
||||
<input type="text" id="smtpHost" value={smtpConfig.host} class="w-full glass-panel bg-slate-950/60 rounded-xl p-2.5 text-xs text-white focus:outline-none focus:border-sky-400" />
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<div>
|
||||
<label class="block text-[10px] font-bold text-slate-400 uppercase mb-1">Port</label>
|
||||
<input type="number" id="smtpPort" value={smtpConfig.port} class="w-full glass-panel bg-slate-950/60 rounded-xl p-2.5 text-xs text-white focus:outline-none focus:border-sky-400" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-[10px] font-bold text-slate-400 uppercase mb-1">User</label>
|
||||
<input type="text" id="smtpUser" value={smtpConfig.user} class="w-full glass-panel bg-slate-950-60 rounded-xl p-2.5 text-xs text-white focus:outline-none focus:border-sky-400" />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-[10px] font-bold text-slate-400 uppercase mb-1">Passwort</label>
|
||||
<input type="password" id="smtpPass" value={smtpConfig.pass} class="w-full glass-panel bg-slate-950-60 rounded-xl p-2.5 text-xs text-white focus:outline-none focus:border-sky-400" />
|
||||
</div>
|
||||
<label class="flex items-center gap-2 pt-2 cursor-pointer">
|
||||
<input type="checkbox" id="smtpIsConfigured" checked={smtpConfig.isConfigured} class="rounded text-sky-400 focus:ring-0" />
|
||||
<span class="text-xs text-slate-300 font-semibold">SMTP Aktivieren (Live-Betrieb)</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</Layout>
|
||||
|
||||
<script is:inline>
|
||||
const saveBtn = document.getElementById('save-all-btn');
|
||||
const alertBox = document.getElementById('admin-alert');
|
||||
|
||||
if (saveBtn) {
|
||||
saveBtn.addEventListener('click', async () => {
|
||||
saveBtn.disabled = true;
|
||||
saveBtn.textContent = 'Wird gespeichert...';
|
||||
|
||||
const selectedPreset = document.querySelector('input[name="activePreset"]:checked')?.value || 'corporateDark';
|
||||
const sections = document.getElementById('sectionsOrder').value.split(',').map(s => s.trim()).filter(Boolean);
|
||||
|
||||
const getVal = (id) => (document.getElementById(id))?.value || '';
|
||||
const getNum = (id) => parseInt(document.getElementById(id)?.value || '0') || 587;
|
||||
const getBool = (id) => !!(document.getElementById(id))?.checked;
|
||||
|
||||
const payload = {
|
||||
siteConfig: {
|
||||
siteName: getVal('siteName'),
|
||||
metaDescription: getVal('metaDescription'),
|
||||
hero: {
|
||||
badge: getVal('heroBadge'),
|
||||
title: getVal('heroTitle'),
|
||||
subtitle: getVal('heroSubtitle'),
|
||||
ctaPrimaryText: getVal('ctaPrimaryText'),
|
||||
ctaPrimaryLink: getVal('ctaPrimaryLink')
|
||||
},
|
||||
sectionsOrder: sections
|
||||
},
|
||||
themeConfig: {
|
||||
activePreset: selectedPreset
|
||||
},
|
||||
smtpConfig: {
|
||||
host: getVal('smtpHost'),
|
||||
port: getNum('smtpPort'),
|
||||
user: getVal('smtpUser'),
|
||||
pass: getVal('smtpPass'),
|
||||
isConfigured: getBool('smtpIsConfigured')
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/config', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
const result = await res.json();
|
||||
alertBox.classList.remove('hidden', 'bg-emerald-500/10', 'text-emerald-400', 'bg-red-500/10', 'text-red-400');
|
||||
if (result.success) {
|
||||
alertBox.classList.add('bg-emerald-500/10', 'text-emerald-400');
|
||||
alertBox.textContent = 'Konfiguration erfolgeich gespeichert! SSR empfaengt die Aenderungen sofort.';
|
||||
} else {
|
||||
alertBox.classList.add('bg-red-500/10', 'text-red-400');
|
||||
alertBox.textContent = 'Fehler: ' + result.message;
|
||||
}
|
||||
} catch (e) {
|
||||
alertBox.classList.remove('hidden');
|
||||
alertBox.classList.add('bg-red-500/10', 'text-emerald-400');
|
||||
alertBox.textContent = 'Fehler beim Speichern.';
|
||||
} finally {
|
||||
saveBtn.disabled = false;
|
||||
saveBtn.textContent = 'Einstellungen Speichern';
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
Reference in New Issue
Block a user