Compare commits

...

7 Commits

6 changed files with 221 additions and 23 deletions

View File

@@ -20,7 +20,10 @@ interface Props {
const { siteConfig, themeConfig } = loadWaasConfigs(Astro.request.headers.get('user-agent') ?? ''); const { siteConfig, themeConfig } = loadWaasConfigs(Astro.request.headers.get('user-agent') ?? '');
const activePreset = THEME_PRESETS[themeConfig.activePreset] || THEME_PRESETS.corporateDark; 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.", ogImage } = Astro.props;
@@ -54,10 +57,11 @@ const { title, description = "Professionelles Webdesign & moderne Websites.", og
html.light { html.light {
--theme-bg: #F8FAFC; --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-text: #0F172A;
--theme-border: rgba(0, 0, 0, 0.1); --theme-border: rgba(0, 0, 0, 0.1);
} }
</style> </style>
<!-- Inline theme initialization to prevent FOUC --> <!-- Inline theme initialization to prevent FOUC -->

View File

@@ -169,15 +169,81 @@ const previewUrl = `${targetPage?.slug || '/'}?preview=draft`;
}); });
// 4. Preset Wechsel // 4. Preset Wechsel
document.getElementById('preset-selector')?.addEventListener('change', (e: any) => { document.getElementById('preset-selector')?.addEventListener('change', async (e: any) => {
sendToIframe('THEME_UPDATE', { preset: e.target.value }); const presetKey = e.target.value;
sendToIframe('THEME_UPDATE', { preset: presetKey });
// Speichere Preset in theme.config.json
await fetch('/api/config', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ themeConfig: { activePreset: presetKey } })
});
});
// Helper zum Verschieben & Speichern der Sektionen-Reihenfolge
async function updateSectionsOrder(newSections: any[]) {
const urlParams = new URLSearchParams(window.location.search);
const pageId = urlParams.get('page_id') || 'page_home';
const res = await fetch('/api/editor/update-section', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ page_id: pageId, sections: newSections })
});
if (res.ok) {
window.location.reload();
}
}
function getSectionsFromDOM() {
const list: any[] = [];
document.querySelectorAll('#section-list > div').forEach(card => {
const id = card.getAttribute('data-section-id');
const type = card.getAttribute('data-section-type');
const settings = JSON.parse(card.getAttribute('data-settings') || '{}');
list.push({ id, type, settings });
});
return list;
}
// Event: Sektion nach oben / unten verschieben
document.querySelectorAll('.btn-move-up').forEach(btn => {
btn.addEventListener('click', async (e: any) => {
const sections = getSectionsFromDOM();
const card = e.target.closest('[data-section-id]');
const sectionId = card.getAttribute('data-section-id');
const index = sections.findIndex(s => s.id === sectionId);
if (index > 0) {
const temp = sections[index];
sections[index] = sections[index - 1];
sections[index - 1] = temp;
await updateSectionsOrder(sections);
}
});
});
document.querySelectorAll('.btn-move-down').forEach(btn => {
btn.addEventListener('click', async (e: any) => {
const sections = getSectionsFromDOM();
const card = e.target.closest('[data-section-id]');
const sectionId = card.getAttribute('data-section-id');
const index = sections.findIndex(s => s.id === sectionId);
if (index >= 0 && index < sections.length - 1) {
const temp = sections[index];
sections[index] = sections[index + 1];
sections[index + 1] = temp;
await updateSectionsOrder(sections);
}
});
}); });
// 5. Event: Sektion hinzufügen // 5. Event: Sektion hinzufügen
document.getElementById('btn-add-section')?.addEventListener('click', async () => { document.getElementById('btn-add-section')?.addEventListener('click', async () => {
const sectionType = (document.getElementById('select-new-section-type') as HTMLSelectElement).value; const sectionType = (document.getElementById('select-new-section-type') as HTMLSelectElement).value;
const urlParams = new URLSearchParams(window.location.search); const urlParams = new URLSearchParams(window.location.search);
const pageId = urlParams.get('page_id'); const pageId = urlParams.get('page_id') || 'page_home';
const res = await fetch('/api/editor/add-section', { const res = await fetch('/api/editor/add-section', {
method: 'POST', method: 'POST',
@@ -196,7 +262,7 @@ const previewUrl = `${targetPage?.slug || '/'}?preview=draft`;
const card = e.target.closest('[data-section-id]'); const card = e.target.closest('[data-section-id]');
const sectionId = card?.getAttribute('data-section-id'); const sectionId = card?.getAttribute('data-section-id');
const urlParams = new URLSearchParams(window.location.search); const urlParams = new URLSearchParams(window.location.search);
const pageId = urlParams.get('page_id'); const pageId = urlParams.get('page_id') || 'page_home';
const res = await fetch('/api/editor/delete-section', { const res = await fetch('/api/editor/delete-section', {
method: 'POST', method: 'POST',
@@ -259,7 +325,7 @@ const previewUrl = `${targetPage?.slug || '/'}?preview=draft`;
formData.forEach((val, key) => { settings[key] = val; }); formData.forEach((val, key) => { settings[key] = val; });
const urlParams = new URLSearchParams(window.location.search); const urlParams = new URLSearchParams(window.location.search);
const pageId = urlParams.get('page_id'); const pageId = urlParams.get('page_id') || 'page_home';
const res = await fetch('/api/editor/update-section-settings', { const res = await fetch('/api/editor/update-section-settings', {
method: 'POST', method: 'POST',
@@ -269,9 +335,8 @@ const previewUrl = `${targetPage?.slug || '/'}?preview=draft`;
if (res.ok) { if (res.ok) {
editModal?.close(); editModal?.close();
// Schicke Live-Reload Signal an Iframe Bridge window.location.reload();
const iframeElement = document.getElementById('preview-iframe') as HTMLIFrameElement;
iframeElement?.contentWindow?.postMessage({ type: 'RELOAD_CANVAS' }, '*');
} }
}); });
</script> </script>

View File

@@ -72,12 +72,13 @@ const presets = Object.values(THEME_PRESETS);
<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> <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>
<label class="flex items-center gap-3 pt-2 p-3 bg-amber-500/10 border border-amber-500/20 rounded-xl cursor-pointer"> <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 as any).is_demo_mode ?? false} class="rounded text-amber-400 focus:ring-0 h-4 w-4" /> <input type="checkbox" id="isDemoMode" checked={!!siteConfig.is_demo_mode} class="rounded text-amber-400 focus:ring-0 h-4 w-4" />
<div> <div>
<span class="text-xs text-amber-400 font-bold block">🎭 Demo-Modus aktivieren</span> <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> <span class="text-[10px] text-slate-400">Zeigt einen festen Demo-Hinweis für Interessenten auf der Live-Website</span>
</div> </div>
</label> </label>
</div> </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> <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>
@@ -99,19 +100,66 @@ const presets = Object.values(THEME_PRESETS);
<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" /> <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>
</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>
<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>
</div> </div>
<!-- Sidebar Column --> <!-- Sidebar Column -->
<div class="space-y-8"> <div class="space-y-8">
<!-- Theme Preset Picker --> <!-- Theme Preset Picker & Custom Colors -->
<div class="glass-panel rounded-2xl p-6 space-y-4"> <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"> <h2 class="text-lg font-bold text-sky-400 uppercase tracking-wider flex items-center gap-2">
Theme Preset (10 Styles) Theme Preset (10 Styles)
</h2> </h2>
<div class="space-y-2 max-h-[320px] overflow-y-auto pr-1"> <div class="space-y-2 max-h-[220px] overflow-y-auto pr-1">
{presets.map((p) => ( {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'}`}> <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"> <div class="flex items-center gap-3">
@@ -125,7 +173,30 @@ const presets = Object.values(THEME_PRESETS);
</label> </label>
))} ))}
</div> </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>
<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 --> <!-- SMTP Config -->
<div class="glass-panel rounded-3xl p-6 space-y-4"> <div class="glass-panel rounded-3xl p-6 space-y-4">
@@ -192,12 +263,33 @@ const presets = Object.values(THEME_PRESETS);
email: getVal('infoEmail'), email: getVal('infoEmail'),
phone: getVal('infoPhone'), phone: getVal('infoPhone'),
address: getVal('infoAddress') address: getVal('infoAddress')
},
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: { themeConfig: {
activePreset: selectedPreset activePreset: selectedPreset,
customColors: {
bg: getVal('customColorBg'),
accent: getVal('customColorAccent'),
text: getVal('customColorText'),
border: getVal('customColorBorder')
}
}, },
smtpConfig: { smtpConfig: {
host: getVal('smtpHost'), host: getVal('smtpHost'),
port: getNum('smtpPort'), port: getNum('smtpPort'),

View File

@@ -11,16 +11,35 @@ export const POST: APIRoute = async ({ request }) => {
fs.mkdirSync(dataDir, { recursive: true }); 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) { if (body.siteConfig) {
const newSite = { const newSite = {
...currentSite, ...currentSite,
...body.siteConfig, ...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) { if (body.themeConfig) {
const newTheme = { ...currentTheme, ...body.themeConfig }; const newTheme = { ...currentTheme, ...body.themeConfig };

View File

@@ -8,18 +8,29 @@ const DATA_DIR = process.env.DATA_DIR || path.join(process.cwd(), 'app', 'data')
export const POST: APIRoute = async ({ request }) => { export const POST: APIRoute = async ({ request }) => {
try { try {
const draftConfig = await request.json(); const draftBody = await request.json();
if (!draftConfig || typeof draftConfig !== 'object') { const draftPath = path.join(DATA_DIR, 'site.config.draft.json');
return new Response(JSON.stringify({ error: 'Ungültige Konfigurationsdaten' }), { status: 400 }); const livePath = path.join(DATA_DIR, 'site.config.json');
let baseConfig: any = {};
try {
const raw = await fs.readFile(draftPath, 'utf-8');
baseConfig = JSON.parse(raw);
} catch {
try {
const rawLive = await fs.readFile(livePath, 'utf-8');
baseConfig = JSON.parse(rawLive);
} catch {}
} }
const draftPath = path.join(DATA_DIR, 'site.config.draft.json'); const updatedConfig = { ...baseConfig, ...draftBody, updated_at: new Date().toISOString() };
await fs.mkdir(DATA_DIR, { recursive: true }); await fs.mkdir(DATA_DIR, { recursive: true });
await fs.writeFile(draftPath, JSON.stringify(draftConfig, null, 2), 'utf-8'); await fs.writeFile(draftPath, JSON.stringify(updatedConfig, null, 2), 'utf-8');
return new Response(JSON.stringify({ success: true, message: 'Entwurf erfolgreich gespeichert' }), { status: 200 }); return new Response(JSON.stringify({ success: true, message: 'Entwurf erfolgreich gespeichert' }), { status: 200 });
} catch (error) { } catch (error) {
return new Response(JSON.stringify({ error: 'Fehler beim Speichern des Entwurfs' }), { status: 500 }); return new Response(JSON.stringify({ error: 'Fehler beim Speichern des Entwurfs' }), { status: 500 });
} }
}; };

View File

@@ -73,6 +73,7 @@ export interface SiteConfig {
sectionsOrder: string[]; sectionsOrder: string[];
sectionsDetails?: SectionConfig[]; sectionsDetails?: SectionConfig[];
is_demo_mode?: boolean;
site_info?: { site_info?: {
title?: string; title?: string;
company_name?: string; company_name?: string;
@@ -99,10 +100,14 @@ export interface ThemeConfig {
glassBorderRadius?: string; // 'rounded-lg' | 'rounded-xl' | 'rounded-2xl' glassBorderRadius?: string; // 'rounded-lg' | 'rounded-xl' | 'rounded-2xl'
customColors?: { customColors?: {
bg?: string; bg?: string;
cardBg?: string;
text?: string;
accent?: string; accent?: string;
border?: string;
}; };
} }
export interface SmtpConfig { export interface SmtpConfig {
host: string; host: string;
port: number; port: number;
@@ -117,6 +122,8 @@ export interface SmtpConfig {
export const FALLBACK_SITE_CONFIG: SiteConfig = { export const FALLBACK_SITE_CONFIG: SiteConfig = {
siteName: 'N&D IT Solutions', siteName: 'N&D IT Solutions',
metaDescription: 'High-Performance Webdesign & Digital Solutions - 100% DSGVO-konform.', metaDescription: 'High-Performance Webdesign & Digital Solutions - 100% DSGVO-konform.',
is_demo_mode: false,
header: { header: {
brandName: 'N&D', brandName: 'N&D',
brandHighlight: 'i-t SOLUTIONS', brandHighlight: 'i-t SOLUTIONS',