Files
ndsolutionswebsite/src/pages/admin/editor.astro
Daniel S 0c073983e9
All checks were successful
Production Build & Deploy / build-and-deploy (push) Successful in 1m3s
feat(editor): add section manipulation APIs and interactive sidebar controls
2026-08-09 21:08:35 +02:00

185 lines
8.4 KiB
Plaintext

---
// src/pages/admin/editor.astro
import Layout from '../../layouts/Layout.astro';
import { loadWaasConfigs } from '../../utils/configLoader';
import { THEME_PRESETS } from '../../utils/themePresets';
export const prerender = false;
const { searchParams } = Astro.url;
const pageId = searchParams.get('page_id');
// Lade den aktuellen Entwurf oder Fallback auf Live Config
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 previewUrl = `${targetPage?.slug || '/'}?preview=draft`;
---
<Layout title={`Editor: ${targetPage?.title || 'Baukasten'} | N&D Admin`}>
<div class="h-screen w-screen flex overflow-hidden bg-slate-950 font-sans">
<aside class="w-80 border-r border-slate-800 bg-slate-900 flex flex-col justify-between z-20">
<div class="p-4 space-y-6 overflow-y-auto">
<div class="flex items-center justify-between border-b border-slate-800 pb-4">
<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>
</div>
<div class="space-y-2">
<label class="text-xs font-semibold text-slate-400 uppercase tracking-wider block">Design-Preset</label>
<select id="preset-selector" class="w-full bg-slate-800 border border-slate-700 rounded-lg p-2.5 text-xs text-white focus:outline-none focus:border-sky-500">
{Object.entries(THEME_PRESETS).map(([key, preset]) => (
<option value={key}>{preset.name}</option>
))}
</select>
</div>
<div class="space-y-2">
<label class="text-xs font-semibold text-slate-400 uppercase tracking-wider block">Farbschema</label>
<div class="grid grid-cols-2 gap-2">
<button id="btn-mode-dark" class="px-3 py-2 bg-slate-800 border border-slate-700 rounded-lg text-xs font-bold text-white hover:border-sky-500 transition-colors">🌙 Dark Mode</button>
<button id="btn-mode-light" class="px-3 py-2 bg-slate-800 border border-slate-700 rounded-lg text-xs font-bold text-slate-300 hover:border-sky-500 transition-colors">☀️ Light Mode</button>
</div>
</div>
<div class="space-y-2 pt-4 border-t border-slate-800">
<label class="text-xs font-semibold text-slate-400 uppercase tracking-wider block">Sektion hinzufügen</label>
<div class="flex gap-2">
<select id="select-new-section-type" class="flex-1 bg-slate-800 border border-slate-700 rounded-lg p-2 text-xs text-white focus:outline-none focus:border-sky-500">
<option value="HeroSection">Hero Section</option>
<option value="BentoGrid">Bento Grid</option>
<option value="ContactSection">Kontakt Formular</option>
</select>
<button id="btn-add-section" class="bg-sky-500 hover:bg-sky-400 text-slate-950 font-bold px-3 py-2 rounded-lg text-xs transition-colors">
+ Hinzufügen
</button>
</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}>
<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-move-up hover:text-sky-400 px-1" title="Nach oben">⬆️</button>
<button class="btn-move-down hover:text-sky-400 px-1" title="Nach unten">⬇️</button>
<button class="btn-delete-section hover:text-red-400 px-1" title="Sektion löschen">🗑️</button>
</div>
</div>
</div>
))}
</div>
</div>
</div>
<div class="p-4 border-t border-slate-800 bg-slate-950/50 space-y-2">
<button id="btn-save-draft" class="w-full bg-slate-800 hover:bg-slate-700 text-slate-200 font-semibold py-2.5 rounded-lg text-xs transition-colors border border-slate-700">
💾 Entwurf speichern
</button>
<button id="btn-publish" class="w-full bg-sky-500 hover:bg-sky-400 text-slate-950 font-bold py-2.5 rounded-lg text-xs transition-colors shadow-lg shadow-sky-500/20">
🚀 Live Veröffentlichen
</button>
</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>
</div>
<iframe
id="preview-iframe"
src={previewUrl}
class="w-full h-full border-none bg-slate-950"
title="Live Website Preview"
></iframe>
</main>
</div>
</Layout>
<script>
const iframe = document.getElementById('preview-iframe') as HTMLIFrameElement;
const statusBadge = document.getElementById('editor-status-badge');
// 1. PostMessage helper
function sendToIframe(type: string, payload: any) {
if (iframe?.contentWindow) {
iframe.contentWindow.postMessage({ type, payload }, '*');
}
}
// 2. Draft Speichern
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 = 'Entwurf gespeichert';
statusBadge.className = 'text-sky-400 bg-sky-950/60 border border-sky-800/60 px-2 py-0.5 rounded text-[10px]';
}
});
// 3. Veröffentlichen
document.getElementById('btn-publish')?.addEventListener('click', async () => {
const res = await fetch('/api/editor/publish', { method: 'POST' });
if (res.ok && statusBadge) {
statusBadge.textContent = 'Live Veröffentlicht!';
statusBadge.className = 'text-emerald-400 bg-emerald-950/60 border border-emerald-800/60 px-2 py-0.5 rounded text-[10px]';
sendToIframe('RELOAD_CANVAS', {});
}
});
// 4. Preset Wechsel
document.getElementById('preset-selector')?.addEventListener('change', (e: any) => {
sendToIframe('THEME_UPDATE', { preset: e.target.value });
});
// 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');
const res = await fetch('/api/editor/add-section', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ page_id: pageId, section_type: sectionType })
});
if (res.ok) {
window.location.reload();
}
});
// 6. Event: Sektion löschen
document.querySelectorAll('.btn-delete-section').forEach(btn => {
btn.addEventListener('click', async (e: any) => {
const card = e.target.closest('[data-section-id]');
const sectionId = card?.getAttribute('data-section-id');
const urlParams = new URLSearchParams(window.location.search);
const pageId = urlParams.get('page_id');
const res = await fetch('/api/editor/delete-section', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ page_id: pageId, section_id: sectionId })
});
if (res.ok) {
window.location.reload();
}
});
});
</script>