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
|
||||
.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 device = detectDeviceType(userAgent);
|
||||
|
||||
const draftSitePath = path.join(dataDir, 'site.config.draft.json');
|
||||
const deviceSitePath = path.join(dataDir, `site.${device}.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 genericThemePath = path.join(dataDir, 'theme.config.json');
|
||||
@@ -239,3 +247,8 @@ export function loadWaasConfigs(userAgent: string = '') {
|
||||
|
||||
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