Compare commits
33 Commits
be071ff82f
...
staging
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1939563ef2 | ||
|
|
698716c34e | ||
|
|
488e024451 | ||
|
|
9c166817fa | ||
|
|
4b7f3f554b | ||
|
|
db2f607e5a | ||
|
|
950497311c | ||
|
|
28473bbcdc | ||
|
|
fc2017b210 | ||
|
|
831a0d53f0 | ||
|
|
9284717a80 | ||
|
|
4d072864bc | ||
|
|
e932495742 | ||
|
|
6042c5ad23 | ||
|
|
041cf20ca3 | ||
|
|
4355bb8406 | ||
|
|
30b407d2e8 | ||
|
|
591eba43c1 | ||
|
|
895fcd4323 | ||
|
|
adefc89ab2 | ||
|
|
aec7c78fb0 | ||
|
|
10d534e836 | ||
|
|
15b2a34957 | ||
|
|
baa639a9b2 | ||
|
|
5e9b69c319 | ||
|
|
df6bb52ab0 | ||
|
|
11f8e33818 | ||
|
|
da50ac468b | ||
|
|
d5b66ff830 | ||
|
|
e8f332da43 | ||
|
|
334c6723e9 | ||
|
|
bb96b0049c | ||
|
|
0a1aa5030d |
@@ -1,104 +1,32 @@
|
||||
{
|
||||
"siteName": "N&D IT Solutions",
|
||||
"metaDescription": "Webdesign & Digital Solutions",
|
||||
"hero": {
|
||||
"badge": "WaaS",
|
||||
"title": "Maßgeschneiderte Webseiten. Digital im Griff.",
|
||||
"subtitle": "Wir machen Zeugs",
|
||||
"ctaPrimaryText": "Projekt anfragen",
|
||||
"ctaPrimaryLink": "#contact",
|
||||
"ctaSecondaryText": "Mehr erfahren",
|
||||
"ctaSecondaryLink": "#services",
|
||||
"elements": [
|
||||
{
|
||||
"id": "el-1",
|
||||
"type": "badge",
|
||||
"content": " WaaS Engine"
|
||||
},
|
||||
{
|
||||
"id": "el-2",
|
||||
"type": "button",
|
||||
"content": "Neuer Button",
|
||||
"link": "#contact",
|
||||
"animation0": "spring-fade",
|
||||
"style": "primary"
|
||||
}
|
||||
]
|
||||
"metaDescription": "High-Performance Webdesign & Digital Solutions - 100% DSGVO-konform.",
|
||||
"is_demo_mode": false,
|
||||
"header": {
|
||||
"brandName": "N&D",
|
||||
"brandHighlight": "i-t SOLUTIONS",
|
||||
"ctaText": "Erstgespräch buchen",
|
||||
"ctaLink": "#contact",
|
||||
"showThemeToggle": true,
|
||||
"glassEffect": true
|
||||
},
|
||||
"bento": {
|
||||
"sectionTitle": "High-End Features & Leistung",
|
||||
"sectionSubtitle": "Modernste Technologie & Höchster Datenschutz vereint.",
|
||||
"cards": [
|
||||
{
|
||||
"id": "card-1",
|
||||
"title": "Ultra Fast SSR",
|
||||
"description": "Sub-100ms Ladezeiten dank Astro Hybrid SSR Architecture. Google Core Web Vitals > 95%.",
|
||||
"colSpan": 2,
|
||||
"badge": "Performance",
|
||||
"icon": "zap"
|
||||
},
|
||||
{
|
||||
"id": "card-2",
|
||||
"title": "100% DSGVO-Sicher",
|
||||
"description": "Keine Cookies, keine Tracking-Skripte, Zero Third-Party Requests.",
|
||||
"colSpan": 1,
|
||||
"badge": "Datenschutz",
|
||||
"icon": "shield"
|
||||
},
|
||||
{
|
||||
"id": "card-3",
|
||||
"title": "Liquid Glass UI",
|
||||
"description": "Moderne Ästhetik mit matte Boxen, Aurora Glows und physischen Kanten-Effekten.",
|
||||
"colSpan": 1,
|
||||
"badge": "Design",
|
||||
"icon": "layers"
|
||||
},
|
||||
{
|
||||
"id": "card-4",
|
||||
"title": "Sorgenfrei-Paket",
|
||||
"description": "Hosting, SSL, Wartung und regelmäßige Inhalts-Updates komplett inklusive.",
|
||||
"colSpan": 2,
|
||||
"badge": "Full-Service",
|
||||
"icon": "check"
|
||||
}
|
||||
]
|
||||
"footer": {
|
||||
"brandDescription": "Ihr Partner für professionelles Webdesign, Website-Entwicklung und lokale SEO.",
|
||||
"copyrightText": "N&D i-t SOLUTIONS. Alle Rechte vorbehalten.",
|
||||
"showLegalLinks": true
|
||||
},
|
||||
"contact": {
|
||||
"title": "Bereit für Ihr Projekt?",
|
||||
"subtitle": "Schreiben Sie uns direkt. Wir antworten innerhalb von 24 Stunden persönlich.",
|
||||
"recipientEmail": "kontakt@ndsolutions.de",
|
||||
"phone": "+49 (0) 123 456789",
|
||||
"address": "Deutschland"
|
||||
"site_info": {
|
||||
"title": "N&D IT Solutions",
|
||||
"homepage_id": "page_home"
|
||||
},
|
||||
"sectionsOrder": [
|
||||
"contact",
|
||||
"bento",
|
||||
"hero"
|
||||
],
|
||||
"sectionsDetails": [
|
||||
"pages": [
|
||||
{
|
||||
"id": "hero",
|
||||
"type": "hero",
|
||||
"title": "Hero Sektion",
|
||||
"elements": [
|
||||
{
|
||||
"id": "el-1",
|
||||
"type": "badge",
|
||||
"content": " WaaS Engine"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "bento",
|
||||
"type": "bento",
|
||||
"title": "Bento Grid",
|
||||
"elements": []
|
||||
},
|
||||
{
|
||||
"id": "contact",
|
||||
"type": "contact",
|
||||
"title": "Contact Section",
|
||||
"elements": []
|
||||
"id": "page_home",
|
||||
"slug": "/",
|
||||
"title": "Startseite",
|
||||
"is_published": true,
|
||||
"sections": []
|
||||
}
|
||||
]
|
||||
],
|
||||
"updated_at": "2026-08-09T22:28:31.200Z"
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
{
|
||||
"activePreset": "amberGlow",
|
||||
"activePreset": "midnightBlue",
|
||||
"customColors": {
|
||||
"bg": "#0B0F19",
|
||||
"accent": "#38BDF8"
|
||||
"bg": "#0b0f19",
|
||||
"accent": "#38bdf8",
|
||||
"text": "#e0e0e0",
|
||||
"border": ""
|
||||
}
|
||||
}
|
||||
BIN
app/data/waas.db
Normal file
BIN
app/data/waas.db
Normal file
Binary file not shown.
@@ -8,6 +8,11 @@ echo "🚀 [WaaS Container Entrypoint] Prüfe Daten-Volume unter ${DATA_DIR}..."
|
||||
# 1. Sicherstellen, dass das Volume-Verzeichnis existiert
|
||||
mkdir -p "$DATA_DIR"
|
||||
|
||||
# 1.5 SQLite DB initialisieren (falls nicht vorhanden)
|
||||
if [ ! -f "$DATA_DIR/waas.db" ]; then
|
||||
echo "🗄️ Keine SQLite-Datenbank waas.db gefunden. Initialisierung wird beim Anwendungsstart durchgeführt..."
|
||||
fi
|
||||
|
||||
# 2. site.config.json Seeding
|
||||
if [ ! -f "$DATA_DIR/site.config.json" ]; then
|
||||
echo "📄 Keine site.config.json gefunden. Erstelle Standard-Konfiguration..."
|
||||
|
||||
@@ -6,17 +6,54 @@
|
||||
|
||||
console.log('[N&D Editor Bridge] Active on preview canvas');
|
||||
|
||||
// Optischer Hinweis-Banner im Bearbeitungsmodus
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const banner = document.createElement('div');
|
||||
banner.id = 'editor-preview-banner';
|
||||
banner.style.cssText = 'position: fixed; top: 0; left: 0; right: 0; z-index: 99999; background: #d97706; color: #0f172a; text-align: center; padding: 6px 12px; font-size: 12px; font-weight: 700; font-family: monospace; letter-spacing: 0.5px; box-shadow: 0 2px 10px rgba(0,0,0,0.3); pointer-events: none; opacity: 0.95;';
|
||||
banner.innerHTML = '🛠️ BEARBEITUNGSMODUS (VORSCHAU / ENTWURF) — Änderungen sind noch nicht live!';
|
||||
document.body.appendChild(banner);
|
||||
const currentPadding = parseInt(document.body.style.paddingTop || '0', 10);
|
||||
document.body.style.paddingTop = (currentPadding + 30) + 'px';
|
||||
});
|
||||
|
||||
window.addEventListener('message', (event) => {
|
||||
const { type, payload } = event.data || {};
|
||||
|
||||
if (type === 'THEME_UPDATE') {
|
||||
// Injiziert CSS-Variablen live in den DOM (<head>)
|
||||
const root = document.documentElement;
|
||||
if (payload.colors) {
|
||||
Object.entries(payload.colors).forEach(([key, val]) => {
|
||||
root.style.setProperty(`--${key}`, val as string);
|
||||
root.style.setProperty(`--${key}`, val);
|
||||
});
|
||||
}
|
||||
|
||||
// Live Section DOM Patching
|
||||
if (payload.sectionId && payload.settings) {
|
||||
const secElement = document.querySelector(`[data-section-id="${payload.sectionId}"], #${payload.sectionId}`) || document.querySelector('section');
|
||||
if (secElement) {
|
||||
const { title, subtitle, cta_text, bg_color, accent_color } = payload.settings;
|
||||
|
||||
if (title !== undefined) {
|
||||
const h1OrH2 = secElement.querySelector('h1, h2, h3');
|
||||
if (h1OrH2) h1OrH2.textContent = title;
|
||||
}
|
||||
if (subtitle !== undefined) {
|
||||
const p = secElement.querySelector('p');
|
||||
if (p) p.textContent = subtitle;
|
||||
}
|
||||
if (cta_text !== undefined) {
|
||||
const btn = secElement.querySelector('a, button');
|
||||
if (btn) btn.textContent = cta_text;
|
||||
}
|
||||
if (bg_color !== undefined && bg_color !== '') {
|
||||
secElement.style.backgroundColor = bg_color;
|
||||
}
|
||||
if (accent_color !== undefined && accent_color !== '') {
|
||||
secElement.style.setProperty('--accent-color', accent_color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (type === 'RELOAD_CANVAS') {
|
||||
@@ -25,3 +62,4 @@
|
||||
}
|
||||
});
|
||||
})();
|
||||
|
||||
|
||||
@@ -18,8 +18,9 @@ const currentYear = new Date().getFullYear();
|
||||
<!-- Brand logo/name -->
|
||||
<div>
|
||||
<a href="#" class="text-lg font-bold tracking-wider text-slate-900 dark:text-white">
|
||||
{siteConfig.header?.brandName || 'N&D'} <span class="text-brand-arcticBlue">{siteConfig.header?.brandHighlight || 'i-t SOLUTIONS'}</span>
|
||||
{siteConfig.header?.brandName || 'N&D'} <span class="text-accent">{siteConfig.header?.brandHighlight || 'i-t SOLUTIONS'}</span>
|
||||
</a>
|
||||
|
||||
<p class="mt-2 text-xs text-slate-500 dark:text-gray-500 max-w-sm">
|
||||
{footer.brandDescription}
|
||||
</p>
|
||||
|
||||
@@ -21,10 +21,11 @@ const header = siteConfig.header || {
|
||||
<!-- Logo -->
|
||||
<a href="#" class="flex items-center gap-2 group">
|
||||
<span class="text-lg sm:text-xl font-bold tracking-wider text-slate-900 dark:text-white select-none transition-colors duration-300">
|
||||
{header.brandName || 'N&D'} <span class="text-brand-arcticBlue">{header.brandHighlight || 'i-t SOLUTIONS'}</span>
|
||||
{header.brandName || 'N&D'} <span class="text-accent">{header.brandHighlight || 'i-t SOLUTIONS'}</span>
|
||||
</span>
|
||||
</a>
|
||||
|
||||
|
||||
<!-- Desktop Nav Links -->
|
||||
<nav class="hidden md:flex items-center gap-8">
|
||||
<a href="#services" class="text-xs tracking-wider uppercase font-medium text-slate-600 dark:text-zinc-400 hover:text-brand-arcticBlue dark:hover:text-brand-arcticBlue transition-colors">Leistungen</a>
|
||||
|
||||
112
src/components/admin/AdminSidebar.astro
Normal file
112
src/components/admin/AdminSidebar.astro
Normal file
@@ -0,0 +1,112 @@
|
||||
---
|
||||
// src/components/admin/AdminSidebar.astro
|
||||
const { currentPath = '' } = Astro.props;
|
||||
|
||||
const navItems = [
|
||||
{ href: '/admin', label: 'Allgemein & Konfig', icon: '🎛️' },
|
||||
{ href: '/admin/pages', label: 'Seiten-Verwaltung', icon: '📄' },
|
||||
{ href: '/admin/settings', label: 'Stammdaten & SEO', icon: '⚙️' },
|
||||
];
|
||||
|
||||
---
|
||||
|
||||
<!-- Burger Button (Mobile / Sidebar Toggle) -->
|
||||
<button
|
||||
id="admin-menu-toggle"
|
||||
type="button"
|
||||
aria-label="Admin Menü öffnen"
|
||||
class="fixed top-3 left-3 z-50 p-2.5 rounded-xl bg-slate-900/90 border border-white/10 text-white hover:border-sky-400 focus:outline-none focus:ring-2 focus:ring-sky-400 backdrop-blur-md transition-all"
|
||||
>
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"></path>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<!-- Backdrop Overlay for Mobile -->
|
||||
<div
|
||||
id="admin-sidebar-backdrop"
|
||||
class="fixed inset-0 bg-slate-950/80 backdrop-blur-sm z-40 hidden transition-opacity"
|
||||
></div>
|
||||
|
||||
<!-- Drawer Sidebar -->
|
||||
<aside
|
||||
id="admin-sidebar"
|
||||
class="fixed top-0 left-0 h-full w-72 bg-slate-950 border-r border-white/10 p-6 z-50 transform -translate-x-full transition-transform duration-300 ease-in-out flex flex-col justify-between shadow-2xl"
|
||||
>
|
||||
<div class="space-y-6">
|
||||
<!-- Header with Close Button -->
|
||||
<div class="flex items-center justify-between border-b border-white/10 pb-4">
|
||||
<div>
|
||||
<span class="text-[10px] font-mono text-sky-400 uppercase tracking-widest block">N&D WaaS Admin</span>
|
||||
<h2 class="text-lg font-bold text-white">Navigation</h2>
|
||||
</div>
|
||||
<button
|
||||
id="admin-menu-close"
|
||||
type="button"
|
||||
class="p-2 text-slate-400 hover:text-white rounded-lg hover:bg-slate-900 transition-colors"
|
||||
aria-label="Menü schließen"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Navigation Links -->
|
||||
<nav class="space-y-1.5" aria-label="Admin Sidebar">
|
||||
{navItems.map((item) => {
|
||||
const isActive = currentPath === item.href || (item.href !== '/admin' && currentPath.startsWith(item.href));
|
||||
return (
|
||||
<a
|
||||
href={item.href}
|
||||
class={`flex items-center gap-3 px-4 py-3 rounded-xl text-xs font-semibold transition-all ${
|
||||
isActive
|
||||
? 'bg-sky-400/10 text-sky-400 border border-sky-400/30'
|
||||
: 'text-slate-300 hover:bg-slate-900 hover:text-white border border-transparent'
|
||||
}`}
|
||||
>
|
||||
<span class="text-base">{item.icon}</span>
|
||||
<span>{item.label}</span>
|
||||
</a>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<!-- Bottom Quick Links -->
|
||||
<div class="pt-4 border-t border-white/10 space-y-2">
|
||||
<a
|
||||
href="/?preview=draft"
|
||||
target="_blank"
|
||||
class="flex items-center justify-center gap-2 w-full p-2.5 rounded-xl bg-amber-500/10 text-amber-400 border border-amber-500/20 text-xs font-semibold hover:bg-amber-500 hover:text-slate-950 transition-all"
|
||||
>
|
||||
🛠️ Entwurf-Vorschau
|
||||
</a>
|
||||
<a
|
||||
href="/"
|
||||
target="_blank"
|
||||
class="flex items-center justify-center gap-2 w-full p-2.5 rounded-xl bg-slate-900 text-slate-300 border border-white/10 text-xs font-semibold hover:border-sky-400 hover:text-white transition-all"
|
||||
>
|
||||
🌐 Live-Website
|
||||
</a>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<script>
|
||||
const toggleBtn = document.getElementById('admin-menu-toggle');
|
||||
const closeBtn = document.getElementById('admin-menu-close');
|
||||
const sidebar = document.getElementById('admin-sidebar');
|
||||
const backdrop = document.getElementById('admin-sidebar-backdrop');
|
||||
|
||||
function openMenu() {
|
||||
sidebar?.classList.remove('-translate-x-full');
|
||||
backdrop?.classList.remove('hidden');
|
||||
}
|
||||
|
||||
function closeMenu() {
|
||||
sidebar?.classList.add('-translate-x-full');
|
||||
backdrop?.classList.add('hidden');
|
||||
}
|
||||
|
||||
toggleBtn?.addEventListener('click', openMenu);
|
||||
closeBtn?.addEventListener('click', closeMenu);
|
||||
backdrop?.addEventListener('click', closeMenu);
|
||||
</script>
|
||||
@@ -5,17 +5,22 @@ interface Props {
|
||||
config: SiteConfig['bento'];
|
||||
}
|
||||
|
||||
const { config } = Astro.props;
|
||||
const props = Astro.props;
|
||||
const config = props.config || props.settings || props;
|
||||
const sectionTitle = config.sectionTitle || config.title || 'Unsere Leistungen';
|
||||
const sectionSubtitle = config.sectionSubtitle || config.subtitle || '';
|
||||
const cards = config.cards || config.items || [];
|
||||
---
|
||||
|
||||
<section id="services" class={`py-20 px-4 sm:px-6 lg:px-8 max-w-7xl mx-auto relative ${config.animation ? `anim-${config.animation}` : ''}`}>
|
||||
<section id="services" class={`py-20 px-4 sm:px-6 lg:px-8 max-w-7xl mx-auto relative ${config.animation ? `anim-${config.animation}` : ''}`} style={(config as any).bg_color ? `background-color: ${(config as any).bg_color};` : ''}>
|
||||
|
||||
<div class="text-center max-w-3xl mx-auto mb-16 space-y-3">
|
||||
<h2 class="text-3xl font-extrabold text-white tracking-tight uppercase">{config.sectionTitle}</h2>
|
||||
<p class="text-slate-400 text-xs sm:text-sm">{config.sectionSubtitle}</p>
|
||||
<h2 class="text-3xl font-extrabold text-white tracking-tight uppercase">{sectionTitle}</h2>
|
||||
<p class="text-slate-400 text-xs sm:text-sm">{sectionSubtitle}</p>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
{config.cards.map((card) => (
|
||||
{cards.map((card: any) => (
|
||||
<div class={`relative glass-panel glass-panel-hover rounded-2xl p-8 flex flex-col justify-between overflow-hidden group ${card.colSpan === 2 ? 'md:col-span-2' : 'md:col-span-1'} ${card.animation ? `anim-${card.animation}` : ''}`}>
|
||||
<div class="space-y-4 relative z-10">
|
||||
{card.badge && (
|
||||
|
||||
@@ -6,14 +6,19 @@ interface Props {
|
||||
smtpConfig: SmtpConfig;
|
||||
}
|
||||
|
||||
const { contactConfig, smtpConfig } = Astro.props;
|
||||
const props = Astro.props;
|
||||
const contactConfig = props.contactConfig || props.settings || props;
|
||||
const smtpConfig = props.smtpConfig || { isConfigured: false };
|
||||
const title = contactConfig.title || 'Kontaktieren Sie uns';
|
||||
const subtitle = contactConfig.subtitle || 'Wir freuen uns auf Ihre Nachricht.';
|
||||
---
|
||||
|
||||
<section id="contact" class="py-20 px-4 sm:px-6 lg:px-8 max-w-4xl mx-auto relative">
|
||||
<section id="contact" class="py-20 px-4 sm:px-6 lg:px-8 max-w-4xl mx-auto relative" style={(contactConfig as any).bg_color ? `background-color: ${(contactConfig as any).bg_color};` : ''}>
|
||||
|
||||
<div class="glass-panel rounded-3xl p-8 sm:p-12 relative overflow-hidden">
|
||||
<div class="text-center space-y-3 mb-10">
|
||||
<h2 class="text-3xl font-extrabold text-white tracking-tight uppercase">{contactConfig.title}</h2>
|
||||
<p class="text-slate-400 text-xs sm:text-sm">{contactConfig.subtitle}</p>
|
||||
<h2 class="text-3xl font-extrabold text-white tracking-tight uppercase">{title}</h2>
|
||||
<p class="text-slate-400 text-xs sm:text-sm">{subtitle}</p>
|
||||
</div>
|
||||
|
||||
{!smtpConfig.isConfigured && (
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
---
|
||||
interface Props {
|
||||
sectionName: string;
|
||||
sectionName?: string;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
const { sectionName } = Astro.props;
|
||||
const { sectionName = 'Unbekannt', message } = Astro.props;
|
||||
---
|
||||
|
||||
<section class="py-12 px-4 max-w-4xl mx-auto text-center">
|
||||
<div class="glass-panel rounded-2xl p-8 text-slate-400 text-xs font-mono">
|
||||
<p>⚙️ Section <strong>{sectionName}</strong> ist noch in Bearbeitung oder Platzhalter.</p>
|
||||
<p>⚙️ {message || `Section ${sectionName} ist noch in Bearbeitung oder Platzhalter.`}</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
@@ -7,11 +7,13 @@ interface Props {
|
||||
contextData?: Record<string, any>;
|
||||
}
|
||||
|
||||
const { config, contextData = {} } = Astro.props;
|
||||
const props = Astro.props;
|
||||
const config = props.config || props.settings || props;
|
||||
const contextData = props.contextData || {};
|
||||
|
||||
const heroTitle = resolveDynamicBinding(config.title, contextData);
|
||||
const heroSubtitle = resolveDynamicBinding(config.subtitle, contextData);
|
||||
const heroBadge = resolveDynamicBinding(config.badge, contextData);
|
||||
const heroTitle = resolveDynamicBinding(config.title || config.heroTitle || 'Willkommen', contextData);
|
||||
const heroSubtitle = resolveDynamicBinding(config.subtitle || config.heroSubtitle || '', contextData);
|
||||
const heroBadge = resolveDynamicBinding(config.badge || config.heroBadge || '', contextData);
|
||||
|
||||
const elements = config.elements || [];
|
||||
const badgeElements = elements.filter((el: UIComponent) => el.slotId === 'hero-badge-slot');
|
||||
@@ -19,10 +21,11 @@ const actionElements = elements.filter((el: UIComponent) => el.slotId === 'hero-
|
||||
const customElements = elements.filter((el: UIComponent) => !el.slotId || el.slotId === 'hero-custom-slot');
|
||||
---
|
||||
|
||||
<section class={`relative py-24 px-4 sm:px-6 lg:px-8 max-w-7xl mx-auto overflow-hidden ${config.animation ? `anim-${config.animation}` : ''}`}>
|
||||
<section class={`relative py-24 px-4 sm:px-6 lg:px-8 max-w-7xl mx-auto overflow-hidden ${config.animation ? `anim-${config.animation}` : ''}`} style={config.bg_color ? `background-color: ${config.bg_color};` : ''}>
|
||||
<span class="aurora-glow aurora-glow-sky w-[500px] h-[500px] -top-32 -left-32"></span>
|
||||
<span class="aurora-glow aurora-glow-indigo w-[400px] h-[400px] bottom-0 right-0"></span>
|
||||
|
||||
|
||||
<div class="relative z-10 text-center max-w-4xl mx-auto space-y-8">
|
||||
|
||||
<!-- Slot 1: Badge Slot -->
|
||||
|
||||
109
src/db/index.ts
Normal file
109
src/db/index.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
import { DatabaseSync } from 'node:sqlite';
|
||||
import path from 'node:path';
|
||||
import fs from 'node:fs';
|
||||
|
||||
const DATA_DIR = process.env.DATA_DIR || path.join(process.cwd(), 'app', 'data');
|
||||
const DB_PATH = path.join(DATA_DIR, 'waas.db');
|
||||
|
||||
let dbInstance: DatabaseSync | null = null;
|
||||
|
||||
export function getDb(): DatabaseSync {
|
||||
if (dbInstance) return dbInstance;
|
||||
|
||||
if (!fs.existsSync(DATA_DIR)) {
|
||||
fs.mkdirSync(DATA_DIR, { recursive: true });
|
||||
}
|
||||
|
||||
dbInstance = new DatabaseSync(DB_PATH);
|
||||
initSchema(dbInstance);
|
||||
return dbInstance;
|
||||
}
|
||||
|
||||
function initSchema(db: DatabaseSync) {
|
||||
// 1. site_settings
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS site_settings (
|
||||
key TEXT PRIMARY KEY,
|
||||
value_json TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL
|
||||
);
|
||||
`);
|
||||
|
||||
// 2. pages
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS pages (
|
||||
id TEXT PRIMARY KEY,
|
||||
slug TEXT NOT NULL UNIQUE,
|
||||
title TEXT NOT NULL,
|
||||
status TEXT NOT NULL DEFAULT 'published',
|
||||
homepage_id TEXT,
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL
|
||||
);
|
||||
`);
|
||||
|
||||
// 3. page_sections
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS page_sections (
|
||||
id TEXT PRIMARY KEY,
|
||||
page_id TEXT NOT NULL,
|
||||
section_type TEXT NOT NULL,
|
||||
order_index INTEGER NOT NULL DEFAULT 0,
|
||||
content_draft_json TEXT NOT NULL,
|
||||
content_published_json TEXT NOT NULL,
|
||||
styles_json TEXT,
|
||||
FOREIGN KEY (page_id) REFERENCES pages (id) ON DELETE CASCADE
|
||||
);
|
||||
`);
|
||||
|
||||
// Initial Seeding falls DB leer
|
||||
seedInitialData(db);
|
||||
}
|
||||
|
||||
function seedInitialData(db: DatabaseSync) {
|
||||
const checkPages = db.prepare('SELECT COUNT(*) as count FROM pages').get() as { count: number };
|
||||
if (checkPages.count > 0) return;
|
||||
|
||||
const now = new Date().toISOString();
|
||||
|
||||
// Seed site_settings
|
||||
const initialSettings = {
|
||||
siteName: 'N&D IT Solutions',
|
||||
metaDescription: 'High-Performance Webdesign & Digital Solutions - 100% DSGVO-konform.',
|
||||
is_demo_mode: false,
|
||||
header: {
|
||||
brandName: 'N&D',
|
||||
brandHighlight: 'i-t SOLUTIONS',
|
||||
ctaText: 'Erstgespräch buchen',
|
||||
ctaLink: '#contact',
|
||||
showThemeToggle: true,
|
||||
glassEffect: true
|
||||
},
|
||||
footer: {
|
||||
brandDescription: 'Ihr Partner für professionelles Webdesign, Website-Entwicklung und lokale SEO.',
|
||||
copyrightText: 'N&D i-t SOLUTIONS. Alle Rechte vorbehalten.',
|
||||
showLegalLinks: true
|
||||
},
|
||||
site_info: {
|
||||
title: 'N&D IT Solutions',
|
||||
homepage_id: 'page_home'
|
||||
}
|
||||
};
|
||||
|
||||
db.prepare('INSERT INTO site_settings (key, value_json, updated_at) VALUES (?, ?, ?)').run(
|
||||
'site_config',
|
||||
JSON.stringify(initialSettings),
|
||||
now
|
||||
);
|
||||
|
||||
// Seed default page only (no prefilled sections/demo content)
|
||||
db.prepare('INSERT INTO pages (id, slug, title, status, homepage_id, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?)').run(
|
||||
'page_home',
|
||||
'/',
|
||||
'Startseite',
|
||||
'published',
|
||||
'page_home',
|
||||
now,
|
||||
now
|
||||
);
|
||||
}
|
||||
33
src/db/resetDb.ts
Normal file
33
src/db/resetDb.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { DatabaseSync } from 'node:sqlite';
|
||||
import path from 'node:path';
|
||||
import fs from 'node:fs';
|
||||
|
||||
const DATA_DIR = process.env.DATA_DIR || path.join(process.cwd(), 'app', 'data');
|
||||
const DB_PATH = path.join(DATA_DIR, 'waas.db');
|
||||
|
||||
export function resetDatabaseToBlankSlate(): void {
|
||||
const db = new DatabaseSync(DB_PATH);
|
||||
|
||||
// Lösche bestehende Seiten & Sektionen
|
||||
db.exec('DELETE FROM page_sections');
|
||||
db.exec('DELETE FROM pages');
|
||||
|
||||
const now = new Date().toISOString();
|
||||
|
||||
// Leere Startseite anlegen (ohne vordefinierte Sektionen/Inhalte)
|
||||
db.prepare('INSERT INTO pages (id, slug, title, status, homepage_id, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?)').run(
|
||||
'page_home',
|
||||
'/',
|
||||
'Startseite',
|
||||
'published',
|
||||
'page_home',
|
||||
now,
|
||||
now
|
||||
);
|
||||
|
||||
console.log('[WaaS DB] Datenbank auf leeren Stand (Blank Slate) zurückgesetzt.');
|
||||
}
|
||||
|
||||
if (import.meta.url === `file://${process.argv[1]}` || process.argv[1]?.endsWith('resetDb.ts')) {
|
||||
resetDatabaseToBlankSlate();
|
||||
}
|
||||
@@ -15,13 +15,18 @@ import { THEME_PRESETS } from '../utils/themePresets';
|
||||
interface Props {
|
||||
title: string;
|
||||
description?: string;
|
||||
ogImage?: string;
|
||||
}
|
||||
|
||||
const { siteConfig, themeConfig } = loadWaasConfigs(Astro.request.headers.get('user-agent') ?? '');
|
||||
const activePreset = THEME_PRESETS[themeConfig.activePreset] || THEME_PRESETS.corporateDark;
|
||||
const colors = activePreset.colors;
|
||||
const colors = {
|
||||
...activePreset.colors,
|
||||
...(themeConfig.customColors || {})
|
||||
};
|
||||
|
||||
const { title, description = "Professionelles Webdesign & moderne Websites.", ogImage } = Astro.props;
|
||||
|
||||
const { title, description = "Professionelles Webdesign & moderne Websites." } = Astro.props;
|
||||
---
|
||||
|
||||
<!doctype html>
|
||||
@@ -52,10 +57,11 @@ const { title, description = "Professionelles Webdesign & moderne Websites." } =
|
||||
|
||||
html.light {
|
||||
--theme-bg: #F8FAFC;
|
||||
--theme-card-bg: rgba(255, 255, 255, 0.7);
|
||||
--theme-card-bg: rgba(255, 255, 255, 0.85);
|
||||
--theme-text: #0F172A;
|
||||
--theme-border: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
<!-- Inline theme initialization to prevent FOUC -->
|
||||
@@ -73,8 +79,15 @@ const { title, description = "Professionelles Webdesign & moderne Websites." } =
|
||||
</script>
|
||||
</head>
|
||||
<body class="font-sans transition-colors duration-500 antialiased">
|
||||
{(siteConfig as any).is_demo_mode && (
|
||||
<div class="bg-gradient-to-r from-amber-600 via-amber-500 to-amber-600 text-slate-950 text-center py-2 px-4 text-xs font-bold font-mono tracking-wide shadow-md flex items-center justify-center gap-2">
|
||||
<span>🎭 DEMO-MODUS AKTIV</span>
|
||||
<span class="opacity-75 font-sans font-normal">— Dies ist eine interaktive Vorschau-Instanz der N&D WaaS Engine.</span>
|
||||
</div>
|
||||
)}
|
||||
<slot />
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
||||
<style is:global>
|
||||
|
||||
25
src/pages/404.astro
Normal file
25
src/pages/404.astro
Normal file
@@ -0,0 +1,25 @@
|
||||
---
|
||||
// src/pages/404.astro
|
||||
import Layout from '../layouts/Layout.astro';
|
||||
---
|
||||
|
||||
<Layout title="404 - Seite nicht gefunden | N&D IT Solutions">
|
||||
<main class="min-h-screen bg-[#0B0F19] text-white flex items-center justify-center p-4">
|
||||
<div class="preset-card p-10 bg-slate-900/80 border border-white/10 rounded-2xl max-w-lg w-full text-center space-y-6 shadow-2xl backdrop-blur-md">
|
||||
<span class="text-6xl font-extrabold text-sky-400 font-mono tracking-widest block">404</span>
|
||||
<h1 class="text-2xl font-bold text-white">Seite nicht gefunden</h1>
|
||||
<p class="text-slate-400 text-sm leading-relaxed">
|
||||
Die von Ihnen angeforderte Seite existiert nicht oder wurde verschoben.
|
||||
</p>
|
||||
|
||||
<div class="pt-4">
|
||||
<a
|
||||
href="/"
|
||||
class="inline-block bg-sky-500 hover:bg-sky-400 text-slate-950 font-bold px-6 py-3 rounded-xl text-sm transition-all shadow-lg shadow-sky-500/20 active:scale-95"
|
||||
>
|
||||
← Zurück zur Startseite
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</Layout>
|
||||
@@ -71,11 +71,13 @@ let page = pages.find((p: any) => {
|
||||
return p.slug === currentSlug || p.slug === `/${currentSlug}`;
|
||||
});
|
||||
|
||||
// 5. Zero-Crash Fallback Guard: Wenn Seite fehlt oder unveröffentlicht ist
|
||||
if (!page || (page.is_published === false && import.meta.env.PROD)) {
|
||||
return Astro.redirect('/404');
|
||||
// 5. Zero-Crash Fallback Guard: Wenn Seite nicht existiert oder nicht veröffentlicht ist -> 404 Status
|
||||
if (!page || (page.is_published === false && !isDraft)) {
|
||||
Astro.response.status = 404;
|
||||
return Astro.rewrite('/404');
|
||||
}
|
||||
|
||||
|
||||
// 6. Dynamic Component Registry Mapping
|
||||
const ComponentRegistry: Record<string, any> = {
|
||||
hero: HeroSection,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,6 @@
|
||||
---
|
||||
import Layout from '../../layouts/Layout.astro';
|
||||
import AdminSidebar from '../../components/admin/AdminSidebar.astro';
|
||||
import { loadWaasConfigs } from '../../utils/configLoader';
|
||||
import { THEME_PRESETS } from '../../utils/themePresets';
|
||||
|
||||
@@ -8,7 +9,9 @@ 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">
|
||||
<AdminSidebar currentPath="/admin" />
|
||||
<div class="min-h-screen bg-[#0B0F19] text-white p-6 sm:p-10 pl-16 sm:pl-20 font-sans">
|
||||
|
||||
<div class="max-w-6xl mx-auto space-y-10">
|
||||
|
||||
<!-- Admin Header & Quick Navigation -->
|
||||
@@ -19,41 +22,31 @@ const presets = Object.values(THEME_PRESETS);
|
||||
<h1 class="text-3xl font-extrabold tracking-tight mt-1">System & Admin Dashboard</h1>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<a id="toggle-preview-mode" href="/?preview=draft" target="_blank" class="px-4 py-2 rounded-xl bg-amber-500/10 text-amber-400 border border-amber-500/20 text-xs font-semibold hover:bg-amber-500 hover:text-slate-950 transition-all flex items-center gap-2">
|
||||
🛠️ Bearbeitungsmodus (Entwurf-Vorschau)
|
||||
</a>
|
||||
<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
|
||||
🌐 Live-Seite Ansehen
|
||||
</a>
|
||||
<button id="save-all-btn" class="px-6 py-2.5 rounded-xl 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">
|
||||
Einstellungen Speichern
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Quick Admin Nav Cards / Links -->
|
||||
<nav aria-label="Admin Navigation" class="grid grid-cols-1 sm:grid-cols-3 gap-3 pt-2">
|
||||
<a href="/admin/pages" class="p-3 bg-slate-900/80 border border-white/10 hover:border-sky-400/50 rounded-xl flex items-center justify-between text-xs transition-all group">
|
||||
<nav aria-label="Admin Navigation" class="pt-2">
|
||||
<a href="/admin/pages" class="p-4 bg-slate-900/80 border border-white/10 hover:border-sky-400/50 rounded-xl flex items-center justify-between text-xs transition-all group">
|
||||
<div>
|
||||
<span class="font-bold text-white block group-hover:text-sky-400">📄 Seiten-Verwaltung</span>
|
||||
<span class="text-[10px] text-slate-400">Routen, Slugs & Startseite festlegen</span>
|
||||
<span class="font-bold text-white block group-hover:text-sky-400 text-sm">📄 Seiten-Verwaltung & Visueller Editor</span>
|
||||
<span class="text-[11px] text-slate-400">Routen, Slugs, Startseite & Seiten im Editor öffnen</span>
|
||||
</div>
|
||||
<span class="text-sky-400 text-sm font-bold">→</span>
|
||||
</a>
|
||||
|
||||
<a href="/admin/editor" class="p-3 bg-slate-900/80 border border-white/10 hover:border-sky-400/50 rounded-xl flex items-center justify-between text-xs transition-all group">
|
||||
<div>
|
||||
<span class="font-bold text-white block group-hover:text-sky-400">🎨 Visueller Drag & Drop Editor</span>
|
||||
<span class="text-[10px] text-slate-400">Sektionen & Inhalts-Vorschau</span>
|
||||
</div>
|
||||
<span class="text-sky-400 text-sm font-bold">→</span>
|
||||
</a>
|
||||
|
||||
<a href="/admin/settings" class="p-3 bg-slate-900/80 border border-white/10 hover:border-sky-400/50 rounded-xl flex items-center justify-between text-xs transition-all group">
|
||||
<div>
|
||||
<span class="font-bold text-white block group-hover:text-sky-400">⚙️ Stammdaten & SEO</span>
|
||||
<span class="text-[10px] text-slate-400">Firmen-Daten & Dynamic Data Binding</span>
|
||||
</div>
|
||||
<span class="text-sky-400 text-sm font-bold">→</span>
|
||||
<span class="text-sky-400 text-base font-bold">→</span>
|
||||
</a>
|
||||
</nav>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
@@ -64,72 +57,110 @@ const presets = Object.values(THEME_PRESETS);
|
||||
<!-- 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">
|
||||
<!-- Site Info & Stammdaten -->
|
||||
<div class="glass-panel rounded-2xl p-6 space-y-6">
|
||||
<h2 class="text-lg font-bold text-sky-400 uppercase tracking-wider flex items-center gap-2">
|
||||
Allgemeine Informationen
|
||||
Allgemeine Informationen & SEO
|
||||
</h2>
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label class="block text-xs font-bold text-slate-400 mb-1">Webseiten-Name</label>
|
||||
<label class="block text-xs font-bold text-slate-400 mb-1">Webseiten-Name (Titel)</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>
|
||||
<label class="block text-xs font-bold text-slate-400 mb-1">Meta Beschreibung (SEO)</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>
|
||||
<label class="flex items-center gap-3 pt-2 p-3 bg-amber-500/10 border border-amber-500/20 rounded-xl cursor-pointer">
|
||||
<input type="checkbox" id="isDemoMode" checked={!!siteConfig.is_demo_mode} class="rounded text-amber-400 focus:ring-0 h-4 w-4" />
|
||||
<div>
|
||||
<span class="text-xs text-amber-400 font-bold block">🎭 Demo-Modus aktivieren</span>
|
||||
<span class="text-[10px] text-slate-400">Zeigt einen festen Demo-Hinweis für Interessenten auf der Live-Website</span>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<!-- 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>
|
||||
|
||||
<h3 class="text-xs font-bold text-slate-400 uppercase tracking-wider pt-2 border-t border-white/10">Unternehmens-Stammdaten (Dynamic Data Binding)</h3>
|
||||
<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" />
|
||||
<label class="block text-xs font-semibold text-slate-400 mb-1">Firmenname / Brand</label>
|
||||
<input type="text" id="infoTitle" value={(siteConfig as any).site_info?.title || 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">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>
|
||||
<label class="block text-xs font-semibold text-slate-400 mb-1">E-Mail Adresse</label>
|
||||
<input type="email" id="infoEmail" value={(siteConfig as any).site_info?.email || ''} 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 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" />
|
||||
<label class="block text-xs font-semibold text-slate-400 mb-1">Telefonnummer</label>
|
||||
<input type="text" id="infoPhone" value={(siteConfig as any).site_info?.phone || ''} 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" />
|
||||
<label class="block text-xs font-semibold text-slate-400 mb-1">Adresse / Standort</label>
|
||||
<input type="text" id="infoAddress" value={(siteConfig as any).site_info?.address || ''} 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>
|
||||
|
||||
<!-- Global Header Config -->
|
||||
<h3 class="text-xs font-bold text-slate-400 uppercase tracking-wider pt-4 border-t border-white/10">Globaler Header / Navigation Bar</h3>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-slate-400 mb-1">Brand Name (z.B. N&D)</label>
|
||||
<input type="text" id="headerBrandName" value={siteConfig.header?.brandName || 'N&D'} 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-semibold text-slate-400 mb-1">Brand Highlight (z.B. i-t SOLUTIONS)</label>
|
||||
<input type="text" id="headerBrandHighlight" value={siteConfig.header?.brandHighlight || 'i-t SOLUTIONS'} 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-semibold text-slate-400 mb-1">CTA Button Text</label>
|
||||
<input type="text" id="headerCtaText" value={siteConfig.header?.ctaText || 'Erstgespräch buchen'} 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-semibold text-slate-400 mb-1">CTA Button Link</label>
|
||||
<input type="text" id="headerCtaLink" value={siteConfig.header?.ctaLink || '#contact'} 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>
|
||||
<label class="flex items-center gap-2 cursor-pointer sm:col-span-2">
|
||||
<input type="checkbox" id="headerShowThemeToggle" checked={siteConfig.header?.showThemeToggle !== false} class="rounded text-sky-400 focus:ring-0" />
|
||||
<span class="text-xs text-slate-300 font-semibold">Theme-Toggle Button (☀️/🌙) anzeigen</span>
|
||||
</label>
|
||||
<label class="flex items-center gap-2 cursor-pointer sm:col-span-2">
|
||||
<input type="checkbox" id="headerGlassEffect" checked={siteConfig.header?.glassEffect !== false} class="rounded text-sky-400 focus:ring-0" />
|
||||
<span class="text-xs text-slate-300 font-semibold">Liquid Glassmorphism Effekt aktivieren</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Global Footer Config -->
|
||||
<h3 class="text-xs font-bold text-slate-400 uppercase tracking-wider pt-4 border-t border-white/10">Globaler Footer</h3>
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-slate-400 mb-1">Footer Beschreibungstext</label>
|
||||
<textarea id="footerBrandDescription" 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.footer?.brandDescription || ''}</textarea>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-slate-400 mb-1">Copyright Text</label>
|
||||
<input type="text" id="footerCopyrightText" value={siteConfig.footer?.copyrightText || ''} 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>
|
||||
<label class="flex items-center gap-2 cursor-pointer">
|
||||
<input type="checkbox" id="footerShowLegalLinks" checked={siteConfig.footer?.showLegalLinks !== false} class="rounded text-sky-400 focus:ring-0" />
|
||||
<span class="text-xs text-slate-300 font-semibold">Rechtliche Links (Datenschutz & Impressum) im Footer anzeigen</span>
|
||||
</label>
|
||||
</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 -->
|
||||
<!-- Farb-Auswahl (10 globale Farb-Kombinationen) & Custom Colors -->
|
||||
<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)
|
||||
🎨 Farb-Auswahl (10 Farb-Kombinationen)
|
||||
</h2>
|
||||
<div class="space-y-2 max-h-[320px] overflow-y-auto pr-1">
|
||||
<p class="text-[10px] text-slate-400">Wähle eine globale Farbkombination für das gesamte Farbspektrum:</p>
|
||||
<div class="space-y-2 max-h-[260px] 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">
|
||||
@@ -137,14 +168,40 @@ const presets = Object.values(THEME_PRESETS);
|
||||
<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>
|
||||
<span class="w-2.5 h-2.5 rounded-full inline-block border border-white/20" style={`background-color: ${p.colors.bg};`} title="Hintergrund"></span>
|
||||
<span class="w-2.5 h-2.5 rounded-full inline-block border border-white/20" style={`background-color: ${p.colors.cardBg};`} title="Karten"></span>
|
||||
<span class="w-2.5 h-2.5 rounded-full inline-block border border-white/20" style={`background-color: ${p.colors.text};`} title="Text"></span>
|
||||
<span class="w-2.5 h-2.5 rounded-full inline-block border border-white/20" style={`background-color: ${p.colors.accent};`} title="Akzent"></span>
|
||||
</div>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Custom Colors Section -->
|
||||
<h3 class="text-xs font-bold text-slate-400 uppercase tracking-wider pt-3 border-t border-white/10">Individuelle Farbvergabe (Overrides)</h3>
|
||||
<p class="text-[10px] text-slate-400">Überschreibe hier gezielt einzelne Preset-Farben:</p>
|
||||
<div class="grid grid-cols-2 gap-2 text-xs">
|
||||
<div>
|
||||
<label class="block text-[10px] text-slate-400 mb-1">Hintergrund (Bg)</label>
|
||||
<input type="color" id="customColorBg" value={themeConfig.customColors?.bg || (THEME_PRESETS[themeConfig.activePreset] || THEME_PRESETS.corporateDark).colors.bg} class="w-full h-8 rounded bg-slate-900 border border-white/10 cursor-pointer p-0.5" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-[10px] text-slate-400 mb-1">Akzentfarbe</label>
|
||||
<input type="color" id="customColorAccent" value={themeConfig.customColors?.accent || (THEME_PRESETS[themeConfig.activePreset] || THEME_PRESETS.corporateDark).colors.accent} class="w-full h-8 rounded bg-slate-900 border border-white/10 cursor-pointer p-0.5" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-[10px] text-slate-400 mb-1">Textfarbe</label>
|
||||
<input type="color" id="customColorText" value={themeConfig.customColors?.text || (THEME_PRESETS[themeConfig.activePreset] || THEME_PRESETS.corporateDark).colors.text} class="w-full h-8 rounded bg-slate-900 border border-white/10 cursor-pointer p-0.5" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-[10px] text-slate-400 mb-1">Rahmenfarbe (Border)</label>
|
||||
<input type="text" id="customColorBorder" value={themeConfig.customColors?.border || ''} placeholder="rgba(255,255,255,0.1)" class="w-full h-8 rounded bg-slate-900 border border-white/10 text-[10px] px-2 text-white" />
|
||||
</div>
|
||||
</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">
|
||||
@@ -195,7 +252,6 @@ const presets = Object.values(THEME_PRESETS);
|
||||
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;
|
||||
@@ -205,18 +261,39 @@ const presets = Object.values(THEME_PRESETS);
|
||||
siteConfig: {
|
||||
siteName: getVal('siteName'),
|
||||
metaDescription: getVal('metaDescription'),
|
||||
hero: {
|
||||
badge: getVal('heroBadge'),
|
||||
title: getVal('heroTitle'),
|
||||
subtitle: getVal('heroSubtitle'),
|
||||
ctaPrimaryText: getVal('ctaPrimaryText'),
|
||||
ctaPrimaryLink: getVal('ctaPrimaryLink')
|
||||
is_demo_mode: getBool('isDemoMode'),
|
||||
site_info: {
|
||||
title: getVal('infoTitle'),
|
||||
email: getVal('infoEmail'),
|
||||
phone: getVal('infoPhone'),
|
||||
address: getVal('infoAddress')
|
||||
},
|
||||
sectionsOrder: sections
|
||||
header: {
|
||||
brandName: getVal('headerBrandName'),
|
||||
brandHighlight: getVal('headerBrandHighlight'),
|
||||
ctaText: getVal('headerCtaText'),
|
||||
ctaLink: getVal('headerCtaLink'),
|
||||
showThemeToggle: getBool('headerShowThemeToggle'),
|
||||
glassEffect: getBool('headerGlassEffect')
|
||||
},
|
||||
footer: {
|
||||
brandDescription: getVal('footerBrandDescription'),
|
||||
copyrightText: getVal('footerCopyrightText'),
|
||||
showLegalLinks: getBool('footerShowLegalLinks')
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
themeConfig: {
|
||||
activePreset: selectedPreset
|
||||
activePreset: selectedPreset,
|
||||
customColors: {
|
||||
bg: getVal('customColorBg'),
|
||||
accent: getVal('customColorAccent'),
|
||||
text: getVal('customColorText'),
|
||||
border: getVal('customColorBorder')
|
||||
}
|
||||
},
|
||||
|
||||
smtpConfig: {
|
||||
host: getVal('smtpHost'),
|
||||
port: getNum('smtpPort'),
|
||||
@@ -227,6 +304,8 @@ const presets = Object.values(THEME_PRESETS);
|
||||
};
|
||||
|
||||
try {
|
||||
|
||||
|
||||
const res = await fetch('/api/config', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
---
|
||||
// src/pages/admin/pages.astro
|
||||
import Layout from '../../layouts/Layout.astro';
|
||||
import AdminSidebar from '../../components/admin/AdminSidebar.astro';
|
||||
import { loadWaasConfigs } from '../../utils/configLoader';
|
||||
|
||||
export const prerender = false;
|
||||
@@ -12,7 +13,9 @@ const currentHomepageId = (siteConfig as any).site_info?.homepage_id;
|
||||
---
|
||||
|
||||
<Layout title="Seiten-Verwaltung | Admin Dashboard">
|
||||
<div class="max-w-6xl mx-auto px-4 py-8">
|
||||
<AdminSidebar currentPath="/admin/pages" />
|
||||
<div class="max-w-6xl mx-auto px-4 py-8 pl-16 sm:pl-20">
|
||||
|
||||
<div class="flex items-center justify-between mb-8">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold text-white">Seiten-Verwaltung</h1>
|
||||
@@ -66,15 +69,35 @@ const currentHomepageId = (siteConfig as any).site_info?.homepage_id;
|
||||
</td>
|
||||
<td class="p-4 text-right space-x-2">
|
||||
<a
|
||||
href={`/admin/editor/?page_id=${page.id}`}
|
||||
class="bg-slate-800 hover:bg-slate-700 text-white px-3 py-1.5 rounded text-xs font-medium inline-block transition-colors"
|
||||
href={page.slug}
|
||||
target="_blank"
|
||||
class="bg-slate-800 hover:bg-slate-700 text-sky-400 px-2.5 py-1.5 rounded text-xs font-medium inline-block transition-colors"
|
||||
title="Seite im Browser öffnen"
|
||||
>
|
||||
✏️ Im Editor öffnen
|
||||
🔗 Öffnen
|
||||
</a>
|
||||
<a
|
||||
href={`/admin/editor/?page_id=${page.id}`}
|
||||
class="bg-sky-500/20 hover:bg-sky-500 text-sky-300 hover:text-slate-950 px-2.5 py-1.5 rounded text-xs font-medium inline-block transition-colors border border-sky-500/30"
|
||||
>
|
||||
✏️ Editor
|
||||
</a>
|
||||
{!isHomepage && (
|
||||
<button
|
||||
type="button"
|
||||
data-page-id={page.id}
|
||||
data-page-title={page.title}
|
||||
class="btn-delete-page bg-red-500/10 hover:bg-red-500 text-red-400 hover:text-white px-2.5 py-1.5 rounded text-xs font-medium inline-block transition-colors border border-red-500/20"
|
||||
title="Seite löschen"
|
||||
>
|
||||
🗑️ Löschen
|
||||
</button>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@@ -138,4 +161,28 @@ const currentHomepageId = (siteConfig as any).site_info?.homepage_id;
|
||||
window.location.reload();
|
||||
}
|
||||
});
|
||||
|
||||
// Event: Seite löschen
|
||||
document.querySelectorAll('.btn-delete-page').forEach(btn => {
|
||||
btn.addEventListener('click', async (e: any) => {
|
||||
const pageId = e.target.getAttribute('data-page-id');
|
||||
const pageTitle = e.target.getAttribute('data-page-title');
|
||||
|
||||
if (!confirm(`Möchten Sie die Seite "${pageTitle}" wirklich unwiderruflich löschen?`)) return;
|
||||
|
||||
const res = await fetch('/api/admin/pages/delete', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ page_id: pageId })
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
window.location.reload();
|
||||
} else {
|
||||
const data = await res.json();
|
||||
alert(data.error || 'Fehler beim Löschen der Seite.');
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
---
|
||||
// src/pages/admin/settings.astro
|
||||
import Layout from '../../layouts/Layout.astro';
|
||||
import AdminSidebar from '../../components/admin/AdminSidebar.astro';
|
||||
import { loadWaasConfigs } from '../../utils/configLoader';
|
||||
|
||||
export const prerender = false;
|
||||
@@ -11,7 +12,9 @@ const info = (siteConfig as any).site_info || {};
|
||||
---
|
||||
|
||||
<Layout title="Globale Einstellungen | N&D Admin">
|
||||
<div class="max-w-4xl mx-auto px-4 py-8">
|
||||
<AdminSidebar currentPath="/admin/settings" />
|
||||
<div class="max-w-4xl mx-auto px-4 py-8 pl-16 sm:pl-20">
|
||||
|
||||
<div class="flex items-center justify-between mb-8 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 zum Seiten-Dashboard</a>
|
||||
|
||||
40
src/pages/api/admin/pages/delete.ts
Normal file
40
src/pages/api/admin/pages/delete.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import type { APIRoute } from 'astro';
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
|
||||
export const prerender = false;
|
||||
|
||||
const DATA_DIR = process.env.DATA_DIR || path.join(process.cwd(), 'app', 'data');
|
||||
|
||||
export const POST: APIRoute = async ({ request }) => {
|
||||
try {
|
||||
const { page_id } = await request.json();
|
||||
if (!page_id) {
|
||||
return new Response(JSON.stringify({ error: 'page_id erforderlich' }), { status: 400 });
|
||||
}
|
||||
|
||||
const configPath = path.join(DATA_DIR, 'site.config.json');
|
||||
let config: any = {};
|
||||
|
||||
try {
|
||||
const rawData = await fs.readFile(configPath, 'utf-8');
|
||||
config = JSON.parse(rawData);
|
||||
} catch {
|
||||
return new Response(JSON.stringify({ error: 'Konfigurationsdatei nicht gefunden' }), { status: 404 });
|
||||
}
|
||||
|
||||
// Verhindere Löschen der aktiven Homepage
|
||||
if (config.site_info?.homepage_id === page_id) {
|
||||
return new Response(JSON.stringify({ error: 'Die aktive Startseite kann nicht gelöscht werden.' }), { status: 400 });
|
||||
}
|
||||
|
||||
config.pages = (config.pages || []).filter((p: any) => p.id !== page_id);
|
||||
|
||||
await fs.mkdir(DATA_DIR, { recursive: true });
|
||||
await fs.writeFile(configPath, JSON.stringify(config, null, 2), 'utf-8');
|
||||
|
||||
return new Response(JSON.stringify({ success: true, message: 'Seite erfolgreich gelöscht' }), { status: 200 });
|
||||
} catch (error) {
|
||||
return new Response(JSON.stringify({ error: 'Fehler beim Löschen der Seite' }), { status: 500 });
|
||||
}
|
||||
};
|
||||
@@ -11,17 +11,36 @@ export const POST: APIRoute = async ({ request }) => {
|
||||
fs.mkdirSync(dataDir, { recursive: true });
|
||||
}
|
||||
|
||||
const { siteConfig: currentSite, themeConfig: currentTheme, smtpConfig: currentSmtp } = loadWaasConfigs(Astro.request.headers.get('user-agent') ?? '');
|
||||
const userAgent = request.headers.get('user-agent') ?? '';
|
||||
const { siteConfig: currentSite, themeConfig: currentTheme, smtpConfig: currentSmtp } = loadWaasConfigs(userAgent);
|
||||
|
||||
|
||||
if (body.siteConfig) {
|
||||
const newSite = {
|
||||
...currentSite,
|
||||
...body.siteConfig,
|
||||
hero: { ...currentSite.hero, ...body.siteConfig.hero }
|
||||
hero: { ...(currentSite.hero || {}), ...(body.siteConfig.hero || {}) },
|
||||
site_info: { ...((currentSite as any).site_info || {}), ...(body.siteConfig.site_info || {}) },
|
||||
header: { ...(currentSite.header || {}), ...(body.siteConfig.header || {}) },
|
||||
footer: { ...(currentSite.footer || {}), ...(body.siteConfig.footer || {}) }
|
||||
};
|
||||
fs.writeFileSync(path.join(dataDir, 'site.config.json'), JSON.stringify(newSite, null, 2), 'utf8');
|
||||
|
||||
const sitePath = path.join(dataDir, 'site.config.json');
|
||||
const draftPath = path.join(dataDir, 'site.config.draft.json');
|
||||
fs.writeFileSync(sitePath, JSON.stringify(newSite, null, 2), 'utf8');
|
||||
|
||||
// Falls bereits ein Draft existiert, site_info & is_demo_mode dort ebenfalls aktualisieren
|
||||
if (fs.existsSync(draftPath)) {
|
||||
try {
|
||||
const draftContent = JSON.parse(fs.readFileSync(draftPath, 'utf8'));
|
||||
const updatedDraft = { ...draftContent, ...newSite };
|
||||
fs.writeFileSync(draftPath, JSON.stringify(updatedDraft, null, 2), 'utf8');
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
if (body.themeConfig) {
|
||||
const newTheme = { ...currentTheme, ...body.themeConfig };
|
||||
fs.writeFileSync(path.join(dataDir, 'theme.config.json'), JSON.stringify(newTheme, null, 2), 'utf8');
|
||||
|
||||
@@ -1,40 +1,24 @@
|
||||
import type { APIRoute } from 'astro';
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import { getDb } from '../../../db/index';
|
||||
import { generateDraftConfig } from '../../../services/file-generator';
|
||||
|
||||
export const prerender = false;
|
||||
|
||||
const DATA_DIR = process.env.DATA_DIR || path.join(process.cwd(), 'app', 'data');
|
||||
|
||||
// Vordefinierte Standard-Templates für neue Sektionen
|
||||
// Standard-Templates
|
||||
const SECTION_TEMPLATES: Record<string, any> = {
|
||||
HeroSection: {
|
||||
type: 'HeroSection',
|
||||
settings: {
|
||||
title: 'Neue Überschrift',
|
||||
subtitle: 'Beschreibungstext hier eingeben.',
|
||||
cta_text: 'Jetzt anfragen',
|
||||
cta_url: '#contact'
|
||||
}
|
||||
title: 'Neue Überschrift',
|
||||
subtitle: 'Beschreibungstext hier eingeben.',
|
||||
ctaPrimaryText: 'Jetzt anfragen',
|
||||
ctaPrimaryLink: '#contact'
|
||||
},
|
||||
BentoGrid: {
|
||||
type: 'BentoGrid',
|
||||
settings: {
|
||||
title: 'Unsere Highlights',
|
||||
columns: 3,
|
||||
items: [
|
||||
{ title: 'Feature 1', description: 'Beschreibung 1' },
|
||||
{ title: 'Feature 2', description: 'Beschreibung 2' },
|
||||
{ title: 'Feature 3', description: 'Beschreibung 3' }
|
||||
]
|
||||
}
|
||||
sectionTitle: 'Unsere Highlights',
|
||||
sectionSubtitle: 'Modernste Features im Überblick'
|
||||
},
|
||||
ContactSection: {
|
||||
type: 'ContactSection',
|
||||
settings: {
|
||||
title: 'Kontaktieren Sie uns',
|
||||
email_recipient: 'info@kunden-domain.de'
|
||||
}
|
||||
title: 'Kontaktieren Sie uns',
|
||||
subtitle: 'Wir antworten innerhalb von 24 Stunden.'
|
||||
}
|
||||
};
|
||||
|
||||
@@ -42,41 +26,34 @@ export const POST: APIRoute = async ({ request }) => {
|
||||
try {
|
||||
const { page_id, section_type } = await request.json();
|
||||
|
||||
if (!page_id || !section_type || !SECTION_TEMPLATES[section_type]) {
|
||||
if (!page_id || !section_type) {
|
||||
return new Response(JSON.stringify({ error: 'Gültige page_id und section_type erforderlich' }), { status: 400 });
|
||||
}
|
||||
|
||||
const draftPath = path.join(DATA_DIR, 'site.config.draft.json');
|
||||
const livePath = path.join(DATA_DIR, 'site.config.json');
|
||||
const db = getDb();
|
||||
const secId = `sec_${section_type.toLowerCase()}_${Date.now()}`;
|
||||
const defaultContent = JSON.stringify(SECTION_TEMPLATES[section_type] || {});
|
||||
|
||||
let configRaw: string;
|
||||
try {
|
||||
configRaw = await fs.readFile(draftPath, 'utf-8');
|
||||
} catch {
|
||||
configRaw = await fs.readFile(livePath, 'utf-8');
|
||||
}
|
||||
// Aktuellen max order_index ermitteln
|
||||
const maxOrderRow = db.prepare('SELECT MAX(order_index) as max_idx FROM page_sections WHERE page_id = ?').get(page_id) as { max_idx: number | null };
|
||||
const nextIdx = (maxOrderRow?.max_idx ?? -1) + 1;
|
||||
|
||||
const config = JSON.parse(configRaw);
|
||||
const targetPage = config.pages?.find((p: any) => p.id === page_id);
|
||||
db.prepare('INSERT INTO page_sections (id, page_id, section_type, order_index, content_draft_json, content_published_json, styles_json) VALUES (?, ?, ?, ?, ?, ?, ?)').run(
|
||||
secId,
|
||||
page_id,
|
||||
section_type,
|
||||
nextIdx,
|
||||
defaultContent,
|
||||
defaultContent,
|
||||
JSON.stringify({})
|
||||
);
|
||||
|
||||
if (!targetPage) {
|
||||
return new Response(JSON.stringify({ error: 'Seite nicht gefunden' }), { status: 404 });
|
||||
}
|
||||
// Draft-Config via File-Generator neu kompilieren
|
||||
await generateDraftConfig();
|
||||
|
||||
// Neue Sektion mit eindeutiger ID anfügen
|
||||
const newSection = {
|
||||
id: `sec_${section_type.toLowerCase()}_${Date.now()}`,
|
||||
...SECTION_TEMPLATES[section_type]
|
||||
};
|
||||
|
||||
targetPage.sections = targetPage.sections || [];
|
||||
targetPage.sections.push(newSection);
|
||||
|
||||
await fs.mkdir(DATA_DIR, { recursive: true });
|
||||
await fs.writeFile(draftPath, JSON.stringify(config, null, 2), 'utf-8');
|
||||
|
||||
return new Response(JSON.stringify({ success: true, section: newSection }), { status: 201 });
|
||||
return new Response(JSON.stringify({ success: true, section_id: secId }), { status: 201 });
|
||||
} catch (error) {
|
||||
console.error('[API /api/editor/add-section] Exception:', error);
|
||||
return new Response(JSON.stringify({ error: 'Fehler beim Hinzufügen der Sektion' }), { status: 500 });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import type { APIRoute } from 'astro';
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import { getDb } from '../../../db/index';
|
||||
import { generateDraftConfig } from '../../../services/file-generator';
|
||||
|
||||
export const prerender = false;
|
||||
|
||||
const DATA_DIR = process.env.DATA_DIR || path.join(process.cwd(), 'app', 'data');
|
||||
|
||||
export const POST: APIRoute = async ({ request }) => {
|
||||
try {
|
||||
const { page_id, section_id } = await request.json();
|
||||
@@ -14,31 +12,15 @@ export const POST: APIRoute = async ({ request }) => {
|
||||
return new Response(JSON.stringify({ error: 'page_id und section_id erforderlich' }), { status: 400 });
|
||||
}
|
||||
|
||||
const draftPath = path.join(DATA_DIR, 'site.config.draft.json');
|
||||
const livePath = path.join(DATA_DIR, 'site.config.json');
|
||||
const db = getDb();
|
||||
db.prepare('DELETE FROM page_sections WHERE id = ? AND page_id = ?').run(section_id, page_id);
|
||||
|
||||
let configRaw: string;
|
||||
try {
|
||||
configRaw = await fs.readFile(draftPath, 'utf-8');
|
||||
} catch {
|
||||
configRaw = await fs.readFile(livePath, 'utf-8');
|
||||
}
|
||||
|
||||
const config = JSON.parse(configRaw);
|
||||
const targetPage = config.pages?.find((p: any) => p.id === page_id);
|
||||
|
||||
if (!targetPage) {
|
||||
return new Response(JSON.stringify({ error: 'Seite nicht gefunden' }), { status: 404 });
|
||||
}
|
||||
|
||||
// Sektion filtern / entfernen
|
||||
targetPage.sections = targetPage.sections.filter((sec: any) => sec.id !== section_id);
|
||||
|
||||
await fs.mkdir(DATA_DIR, { recursive: true });
|
||||
await fs.writeFile(draftPath, JSON.stringify(config, null, 2), 'utf-8');
|
||||
// Draft-Config neu kompilieren
|
||||
await generateDraftConfig();
|
||||
|
||||
return new Response(JSON.stringify({ success: true, message: 'Sektion gelöscht' }), { status: 200 });
|
||||
} catch (error) {
|
||||
console.error('[API /api/editor/delete-section] Exception:', error);
|
||||
return new Response(JSON.stringify({ error: 'Fehler beim Löschen der Sektion' }), { status: 500 });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,27 +1,13 @@
|
||||
import type { APIRoute } from 'astro';
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import { publishLiveConfig } from '../../../services/file-generator';
|
||||
|
||||
export const prerender = false;
|
||||
|
||||
const DATA_DIR = process.env.DATA_DIR || path.join(process.cwd(), 'app', 'data');
|
||||
|
||||
export const POST: APIRoute = async () => {
|
||||
try {
|
||||
const draftPath = path.join(DATA_DIR, 'site.config.draft.json');
|
||||
const livePath = path.join(DATA_DIR, 'site.config.json');
|
||||
const liveConfig = await publishLiveConfig();
|
||||
|
||||
// Prüfe ob ein Entwurf existiert
|
||||
try {
|
||||
await fs.access(draftPath);
|
||||
} catch {
|
||||
return new Response(JSON.stringify({ error: 'Kein gespeicherter Entwurf vorhanden' }), { status: 404 });
|
||||
}
|
||||
|
||||
// Atomares Kopieren des Entwurfs auf die Live-Konfiguration
|
||||
await fs.copyFile(draftPath, livePath);
|
||||
|
||||
return new Response(JSON.stringify({ success: true, message: 'Website erfolgreich veröffentlicht!' }), { status: 200 });
|
||||
return new Response(JSON.stringify({ success: true, message: 'Website erfolgreich veröffentlicht!', config: liveConfig }), { status: 200 });
|
||||
} catch (error) {
|
||||
return new Response(JSON.stringify({ error: 'Fehler beim Veröffentlichen' }), { status: 500 });
|
||||
}
|
||||
|
||||
@@ -1,24 +1,16 @@
|
||||
import type { APIRoute } from 'astro';
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import { generateDraftConfig } from '../../../services/file-generator';
|
||||
|
||||
export const prerender = false;
|
||||
|
||||
const DATA_DIR = process.env.DATA_DIR || path.join(process.cwd(), 'app', 'data');
|
||||
|
||||
export const POST: APIRoute = async ({ request }) => {
|
||||
try {
|
||||
const draftConfig = await request.json();
|
||||
if (!draftConfig || typeof draftConfig !== 'object') {
|
||||
return new Response(JSON.stringify({ error: 'Ungültige Konfigurationsdaten' }), { status: 400 });
|
||||
}
|
||||
|
||||
const draftPath = path.join(DATA_DIR, 'site.config.draft.json');
|
||||
const draftBody = await request.json();
|
||||
|
||||
await fs.mkdir(DATA_DIR, { recursive: true });
|
||||
await fs.writeFile(draftPath, JSON.stringify(draftConfig, null, 2), 'utf-8');
|
||||
// Generiert aktualisierte site.config.draft.json
|
||||
const draftConfig = await generateDraftConfig();
|
||||
|
||||
return new Response(JSON.stringify({ success: true, message: 'Entwurf erfolgreich gespeichert' }), { status: 200 });
|
||||
return new Response(JSON.stringify({ success: true, message: 'Entwurf erfolgreich gespeichert', config: draftConfig }), { status: 200 });
|
||||
} catch (error) {
|
||||
return new Response(JSON.stringify({ error: 'Fehler beim Speichern des Entwurfs' }), { status: 500 });
|
||||
}
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import type { APIRoute } from 'astro';
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import { getDb } from '../../../db/index';
|
||||
import { generateDraftConfig } from '../../../services/file-generator';
|
||||
|
||||
export const prerender = false;
|
||||
|
||||
const DATA_DIR = process.env.DATA_DIR || path.join(process.cwd(), 'app', 'data');
|
||||
|
||||
export const POST: APIRoute = async ({ request }) => {
|
||||
try {
|
||||
const { page_id, section_id, settings } = await request.json();
|
||||
@@ -14,36 +12,45 @@ export const POST: APIRoute = async ({ request }) => {
|
||||
return new Response(JSON.stringify({ error: 'page_id, section_id und settings erforderlich' }), { status: 400 });
|
||||
}
|
||||
|
||||
const draftPath = path.join(DATA_DIR, 'site.config.draft.json');
|
||||
const livePath = path.join(DATA_DIR, 'site.config.json');
|
||||
const db = getDb();
|
||||
const existingRow = db.prepare('SELECT content_draft_json, styles_json FROM page_sections WHERE id = ?').get(section_id) as any;
|
||||
|
||||
let configRaw: string;
|
||||
try {
|
||||
configRaw = await fs.readFile(draftPath, 'utf-8');
|
||||
} catch {
|
||||
configRaw = await fs.readFile(livePath, 'utf-8');
|
||||
}
|
||||
|
||||
const config = JSON.parse(configRaw);
|
||||
const targetPage = config.pages?.find((p: any) => p.id === page_id);
|
||||
|
||||
if (!targetPage) {
|
||||
return new Response(JSON.stringify({ error: 'Seite nicht gefunden' }), { status: 404 });
|
||||
}
|
||||
|
||||
// Sektion finden und Einstellungen aktualisieren
|
||||
const targetSection = targetPage.sections?.find((sec: any) => sec.id === section_id);
|
||||
if (!targetSection) {
|
||||
if (!existingRow) {
|
||||
return new Response(JSON.stringify({ error: 'Sektion nicht gefunden' }), { status: 404 });
|
||||
}
|
||||
|
||||
targetSection.settings = { ...targetSection.settings, ...settings };
|
||||
const currentDraft = JSON.parse(existingRow.content_draft_json || '{}');
|
||||
const currentStyles = JSON.parse(existingRow.styles_json || '{}');
|
||||
|
||||
await fs.mkdir(DATA_DIR, { recursive: true });
|
||||
await fs.writeFile(draftPath, JSON.stringify(config, null, 2), 'utf-8');
|
||||
// Trenne Styles (z. B. bg_color, accent_color) von typischem Inhalt
|
||||
const { bg_color, accent_color, animation, backdrop_blur, liquid_edge, padding_top, padding_right, padding_bottom, padding_left, ...contentFields } = settings;
|
||||
|
||||
const updatedDraft = { ...currentDraft, ...contentFields };
|
||||
const updatedStyles = {
|
||||
...currentStyles,
|
||||
...(bg_color !== undefined ? { bg_color } : {}),
|
||||
...(accent_color !== undefined ? { accent_color } : {}),
|
||||
...(animation !== undefined ? { animation } : {}),
|
||||
...(backdrop_blur !== undefined ? { backdrop_blur } : {}),
|
||||
...(liquid_edge !== undefined ? { liquid_edge } : {}),
|
||||
...(padding_top !== undefined ? { padding_top } : {}),
|
||||
...(padding_right !== undefined ? { padding_right } : {}),
|
||||
...(padding_bottom !== undefined ? { padding_bottom } : {}),
|
||||
...(padding_left !== undefined ? { padding_left } : {})
|
||||
};
|
||||
|
||||
return new Response(JSON.stringify({ success: true, settings: targetSection.settings }), { status: 200 });
|
||||
db.prepare('UPDATE page_sections SET content_draft_json = ?, styles_json = ? WHERE id = ?').run(
|
||||
JSON.stringify(updatedDraft),
|
||||
JSON.stringify(updatedStyles),
|
||||
section_id
|
||||
);
|
||||
|
||||
// Draft-Config neu kompilieren
|
||||
await generateDraftConfig();
|
||||
|
||||
return new Response(JSON.stringify({ success: true, settings: { ...updatedDraft, ...updatedStyles } }), { status: 200 });
|
||||
} catch (error) {
|
||||
console.error('[API /api/editor/update-section-settings] Exception:', error);
|
||||
return new Response(JSON.stringify({ error: 'Fehler beim Aktualisieren der Sektions-Einstellungen' }), { status: 500 });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import type { APIRoute } from 'astro';
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import { getDb } from '../../../db/index';
|
||||
import { generateDraftConfig } from '../../../services/file-generator';
|
||||
|
||||
export const prerender = false;
|
||||
|
||||
const DATA_DIR = process.env.DATA_DIR || path.join(process.cwd(), 'app', 'data');
|
||||
|
||||
export const POST: APIRoute = async ({ request }) => {
|
||||
try {
|
||||
const { page_id, sections } = await request.json();
|
||||
@@ -14,36 +12,23 @@ export const POST: APIRoute = async ({ request }) => {
|
||||
return new Response(JSON.stringify({ error: 'page_id und sections-Array erforderlich' }), { status: 400 });
|
||||
}
|
||||
|
||||
const draftPath = path.join(DATA_DIR, 'site.config.draft.json');
|
||||
const livePath = path.join(DATA_DIR, 'site.config.json');
|
||||
const db = getDb();
|
||||
const updateStmt = db.prepare('UPDATE page_sections SET order_index = ?, content_draft_json = ? WHERE id = ?');
|
||||
|
||||
// 1. Lade bestehende Config (Entwurf bevorzugt)
|
||||
let configRaw: string;
|
||||
try {
|
||||
configRaw = await fs.readFile(draftPath, 'utf-8');
|
||||
} catch {
|
||||
configRaw = await fs.readFile(livePath, 'utf-8');
|
||||
}
|
||||
sections.forEach((sec, index) => {
|
||||
const draftJson = JSON.stringify(sec.settings || sec.content || {});
|
||||
updateStmt.run(index, draftJson, sec.id);
|
||||
});
|
||||
|
||||
const config = JSON.parse(configRaw);
|
||||
|
||||
// 2. Finde die Zielseite und aktualisiere das sections-Array
|
||||
const targetPage = config.pages?.find((p: any) => p.id === page_id);
|
||||
if (!targetPage) {
|
||||
return new Response(JSON.stringify({ error: 'Zielseite nicht gefunden' }), { status: 404 });
|
||||
}
|
||||
|
||||
targetPage.sections = sections;
|
||||
|
||||
// 3. Speichere ausschließlich in site.config.draft.json
|
||||
await fs.mkdir(DATA_DIR, { recursive: true });
|
||||
await fs.writeFile(draftPath, JSON.stringify(config, null, 2), 'utf-8');
|
||||
// Draft-Config neu kompilieren
|
||||
await generateDraftConfig();
|
||||
|
||||
return new Response(
|
||||
JSON.stringify({ success: true, message: 'Sektionen im Entwurf aktualisiert' }),
|
||||
{ status: 200 }
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('[API /api/editor/update-section] Exception:', error);
|
||||
return new Response(JSON.stringify({ error: 'Fehler beim Aktualisieren der Sektionen' }), { status: 500 });
|
||||
}
|
||||
};
|
||||
|
||||
98
src/services/file-generator.ts
Normal file
98
src/services/file-generator.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import { getDb } from '../db/index';
|
||||
|
||||
const DATA_DIR = process.env.DATA_DIR || path.join(process.cwd(), 'app', 'data');
|
||||
|
||||
export async function generateDraftConfig(): Promise<any> {
|
||||
const db = getDb();
|
||||
|
||||
// 1. Globale Settings laden
|
||||
const settingsRow = db.prepare('SELECT value_json FROM site_settings WHERE key = ?').get('site_config') as { value_json: string } | undefined;
|
||||
const baseConfig = settingsRow ? JSON.parse(settingsRow.value_json) : {};
|
||||
|
||||
// 2. Alle Seiten laden
|
||||
const pagesRows = db.prepare('SELECT * FROM pages ORDER BY created_at ASC').all() as any[];
|
||||
|
||||
const pages = pagesRows.map((page) => {
|
||||
const sectionsRows = db.prepare('SELECT * FROM page_sections WHERE page_id = ? ORDER BY order_index ASC').all(page.id) as any[];
|
||||
|
||||
const sections = sectionsRows.map((sec) => {
|
||||
const draftContent = JSON.parse(sec.content_draft_json || '{}');
|
||||
const styles = JSON.parse(sec.styles_json || '{}');
|
||||
return {
|
||||
id: sec.id,
|
||||
type: sec.section_type,
|
||||
settings: { ...draftContent, ...styles }
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
id: page.id,
|
||||
slug: page.slug,
|
||||
title: page.title,
|
||||
is_published: page.status === 'published',
|
||||
sections
|
||||
};
|
||||
});
|
||||
|
||||
const fullDraftConfig = {
|
||||
...baseConfig,
|
||||
pages,
|
||||
updated_at: new Date().toISOString()
|
||||
};
|
||||
|
||||
const draftPath = path.join(DATA_DIR, 'site.config.draft.json');
|
||||
await fs.mkdir(DATA_DIR, { recursive: true });
|
||||
await fs.writeFile(draftPath, JSON.stringify(fullDraftConfig, null, 2), 'utf-8');
|
||||
|
||||
return fullDraftConfig;
|
||||
}
|
||||
|
||||
export async function publishLiveConfig(): Promise<any> {
|
||||
const db = getDb();
|
||||
|
||||
// 1. In DB: Setze content_published_json = content_draft_json für alle Sektionen
|
||||
db.exec('UPDATE page_sections SET content_published_json = content_draft_json');
|
||||
|
||||
// 2. Globale Settings laden
|
||||
const settingsRow = db.prepare('SELECT value_json FROM site_settings WHERE key = ?').get('site_config') as { value_json: string } | undefined;
|
||||
const baseConfig = settingsRow ? JSON.parse(settingsRow.value_json) : {};
|
||||
|
||||
// 3. Alle veröffentlichten Seiten laden
|
||||
const pagesRows = db.prepare('SELECT * FROM pages WHERE status = ? ORDER BY created_at ASC').all('published') as any[];
|
||||
|
||||
const pages = pagesRows.map((page) => {
|
||||
const sectionsRows = db.prepare('SELECT * FROM page_sections WHERE page_id = ? ORDER BY order_index ASC').all(page.id) as any[];
|
||||
|
||||
const sections = sectionsRows.map((sec) => {
|
||||
const pubContent = JSON.parse(sec.content_published_json || '{}');
|
||||
const styles = JSON.parse(sec.styles_json || '{}');
|
||||
return {
|
||||
id: sec.id,
|
||||
type: sec.section_type,
|
||||
settings: { ...pubContent, ...styles }
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
id: page.id,
|
||||
slug: page.slug,
|
||||
title: page.title,
|
||||
is_published: true,
|
||||
sections
|
||||
};
|
||||
});
|
||||
|
||||
const fullLiveConfig = {
|
||||
...baseConfig,
|
||||
pages,
|
||||
updated_at: new Date().toISOString()
|
||||
};
|
||||
|
||||
const livePath = path.join(DATA_DIR, 'site.config.json');
|
||||
await fs.mkdir(DATA_DIR, { recursive: true });
|
||||
await fs.writeFile(livePath, JSON.stringify(fullLiveConfig, null, 2), 'utf-8');
|
||||
|
||||
return fullLiveConfig;
|
||||
}
|
||||
@@ -12,6 +12,18 @@ body {
|
||||
color: var(--theme-text) !important;
|
||||
}
|
||||
|
||||
.text-accent {
|
||||
color: var(--theme-accent) !important;
|
||||
}
|
||||
|
||||
.bg-accent {
|
||||
background-color: var(--theme-accent) !important;
|
||||
}
|
||||
|
||||
.border-accent {
|
||||
border-color: var(--theme-accent) !important;
|
||||
}
|
||||
|
||||
.glass-panel {
|
||||
background: var(--theme-card-bg);
|
||||
backdrop-filter: blur(16px);
|
||||
@@ -29,6 +41,7 @@ body {
|
||||
box-shadow: 0 0 25px -5px var(--theme-accent);
|
||||
}
|
||||
|
||||
|
||||
.aurora-glow {
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { detectDeviceType } from './deviceDetect';
|
||||
import { getDb } from '../db/index';
|
||||
export interface UIComponent {
|
||||
id: string; // Eindeutige ID (z.B. uuid / timestamp)
|
||||
slotId: string; // Platz-ID (z.B. 'hero-badge-slot', 'hero-actions-slot', 'bento-slot-1')
|
||||
@@ -47,17 +48,20 @@ export interface SiteConfig {
|
||||
ctaSecondaryText: string;
|
||||
ctaSecondaryLink: string;
|
||||
animation?: string;
|
||||
bg_color?: string;
|
||||
elements?: UIComponent[];
|
||||
};
|
||||
bento: {
|
||||
sectionTitle: string;
|
||||
sectionSubtitle: string;
|
||||
animation?: string;
|
||||
bg_color?: string;
|
||||
cards: Array<{
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
colSpan?: number;
|
||||
|
||||
badge?: string;
|
||||
icon?: string;
|
||||
animation?: string;
|
||||
@@ -71,9 +75,28 @@ export interface SiteConfig {
|
||||
address?: string;
|
||||
};
|
||||
sectionsOrder: string[];
|
||||
|
||||
sectionsDetails?: SectionConfig[];
|
||||
is_demo_mode?: boolean;
|
||||
site_info?: {
|
||||
title?: string;
|
||||
company_name?: string;
|
||||
homepage_id?: string;
|
||||
email?: string;
|
||||
phone?: string;
|
||||
address?: string;
|
||||
};
|
||||
pages?: Array<{
|
||||
id: string;
|
||||
slug: string;
|
||||
title: string;
|
||||
is_published?: boolean;
|
||||
seo?: { description?: string; og_image?: string };
|
||||
sections?: Array<{ id: string; type: string; settings?: Record<string, any> }>;
|
||||
}>;
|
||||
}
|
||||
|
||||
|
||||
export interface ThemeConfig {
|
||||
activePreset: string;
|
||||
glassBlur?: string; // 'none' | 'sm' | 'md' | 'lg' | 'xl' | '2xl'
|
||||
@@ -81,10 +104,14 @@ export interface ThemeConfig {
|
||||
glassBorderRadius?: string; // 'rounded-lg' | 'rounded-xl' | 'rounded-2xl'
|
||||
customColors?: {
|
||||
bg?: string;
|
||||
cardBg?: string;
|
||||
text?: string;
|
||||
accent?: string;
|
||||
border?: string;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
export interface SmtpConfig {
|
||||
host: string;
|
||||
port: number;
|
||||
@@ -99,6 +126,8 @@ export interface SmtpConfig {
|
||||
export const FALLBACK_SITE_CONFIG: SiteConfig = {
|
||||
siteName: 'N&D IT Solutions',
|
||||
metaDescription: 'High-Performance Webdesign & Digital Solutions - 100% DSGVO-konform.',
|
||||
is_demo_mode: false,
|
||||
|
||||
header: {
|
||||
brandName: 'N&D',
|
||||
brandHighlight: 'i-t SOLUTIONS',
|
||||
@@ -175,9 +204,45 @@ export const FALLBACK_SITE_CONFIG: SiteConfig = {
|
||||
{ id: 'hero', type: 'hero', title: 'Hero Sektion', elements: [ { id: 'el-1', slotId: 'hero-badge-slot', type: 'badge', content: 'WaaS Engine' } ] },
|
||||
{ id: 'bento', type: 'bento', title: 'Bento Grid', elements: [] },
|
||||
{ id: 'contact', type: 'contact', title: 'Contact Section', elements: [] }
|
||||
],
|
||||
site_info: {
|
||||
title: 'N&D IT Solutions',
|
||||
homepage_id: 'page_home'
|
||||
},
|
||||
pages: [
|
||||
{
|
||||
id: 'page_home',
|
||||
slug: '/',
|
||||
title: 'Startseite (Hauptseite)',
|
||||
is_published: true,
|
||||
sections: [
|
||||
{ id: 'hero', type: 'HeroSection', settings: {} },
|
||||
{ id: 'bento', type: 'BentoGrid', settings: {} },
|
||||
{ id: 'contact', type: 'ContactSection', settings: {} }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'page_templates',
|
||||
slug: '/templates',
|
||||
title: 'Templates & Design-Muster',
|
||||
is_published: true,
|
||||
sections: [
|
||||
{ id: 'hero_templates', type: 'HeroSection', settings: { title: 'Templates', subtitle: 'Unsere fertigen Design-Vorlagen' } }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'page_privacy',
|
||||
slug: '/datenschutz',
|
||||
title: 'Datenschutzerklärung',
|
||||
is_published: true,
|
||||
sections: [
|
||||
{ id: 'privacy_content', type: 'HeroSection', settings: { title: 'Datenschutz', subtitle: '100% DSGVO-konforme Datenverarbeitung.' } }
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
|
||||
export const FALLBACK_THEME_CONFIG: ThemeConfig = {
|
||||
activePreset: 'corporateDark'
|
||||
};
|
||||
@@ -219,7 +284,29 @@ export function loadWaasConfigs(userAgent: string = '', isDraft: boolean = false
|
||||
const genericSitePath = path.join(dataDir, 'site.config.json');
|
||||
|
||||
let sitePath = genericSitePath;
|
||||
if (isDraft && fs.existsSync(draftSitePath)) {
|
||||
if (isDraft) {
|
||||
if (!fs.existsSync(draftSitePath)) {
|
||||
try {
|
||||
const db = getDb();
|
||||
const settingsRow = db.prepare('SELECT value_json FROM site_settings WHERE key = ?').get('site_config') as { value_json: string } | undefined;
|
||||
const baseConfig = settingsRow ? JSON.parse(settingsRow.value_json) : {};
|
||||
const pagesRows = db.prepare('SELECT * FROM pages ORDER BY created_at ASC').all() as any[];
|
||||
const pages = pagesRows.map((page) => {
|
||||
const sectionsRows = db.prepare('SELECT * FROM page_sections WHERE page_id = ? ORDER BY order_index ASC').all(page.id) as any[];
|
||||
const sections = sectionsRows.map((sec) => ({
|
||||
id: sec.id,
|
||||
type: sec.section_type,
|
||||
settings: { ...JSON.parse(sec.content_draft_json || '{}'), ...JSON.parse(sec.styles_json || '{}') }
|
||||
}));
|
||||
return { id: page.id, slug: page.slug, title: page.title, is_published: page.status === 'published', sections };
|
||||
});
|
||||
const fullDraftConfig = { ...baseConfig, pages, updated_at: new Date().toISOString() };
|
||||
fs.mkdirSync(dataDir, { recursive: true });
|
||||
fs.writeFileSync(draftSitePath, JSON.stringify(fullDraftConfig, null, 2), 'utf-8');
|
||||
} catch (e) {
|
||||
console.warn('[WaaS ConfigLoader] Fallback draft compilation warning:', e);
|
||||
}
|
||||
}
|
||||
sitePath = draftSitePath;
|
||||
} else if (fs.existsSync(deviceSitePath)) {
|
||||
sitePath = deviceSitePath;
|
||||
@@ -239,9 +326,12 @@ export function loadWaasConfigs(userAgent: string = '', isDraft: boolean = false
|
||||
...rawSite,
|
||||
hero: { ...FALLBACK_SITE_CONFIG.hero, ...(rawSite.hero || {}) },
|
||||
bento: { ...FALLBACK_SITE_CONFIG.bento, ...(rawSite.bento || {}) },
|
||||
contact: { ...FALLBACK_SITE_CONFIG.contact, ...(rawSite.contact || {}) }
|
||||
contact: { ...FALLBACK_SITE_CONFIG.contact, ...(rawSite.contact || {}) },
|
||||
pages: (rawSite as any).pages && (rawSite as any).pages.length > 0 ? (rawSite as any).pages : (FALLBACK_SITE_CONFIG as any).pages,
|
||||
site_info: { ...(FALLBACK_SITE_CONFIG as any).site_info, ...((rawSite as any).site_info || {}) }
|
||||
};
|
||||
|
||||
|
||||
const themeConfig = readJsonFile<ThemeConfig>(themePath, FALLBACK_THEME_CONFIG);
|
||||
const smtpConfig = readJsonFile<SmtpConfig>(smtpPath, FALLBACK_SMTP_CONFIG);
|
||||
|
||||
|
||||
30
tests/fileGenerator.test.ts
Normal file
30
tests/fileGenerator.test.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { getDb } from '../src/db/index';
|
||||
import { generateDraftConfig, publishLiveConfig } from '../src/services/file-generator';
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
|
||||
describe('DB-Driven File Generation Service Test', () => {
|
||||
it('soll Entwurf aus DB lesen und site.config.draft.json generieren', async () => {
|
||||
const draftConfig = await generateDraftConfig();
|
||||
expect(draftConfig).toBeDefined();
|
||||
expect(draftConfig.pages).toBeDefined();
|
||||
expect(Array.isArray(draftConfig.pages)).toBe(true);
|
||||
|
||||
const DATA_DIR = process.env.DATA_DIR || path.join(process.cwd(), 'app', 'data');
|
||||
const draftPath = path.join(DATA_DIR, 'site.config.draft.json');
|
||||
const exists = await fs.access(draftPath).then(() => true).catch(() => false);
|
||||
expect(exists).toBe(true);
|
||||
});
|
||||
|
||||
it('soll Live-Config aus DB freigeben und site.config.json atomar erstellen', async () => {
|
||||
const liveConfig = await publishLiveConfig();
|
||||
expect(liveConfig).toBeDefined();
|
||||
expect(liveConfig.pages).toBeDefined();
|
||||
|
||||
const DATA_DIR = process.env.DATA_DIR || path.join(process.cwd(), 'app', 'data');
|
||||
const livePath = path.join(DATA_DIR, 'site.config.json');
|
||||
const exists = await fs.access(livePath).then(() => true).catch(() => false);
|
||||
expect(exists).toBe(true);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user