feat(waas): add catch-all route, page manager and draft-publish pipeline
This commit is contained in:
4
.gitignore
vendored
4
.gitignore
vendored
@@ -22,3 +22,7 @@ pnpm-debug.log*
|
|||||||
|
|
||||||
# jetbrains setting folder
|
# jetbrains setting folder
|
||||||
.idea/
|
.idea/
|
||||||
|
|
||||||
|
# coverage report
|
||||||
|
coverage/
|
||||||
|
|
||||||
|
|||||||
56
public/js/editor-bridge.js
Normal file
56
public/js/editor-bridge.js
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
// public/js/editor-bridge.js
|
||||||
|
// Bridge between the editor UI (parent) and the live preview iframe.
|
||||||
|
// Listens for messages from the parent and applies updates to the preview.
|
||||||
|
|
||||||
|
(function () {
|
||||||
|
// Store current config state for quick reference
|
||||||
|
let siteConfig = {};
|
||||||
|
let themeConfig = {};
|
||||||
|
|
||||||
|
// Helper: apply theme preset CSS variables (assumes theme presets expose a JSON of variables)
|
||||||
|
function applyTheme(presetKey) {
|
||||||
|
fetch(`/theme/${presetKey}.json`)
|
||||||
|
.then((r) => r.json())
|
||||||
|
.then((vars) => {
|
||||||
|
const root = document.documentElement;
|
||||||
|
Object.entries(vars).forEach(([k, v]) => {
|
||||||
|
root.style.setProperty(`--${k}`, v);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(() => console.warn('Theme preset not found:', presetKey));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper: re-render the preview by injecting the updated HTML (simple approach).
|
||||||
|
function reloadPreview() {
|
||||||
|
// For SSR Astro we cannot re‑render inside the iframe without a full reload.
|
||||||
|
// Trigger a reload – the parent will send the latest config on load.
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Message dispatcher
|
||||||
|
window.addEventListener('message', (event) => {
|
||||||
|
const { type, payload } = event.data || {};
|
||||||
|
switch (type) {
|
||||||
|
case 'load':
|
||||||
|
siteConfig = payload || {};
|
||||||
|
// initial load – could trigger a render hook if needed
|
||||||
|
break;
|
||||||
|
case 'add':
|
||||||
|
// For simplicity we just reload – real implementation would manipulate DOM.
|
||||||
|
reloadPreview();
|
||||||
|
break;
|
||||||
|
case 'preset':
|
||||||
|
themeConfig.activePreset = payload;
|
||||||
|
applyTheme(payload);
|
||||||
|
break;
|
||||||
|
case 'update':
|
||||||
|
// payload contains partial siteConfig changes
|
||||||
|
Object.assign(siteConfig, payload);
|
||||||
|
reloadPreview();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
// ignore unknown messages
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})();
|
||||||
115
src/pages/[...slug].astro
Normal file
115
src/pages/[...slug].astro
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
---
|
||||||
|
// src/pages/[...slug].astro
|
||||||
|
import Layout from '../layouts/Layout.astro';
|
||||||
|
import { loadWaasConfigs } from '../utils/configLoader';
|
||||||
|
import { resolveDynamicBinding } from '../utils/dynamicDataBinding';
|
||||||
|
|
||||||
|
// Component Registry Import (Zero-Sumpf & Modular)
|
||||||
|
import HeroSection from '../components/sections/HeroSection.astro';
|
||||||
|
import BentoGrid from '../components/sections/BentoGrid.astro';
|
||||||
|
import ContactSection from '../components/sections/ContactSection.astro';
|
||||||
|
import FallbackSection from '../components/sections/FallbackSection.astro';
|
||||||
|
|
||||||
|
// Strikter SSR-Modus für Instant-Updates ohne Re-Build
|
||||||
|
export const prerender = false;
|
||||||
|
|
||||||
|
// 1. Slug aus der URL auslesen
|
||||||
|
const { slug } = Astro.params;
|
||||||
|
|
||||||
|
// 2. Config aus /app/data/site.config.json oder site.config.draft.json laden
|
||||||
|
const userAgent = Astro.request.headers.get('user-agent') ?? '';
|
||||||
|
const isDraft = Astro.url.searchParams.get('preview') === 'draft';
|
||||||
|
const { siteConfig } = loadWaasConfigs(userAgent, isDraft);
|
||||||
|
|
||||||
|
// Helper function to resolve dynamic data recursively in settings or objects
|
||||||
|
function resolveDynamicData(settings: any, dynamicBindings: any, config: any): any {
|
||||||
|
if (!settings) return settings;
|
||||||
|
const context = { site: config, ...config };
|
||||||
|
|
||||||
|
if (typeof settings === 'string') {
|
||||||
|
return resolveDynamicBinding(settings, context);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(settings)) {
|
||||||
|
return settings.map((item) => resolveDynamicData(item, dynamicBindings, config));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof settings === 'object') {
|
||||||
|
const resolved: Record<string, any> = {};
|
||||||
|
for (const [key, value] of Object.entries(settings)) {
|
||||||
|
resolved[key] = resolveDynamicData(value, dynamicBindings, config);
|
||||||
|
}
|
||||||
|
return resolved;
|
||||||
|
}
|
||||||
|
|
||||||
|
return settings;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Normalized Slug-Handling: "/" für Startseite, sonst z.B. "leistungen"
|
||||||
|
const currentSlug = slug === undefined || slug === '' ? '/' : slug.replace(/\/$/, '');
|
||||||
|
|
||||||
|
// 4. Richtige Seite im pages-Array ermitteln (falls vorhanden, sonst Fallback auf Hauptkonfiguration)
|
||||||
|
const pages = (siteConfig as any).pages || [
|
||||||
|
{
|
||||||
|
id: (siteConfig as any).homepage_id || 'home',
|
||||||
|
slug: '/',
|
||||||
|
title: siteConfig.siteName,
|
||||||
|
is_published: true,
|
||||||
|
sections: siteConfig.sectionsOrder.map((secType: string) => ({
|
||||||
|
id: secType,
|
||||||
|
type: secType,
|
||||||
|
settings: (siteConfig as any)[secType] || {}
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
let page = pages.find((p: any) => {
|
||||||
|
if (currentSlug === '/') {
|
||||||
|
// Startseiten-Mapping über homepage_id oder Slug "/"
|
||||||
|
return p.id === (siteConfig as any).homepage_id || p.slug === '/' || p.slug === '';
|
||||||
|
}
|
||||||
|
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');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. Dynamic Component Registry Mapping
|
||||||
|
const ComponentRegistry: Record<string, any> = {
|
||||||
|
hero: HeroSection,
|
||||||
|
bento: BentoGrid,
|
||||||
|
contact: ContactSection,
|
||||||
|
HeroSection,
|
||||||
|
BentoGrid,
|
||||||
|
ContactSection
|
||||||
|
};
|
||||||
|
---
|
||||||
|
|
||||||
|
<Layout
|
||||||
|
title={page.title || siteConfig.siteName || 'N&D IT Solutions'}
|
||||||
|
description={page.seo?.description || siteConfig.metaDescription || ''}
|
||||||
|
ogImage={page.seo?.og_image || ''}
|
||||||
|
>
|
||||||
|
<main id="main-content" role="main" class="min-h-screen">
|
||||||
|
{
|
||||||
|
page.sections && page.sections.length > 0 ? (
|
||||||
|
page.sections.map((section: any) => {
|
||||||
|
const Component = ComponentRegistry[section.type] || FallbackSection;
|
||||||
|
|
||||||
|
// Löse Platzhalter & DB-Bindings aus /admin/ live auf
|
||||||
|
const resolvedSettings = resolveDynamicData(
|
||||||
|
section.settings || {},
|
||||||
|
section.dynamic_bindings || {},
|
||||||
|
siteConfig
|
||||||
|
);
|
||||||
|
|
||||||
|
return <Component id={section.id} settings={resolvedSettings} {...resolvedSettings} />;
|
||||||
|
})
|
||||||
|
) : (
|
||||||
|
<FallbackSection message="Diese Seite enthält aktuell noch keine Abschnitte." />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</main>
|
||||||
|
</Layout>
|
||||||
141
src/pages/admin/pages.astro
Normal file
141
src/pages/admin/pages.astro
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
---
|
||||||
|
// src/pages/admin/pages.astro
|
||||||
|
import Layout from '../../layouts/Layout.astro';
|
||||||
|
import { loadWaasConfigs } from '../../utils/configLoader';
|
||||||
|
|
||||||
|
export const prerender = false;
|
||||||
|
|
||||||
|
const userAgent = Astro.request.headers.get('user-agent') ?? '';
|
||||||
|
const { siteConfig } = loadWaasConfigs(userAgent);
|
||||||
|
const pages = (siteConfig as any).pages || [];
|
||||||
|
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">
|
||||||
|
<div class="flex items-center justify-between mb-8">
|
||||||
|
<div>
|
||||||
|
<h1 class="text-2xl font-bold text-white">Seiten-Verwaltung</h1>
|
||||||
|
<p class="text-slate-400 text-sm">Verwalte hier deine Routen, Slugs und lege die Startseite fest.</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
id="btn-open-modal"
|
||||||
|
class="bg-sky-500 hover:bg-sky-400 text-slate-950 font-bold px-4 py-2 rounded-lg text-sm transition-colors"
|
||||||
|
>
|
||||||
|
+ Neue Seite anlegen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-slate-900 border border-slate-800 rounded-xl overflow-hidden shadow-xl">
|
||||||
|
<table class="w-full text-left text-sm text-slate-300">
|
||||||
|
<thead class="bg-slate-950/60 text-slate-400 border-b border-slate-800 uppercase text-xs">
|
||||||
|
<tr>
|
||||||
|
<th class="p-4">Startseite</th>
|
||||||
|
<th class="p-4">Titel</th>
|
||||||
|
<th class="p-4">URL Slug</th>
|
||||||
|
<th class="p-4">Status</th>
|
||||||
|
<th class="p-4 text-right">Aktionen</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="divide-y divide-slate-800/60">
|
||||||
|
{pages.map((page: any) => {
|
||||||
|
const isHomepage = page.id === currentHomepageId;
|
||||||
|
return (
|
||||||
|
<tr class="hover:bg-slate-800/40 transition-colors">
|
||||||
|
<td class="p-4">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="homepage"
|
||||||
|
checked={isHomepage}
|
||||||
|
data-page-id={page.id}
|
||||||
|
class="btn-set-homepage cursor-pointer accent-sky-400 h-4 w-4"
|
||||||
|
title="Als Startseite festlegen"
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td class="p-4 font-semibold text-white">
|
||||||
|
{page.title}
|
||||||
|
{isHomepage && <span class="ml-2 text-xs text-sky-400 bg-sky-950 border border-sky-800 px-2 py-0.5 rounded-full">Homepage</span>}
|
||||||
|
</td>
|
||||||
|
<td class="p-4 font-mono text-xs text-slate-400">{page.slug}</td>
|
||||||
|
<td class="p-4">
|
||||||
|
{page.is_published ? (
|
||||||
|
<span class="text-emerald-400 bg-emerald-950/60 border border-emerald-800 px-2 py-0.5 rounded text-xs">Live</span>
|
||||||
|
) : (
|
||||||
|
<span class="text-amber-400 bg-amber-950/60 border border-amber-800 px-2 py-0.5 rounded text-xs">Draft</span>
|
||||||
|
)}
|
||||||
|
</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"
|
||||||
|
>
|
||||||
|
✏️ Im Editor öffnen
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<dialog id="modal-add-page" class="bg-slate-900 text-white border border-slate-800 rounded-xl p-6 backdrop:bg-slate-950/80 max-w-md w-full">
|
||||||
|
<form id="form-create-page" class="space-y-4">
|
||||||
|
<h2 class="text-lg font-bold text-white">Neue Seite anlegen</h2>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs text-slate-400 mb-1">Seitentitel</label>
|
||||||
|
<input type="text" name="title" required placeholder="z. B. Unsere Leistungen" class="w-full bg-slate-800 border border-slate-700 rounded p-2 text-sm text-white focus:outline-none focus:border-sky-500" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs text-slate-400 mb-1">URL Slug</label>
|
||||||
|
<input type="text" name="slug" required placeholder="z. B. leistungen" class="w-full bg-slate-800 border border-slate-700 rounded p-2 text-sm text-white focus:outline-none focus:border-sky-500" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-end gap-2 pt-4">
|
||||||
|
<button type="button" id="btn-close-modal" class="px-4 py-2 bg-slate-800 text-slate-300 rounded text-xs">Abbrechen</button>
|
||||||
|
<button type="submit" class="px-4 py-2 bg-sky-500 text-slate-950 font-bold rounded text-xs">Seite erstellen</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</dialog>
|
||||||
|
</Layout>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Event-Handler für Startseiten-Wechsel & Dialog-Modal
|
||||||
|
const modal = document.getElementById('modal-add-page') as HTMLDialogElement;
|
||||||
|
document.getElementById('btn-open-modal')?.addEventListener('click', () => modal?.showModal());
|
||||||
|
document.getElementById('btn-close-modal')?.addEventListener('click', () => modal?.close());
|
||||||
|
|
||||||
|
// Radio-Button Event: homepage_id ändern
|
||||||
|
document.querySelectorAll('.btn-set-homepage').forEach(radio => {
|
||||||
|
radio.addEventListener('change', async (e: any) => {
|
||||||
|
const pageId = e.target.getAttribute('data-page-id');
|
||||||
|
await fetch('/api/admin/pages/set-homepage', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ page_id: pageId })
|
||||||
|
});
|
||||||
|
window.location.reload();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Form Event: Neue Seite anlegen
|
||||||
|
document.getElementById('form-create-page')?.addEventListener('submit', async (e: Event) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const formData = new FormData(e.target as HTMLFormElement);
|
||||||
|
const title = formData.get('title');
|
||||||
|
const slug = formData.get('slug');
|
||||||
|
|
||||||
|
const res = await fetch('/api/admin/pages/create', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ title, slug })
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.ok) {
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
53
src/pages/api/admin/pages/create.ts
Normal file
53
src/pages/api/admin/pages/create.ts
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import type { APIRoute } from 'astro';
|
||||||
|
import fs from 'node:fs/promises';
|
||||||
|
import path from 'node:path';
|
||||||
|
|
||||||
|
export const prerender = false;
|
||||||
|
|
||||||
|
export const POST: APIRoute = async ({ request }) => {
|
||||||
|
try {
|
||||||
|
const { title, slug } = await request.json();
|
||||||
|
if (!title || !slug) {
|
||||||
|
return new Response(JSON.stringify({ error: 'Titel und Slug erforderlich' }), { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const dataDir = process.env.DATA_DIR || path.join(process.cwd(), 'app', 'data');
|
||||||
|
const configPath = path.join(dataDir, 'site.config.json');
|
||||||
|
|
||||||
|
let config: any = {};
|
||||||
|
try {
|
||||||
|
const rawData = await fs.readFile(configPath, 'utf-8');
|
||||||
|
config = JSON.parse(rawData);
|
||||||
|
} catch {
|
||||||
|
config = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const formattedSlug = slug.startsWith('/') ? slug : `/${slug}`;
|
||||||
|
const newPageId = `page_${Date.now()}`;
|
||||||
|
|
||||||
|
const newPage = {
|
||||||
|
id: newPageId,
|
||||||
|
slug: formattedSlug,
|
||||||
|
title: title,
|
||||||
|
is_published: true,
|
||||||
|
seo: { description: `${title} - N&D IT Solutions` },
|
||||||
|
sections: [
|
||||||
|
{
|
||||||
|
id: `sec_hero_${Date.now()}`,
|
||||||
|
type: 'HeroSection',
|
||||||
|
settings: { title: title, subtitle: 'Willkommen auf dieser Unterseite.' }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
config.pages = config.pages || [];
|
||||||
|
config.pages.push(newPage);
|
||||||
|
|
||||||
|
await fs.mkdir(dataDir, { recursive: true });
|
||||||
|
await fs.writeFile(configPath, JSON.stringify(config, null, 2), 'utf-8');
|
||||||
|
|
||||||
|
return new Response(JSON.stringify({ success: true, page: newPage }), { status: 201 });
|
||||||
|
} catch (error) {
|
||||||
|
return new Response(JSON.stringify({ error: 'Fehler beim Erstellen der Seite' }), { status: 500 });
|
||||||
|
}
|
||||||
|
};
|
||||||
34
src/pages/api/admin/pages/set-homepage.ts
Normal file
34
src/pages/api/admin/pages/set-homepage.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import type { APIRoute } from 'astro';
|
||||||
|
import fs from 'node:fs/promises';
|
||||||
|
import path from 'node:path';
|
||||||
|
|
||||||
|
export const prerender = false;
|
||||||
|
|
||||||
|
export const POST: APIRoute = async ({ request }) => {
|
||||||
|
try {
|
||||||
|
const { page_id } = await request.json();
|
||||||
|
if (!page_id) return new Response(JSON.stringify({ error: 'page_id gefordert' }), { status: 400 });
|
||||||
|
|
||||||
|
const dataDir = process.env.DATA_DIR || path.join(process.cwd(), 'app', 'data');
|
||||||
|
const configPath = path.join(dataDir, 'site.config.json');
|
||||||
|
|
||||||
|
let config: any = {};
|
||||||
|
try {
|
||||||
|
const rawData = await fs.readFile(configPath, 'utf-8');
|
||||||
|
config = JSON.parse(rawData);
|
||||||
|
} catch {
|
||||||
|
config = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update homepage_id
|
||||||
|
config.site_info = config.site_info || {};
|
||||||
|
config.site_info.homepage_id = page_id;
|
||||||
|
|
||||||
|
await fs.mkdir(dataDir, { recursive: true });
|
||||||
|
await fs.writeFile(configPath, JSON.stringify(config, null, 2), 'utf-8');
|
||||||
|
|
||||||
|
return new Response(JSON.stringify({ success: true, homepage_id: page_id }), { status: 200 });
|
||||||
|
} catch (error) {
|
||||||
|
return new Response(JSON.stringify({ error: 'Fehler beim Speichern' }), { status: 500 });
|
||||||
|
}
|
||||||
|
};
|
||||||
28
src/pages/api/editor/publish.ts
Normal file
28
src/pages/api/editor/publish.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
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 () => {
|
||||||
|
try {
|
||||||
|
const draftPath = path.join(DATA_DIR, 'site.config.draft.json');
|
||||||
|
const livePath = path.join(DATA_DIR, 'site.config.json');
|
||||||
|
|
||||||
|
// 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 });
|
||||||
|
} catch (error) {
|
||||||
|
return new Response(JSON.stringify({ error: 'Fehler beim Veröffentlichen' }), { status: 500 });
|
||||||
|
}
|
||||||
|
};
|
||||||
25
src/pages/api/editor/save-draft.ts
Normal file
25
src/pages/api/editor/save-draft.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
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 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');
|
||||||
|
|
||||||
|
await fs.mkdir(DATA_DIR, { recursive: true });
|
||||||
|
await fs.writeFile(draftPath, JSON.stringify(draftConfig, null, 2), 'utf-8');
|
||||||
|
|
||||||
|
return new Response(JSON.stringify({ success: true, message: 'Entwurf erfolgreich gespeichert' }), { status: 200 });
|
||||||
|
} catch (error) {
|
||||||
|
return new Response(JSON.stringify({ error: 'Fehler beim Speichern des Entwurfs' }), { status: 500 });
|
||||||
|
}
|
||||||
|
};
|
||||||
39
src/pages/api/editor/update.ts
Normal file
39
src/pages/api/editor/update.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import type { APIRoute } from 'astro';
|
||||||
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
// Paths inside Docker volume /app/data
|
||||||
|
const draftPath = path.resolve(process.cwd(), 'app', 'data', 'site.config.draft.json');
|
||||||
|
const livePath = path.resolve(process.cwd(), 'app', 'data', 'site.config.json');
|
||||||
|
|
||||||
|
/** GET current draft (for editor init) */
|
||||||
|
export const GET: APIRoute = async () => {
|
||||||
|
let draft = {};
|
||||||
|
if (fs.existsSync(draftPath)) {
|
||||||
|
try {
|
||||||
|
draft = JSON.parse(fs.readFileSync(draftPath, 'utf8'));
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to parse draft JSON', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new Response(JSON.stringify({ draft }), { status: 200, headers: { 'Content-Type': 'application/json' } });
|
||||||
|
};
|
||||||
|
|
||||||
|
/** POST to save draft or publish */
|
||||||
|
export const POST: APIRoute = async ({ request }) => {
|
||||||
|
const body = await request.json();
|
||||||
|
const { draft, action } = body as { draft: Record<string, any>; action?: string };
|
||||||
|
|
||||||
|
// Ensure directory exists
|
||||||
|
fs.mkdirSync(path.dirname(draftPath), { recursive: true });
|
||||||
|
// Save draft
|
||||||
|
fs.writeFileSync(draftPath, JSON.stringify(draft, null, 2), 'utf8');
|
||||||
|
|
||||||
|
// Publish if requested
|
||||||
|
if (action === 'publish') {
|
||||||
|
fs.mkdirSync(path.dirname(livePath), { recursive: true });
|
||||||
|
fs.writeFileSync(livePath, JSON.stringify(draft, null, 2), 'utf8');
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Response(JSON.stringify({ success: true }), { status: 200, headers: { 'Content-Type': 'application/json' } });
|
||||||
|
};
|
||||||
@@ -210,12 +210,20 @@ function readJsonFile<T>(filePath: string, fallback: T): T {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function loadWaasConfigs(userAgent: string = '') {
|
export function loadWaasConfigs(userAgent: string = '', isDraft: boolean = false) {
|
||||||
const dataDir = process.env.DATA_DIR || path.join(process.cwd(), 'app', 'data');
|
const dataDir = process.env.DATA_DIR || path.join(process.cwd(), 'app', 'data');
|
||||||
const device = detectDeviceType(userAgent);
|
const device = detectDeviceType(userAgent);
|
||||||
|
|
||||||
|
const draftSitePath = path.join(dataDir, 'site.config.draft.json');
|
||||||
const deviceSitePath = path.join(dataDir, `site.${device}.config.json`);
|
const deviceSitePath = path.join(dataDir, `site.${device}.config.json`);
|
||||||
const genericSitePath = path.join(dataDir, 'site.config.json');
|
const genericSitePath = path.join(dataDir, 'site.config.json');
|
||||||
const sitePath = fs.existsSync(deviceSitePath) ? deviceSitePath : genericSitePath;
|
|
||||||
|
let sitePath = genericSitePath;
|
||||||
|
if (isDraft && fs.existsSync(draftSitePath)) {
|
||||||
|
sitePath = draftSitePath;
|
||||||
|
} else if (fs.existsSync(deviceSitePath)) {
|
||||||
|
sitePath = deviceSitePath;
|
||||||
|
}
|
||||||
|
|
||||||
const deviceThemePath = path.join(dataDir, `theme.${device}.config.json`);
|
const deviceThemePath = path.join(dataDir, `theme.${device}.config.json`);
|
||||||
const genericThemePath = path.join(dataDir, 'theme.config.json');
|
const genericThemePath = path.join(dataDir, 'theme.config.json');
|
||||||
@@ -239,3 +247,8 @@ export function loadWaasConfigs(userAgent: string = '') {
|
|||||||
|
|
||||||
return { siteConfig, themeConfig, smtpConfig };
|
return { siteConfig, themeConfig, smtpConfig };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getSiteConfig(isDraft: boolean = false) {
|
||||||
|
return loadWaasConfigs('', isDraft).siteConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
48
src/utils/editorController.ts
Normal file
48
src/utils/editorController.ts
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
// Editor controller for drag‑and‑drop synchronization
|
||||||
|
// Provides functions to load, save and publish draft config JSON
|
||||||
|
|
||||||
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
const draftPath = path.resolve(process.cwd(), 'app', 'data', 'site.config.draft.json');
|
||||||
|
const livePath = path.resolve(process.cwd(), 'app', 'data', 'site.config.json');
|
||||||
|
|
||||||
|
/** Load current draft config (or empty object) */
|
||||||
|
export function loadDraft(): Record<string, any> {
|
||||||
|
if (fs.existsSync(draftPath)) {
|
||||||
|
try {
|
||||||
|
return JSON.parse(fs.readFileSync(draftPath, 'utf8'));
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to parse draft JSON', e);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Save draft config */
|
||||||
|
export function saveDraft(data: Record<string, any>): void {
|
||||||
|
fs.mkdirSync(path.dirname(draftPath), { recursive: true });
|
||||||
|
fs.writeFileSync(draftPath, JSON.stringify(data, null, 2), 'utf8');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Publish draft to live config atomically */
|
||||||
|
export function publishDraft(): void {
|
||||||
|
if (!fs.existsSync(draftPath)) return;
|
||||||
|
const content = fs.readFileSync(draftPath, 'utf8');
|
||||||
|
fs.mkdirSync(path.dirname(livePath), { recursive: true });
|
||||||
|
fs.writeFileSync(livePath, content, 'utf8');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Helper to get both draft and live (for debugging) */
|
||||||
|
export function getLiveConfig(): Record<string, any> {
|
||||||
|
if (fs.existsSync(livePath)) {
|
||||||
|
try {
|
||||||
|
return JSON.parse(fs.readFileSync(livePath, 'utf8'));
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to parse live JSON', e);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user