Compare commits
3 Commits
0c073983e9
...
03ceb3de7f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
03ceb3de7f | ||
|
|
a35efb2726 | ||
|
|
58fc8e51cd |
4
.gitignore
vendored
4
.gitignore
vendored
@@ -29,4 +29,8 @@ coverage/
|
||||
# draft files
|
||||
*.draft.json
|
||||
|
||||
# test data directories
|
||||
app/test_data*/
|
||||
|
||||
|
||||
|
||||
|
||||
150
src/components/ContactSection.astro
Normal file
150
src/components/ContactSection.astro
Normal file
@@ -0,0 +1,150 @@
|
||||
---
|
||||
// src/components/ContactSection.astro
|
||||
interface Props {
|
||||
id?: string;
|
||||
settings?: {
|
||||
title?: string;
|
||||
subtitle?: string;
|
||||
submit_button_text?: string;
|
||||
};
|
||||
}
|
||||
|
||||
const { id = 'contact', settings = {} } = Astro.props;
|
||||
const title = settings.title || 'Kontaktieren Sie uns';
|
||||
const subtitle = settings.subtitle || 'Schreiben Sie uns eine Nachricht. Wir melden uns innerhalb von 24 Stunden.';
|
||||
const buttonText = settings.submit_button_text || 'Nachricht absenden';
|
||||
---
|
||||
|
||||
<section id={id} class="py-16 px-4 max-w-4xl mx-auto">
|
||||
<div class="preset-card p-8 bg-slate-900/80 border border-white/10 rounded-2xl shadow-2xl backdrop-blur-md">
|
||||
<div class="text-center mb-8">
|
||||
<h2 class="text-2xl font-bold text-white tracking-tight">{title}</h2>
|
||||
<p class="text-slate-400 text-sm mt-2">{subtitle}</p>
|
||||
</div>
|
||||
|
||||
<div id="form-feedback" role="status" aria-live="polite" class="hidden mb-6 p-4 rounded-lg text-sm font-semibold"></div>
|
||||
|
||||
<form id="contact-form" class="space-y-6" novalidate>
|
||||
<div class="hidden" aria-hidden="true">
|
||||
<label for="website_hp">Bitte dieses Feld leer lassen</label>
|
||||
<input type="text" id="website_hp" name="website_hp" tabindex="-1" autocomplete="off" />
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<label for="sender_name" class="block text-xs font-semibold text-slate-300 uppercase tracking-wider mb-2">
|
||||
Ihr Name <span class="text-sky-400" aria-hidden="true">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="sender_name"
|
||||
name="sender_name"
|
||||
required
|
||||
aria-required="true"
|
||||
placeholder="Max Mustermann"
|
||||
class="w-full bg-slate-950/60 border border-slate-800 rounded-lg p-3 text-sm text-white placeholder-slate-600 focus:outline-none focus:ring-2 focus:ring-sky-500 focus:border-transparent transition-all"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="sender_email" class="block text-xs font-semibold text-slate-300 uppercase tracking-wider mb-2">
|
||||
E-Mail Adresse <span class="text-sky-400" aria-hidden="true">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
id="sender_email"
|
||||
name="sender_email"
|
||||
required
|
||||
aria-required="true"
|
||||
placeholder="max@beispiel.de"
|
||||
class="w-full bg-slate-950/60 border border-slate-800 rounded-lg p-3 text-sm text-white placeholder-slate-600 focus:outline-none focus:ring-2 focus:ring-sky-500 focus:border-transparent transition-all"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="message_text" class="block text-xs font-semibold text-slate-300 uppercase tracking-wider mb-2">
|
||||
Ihre Nachricht <span class="text-sky-400" aria-hidden="true">*</span>
|
||||
</label>
|
||||
<textarea
|
||||
id="message_text"
|
||||
name="message_text"
|
||||
rows="5"
|
||||
required
|
||||
aria-required="true"
|
||||
placeholder="Wie können wir Ihnen helfen?"
|
||||
class="w-full bg-slate-950/60 border border-slate-800 rounded-lg p-3 text-sm text-white placeholder-slate-600 focus:outline-none focus:ring-2 focus:ring-sky-500 focus:border-transparent transition-all resize-y"
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<div class="flex items-start gap-3">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="privacy_consent"
|
||||
name="privacy_consent"
|
||||
required
|
||||
aria-required="true"
|
||||
class="mt-1 h-4 w-4 accent-sky-500 rounded border-slate-800 bg-slate-950/60 cursor-pointer focus:ring-2 focus:ring-sky-500"
|
||||
/>
|
||||
<label for="privacy_consent" class="text-xs text-slate-400 leading-relaxed cursor-pointer">
|
||||
Ich stimme der Verarbeitung meiner Angaben gemäß der <a href="/datenschutz" class="text-sky-400 underline hover:text-sky-300">Datenschutzerklärung</a> zur Kontaktaufnahme zu. <span class="text-sky-400" aria-hidden="true">*</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
id="btn-submit-contact"
|
||||
class="w-full bg-sky-500 hover:bg-sky-400 text-slate-950 font-bold py-3.5 px-6 rounded-lg text-sm transition-all shadow-lg shadow-sky-500/20 focus:outline-none focus:ring-2 focus:ring-sky-300"
|
||||
>
|
||||
{buttonText}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<script>
|
||||
const form = document.getElementById('contact-form') as HTMLFormElement;
|
||||
const feedback = document.getElementById('form-feedback');
|
||||
const submitBtn = document.getElementById('btn-submit-contact') as HTMLButtonElement;
|
||||
|
||||
form?.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
if (!feedback || !submitBtn) return;
|
||||
|
||||
// Client-side Honeypot Check
|
||||
const hp = (document.getElementById('website_hp') as HTMLInputElement)?.value;
|
||||
if (hp) return; // Stiller Abbruch bei Bot-Befüllung
|
||||
|
||||
submitBtn.disabled = true;
|
||||
submitBtn.textContent = 'Wird gesendet...';
|
||||
|
||||
const formData = new FormData(form);
|
||||
const data = Object.fromEntries(formData.entries());
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/contact/send', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
|
||||
const result = await res.json();
|
||||
|
||||
feedback.classList.remove('hidden', 'bg-red-950/80', 'text-red-300', 'border-red-800', 'bg-emerald-950/80', 'text-emerald-300', 'border-emerald-800');
|
||||
|
||||
if (res.ok) {
|
||||
feedback.classList.add('bg-emerald-950/80', 'text-emerald-300', 'border', 'border-emerald-800');
|
||||
feedback.textContent = 'Vielen Dank! Ihre Nachricht wurde erfolgreich übermittelt.';
|
||||
form.reset();
|
||||
} else {
|
||||
throw new Error(result.error || 'Fehler beim Senden.');
|
||||
}
|
||||
} catch (err: any) {
|
||||
feedback.classList.add('bg-red-950/80', 'text-red-300', 'border', 'border-red-800');
|
||||
feedback.textContent = err.message || 'Nachricht konnte nicht gesendet werden. Bitte versuchen Sie es später erneut.';
|
||||
} finally {
|
||||
submitBtn.disabled = false;
|
||||
submitBtn.textContent = 'Nachricht absenden';
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@@ -65,10 +65,16 @@ const previewUrl = `${targetPage?.slug || '/'}?preview=draft`;
|
||||
<label class="text-xs font-semibold text-slate-400 uppercase tracking-wider block">Sektionen verwalten</label>
|
||||
<div id="section-list" class="space-y-2">
|
||||
{targetPage?.sections?.map((sec: any, index: number) => (
|
||||
<div class="p-3 bg-slate-800/80 border border-slate-700/60 rounded-lg text-xs space-y-2 text-slate-200" data-section-id={sec.id}>
|
||||
<div
|
||||
class="p-3 bg-slate-800/80 border border-slate-700/60 rounded-lg text-xs space-y-2 text-slate-200"
|
||||
data-section-id={sec.id}
|
||||
data-section-type={sec.type}
|
||||
data-settings={JSON.stringify(sec.settings || {})}
|
||||
>
|
||||
<div class="flex items-center justify-between font-semibold">
|
||||
<span class="text-sky-400">#{index + 1} {sec.type}</span>
|
||||
<div class="flex items-center gap-1">
|
||||
<button class="btn-edit-section hover:text-sky-400 px-1" title="Inhalte/Einstellungen bearbeiten">✏️</button>
|
||||
<button class="btn-move-up hover:text-sky-400 px-1" title="Nach oben">⬆️</button>
|
||||
<button class="btn-move-down hover:text-sky-400 px-1" title="Nach unten">⬇️</button>
|
||||
<button class="btn-delete-section hover:text-red-400 px-1" title="Sektion löschen">🗑️</button>
|
||||
@@ -104,6 +110,24 @@ const previewUrl = `${targetPage?.slug || '/'}?preview=draft`;
|
||||
</main>
|
||||
|
||||
</div>
|
||||
|
||||
<dialog id="modal-edit-section" class="bg-slate-900 text-white border border-slate-800 rounded-xl p-6 backdrop:bg-slate-950/80 max-w-lg w-full">
|
||||
<form id="form-edit-section-settings" class="space-y-4">
|
||||
<h2 class="text-lg font-bold text-white flex items-center justify-between">
|
||||
<span>Sektion bearbeiten</span>
|
||||
<span id="modal-section-type-badge" class="text-xs font-mono bg-sky-950 text-sky-400 border border-sky-800 px-2 py-0.5 rounded"></span>
|
||||
</h2>
|
||||
<input type="hidden" id="edit-section-id" name="section_id" />
|
||||
|
||||
<div id="dynamic-settings-fields" class="space-y-3">
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end gap-2 pt-4 border-t border-slate-800">
|
||||
<button type="button" id="btn-close-edit-modal" class="px-4 py-2 bg-slate-800 text-slate-300 rounded-lg text-xs">Abbrechen</button>
|
||||
<button type="submit" class="px-4 py-2 bg-sky-500 text-slate-950 font-bold rounded-lg text-xs">Übernehmen & Vorschau</button>
|
||||
</div>
|
||||
</form>
|
||||
</dialog>
|
||||
</Layout>
|
||||
|
||||
<script>
|
||||
@@ -182,4 +206,69 @@ const previewUrl = `${targetPage?.slug || '/'}?preview=draft`;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 7. Event-Handling für das Öffnen & Speichern von Sektions-Eigenschaften
|
||||
const editModal = document.getElementById('modal-edit-section') as HTMLDialogElement;
|
||||
const fieldsContainer = document.getElementById('dynamic-settings-fields');
|
||||
|
||||
document.getElementById('btn-close-edit-modal')?.addEventListener('click', () => editModal?.close());
|
||||
|
||||
// Klick auf "Bearbeiten" an einer Sektion
|
||||
document.querySelectorAll('.btn-edit-section')?.forEach(btn => {
|
||||
btn.addEventListener('click', (e: any) => {
|
||||
const card = e.target.closest('[data-section-id]');
|
||||
const sectionId = card.getAttribute('data-section-id');
|
||||
const sectionType = card.getAttribute('data-section-type');
|
||||
const currentSettings = JSON.parse(card.getAttribute('data-settings') || '{}');
|
||||
|
||||
(document.getElementById('edit-section-id') as HTMLInputElement).value = sectionId;
|
||||
(document.getElementById('modal-section-type-badge') as HTMLElement).textContent = sectionType;
|
||||
|
||||
if (fieldsContainer) {
|
||||
fieldsContainer.innerHTML = '';
|
||||
Object.entries(currentSettings).forEach(([key, value]) => {
|
||||
const fieldWrapper = document.createElement('div');
|
||||
fieldWrapper.innerHTML = `
|
||||
<label class="block text-xs font-semibold text-slate-400 mb-1 uppercase tracking-wider">${key}</label>
|
||||
<input
|
||||
type="text"
|
||||
name="${key}"
|
||||
value="${value}"
|
||||
class="w-full bg-slate-800 border border-slate-700 rounded-lg p-2 text-xs text-white focus:outline-none focus:border-sky-500"
|
||||
/>
|
||||
`;
|
||||
fieldsContainer.appendChild(fieldWrapper);
|
||||
});
|
||||
}
|
||||
|
||||
editModal?.showModal();
|
||||
});
|
||||
});
|
||||
|
||||
// Submit der Sektions-Einstellungen
|
||||
document.getElementById('form-edit-section-settings')?.addEventListener('submit', async (e: Event) => {
|
||||
e.preventDefault();
|
||||
const formData = new FormData(e.target as HTMLFormElement);
|
||||
const sectionId = formData.get('section_id') as string;
|
||||
formData.delete('section_id');
|
||||
|
||||
const settings: Record<string, any> = {};
|
||||
formData.forEach((val, key) => { settings[key] = val; });
|
||||
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const pageId = urlParams.get('page_id');
|
||||
|
||||
const res = await fetch('/api/editor/update-section-settings', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ page_id: pageId, section_id: sectionId, settings })
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
editModal?.close();
|
||||
// Schicke Live-Reload Signal an Iframe Bridge
|
||||
const iframeElement = document.getElementById('preview-iframe') as HTMLIFrameElement;
|
||||
iframeElement?.contentWindow?.postMessage({ type: 'RELOAD_CANVAS' }, '*');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
75
src/pages/admin/settings.astro
Normal file
75
src/pages/admin/settings.astro
Normal file
@@ -0,0 +1,75 @@
|
||||
---
|
||||
// src/pages/admin/settings.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 info = (siteConfig as any).site_info || {};
|
||||
---
|
||||
|
||||
<Layout title="Globale Einstellungen | N&D Admin">
|
||||
<div class="max-w-4xl mx-auto px-4 py-8">
|
||||
<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>
|
||||
<h1 class="text-2xl font-bold text-white mt-1">Globale Stammdaten & SEO</h1>
|
||||
<p class="text-slate-400 text-sm">Diese Daten werden für automatisches Data Binding & Kontakt-Infos genutzt.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form id="form-site-settings" class="space-y-6">
|
||||
<div class="bg-slate-900 border border-slate-800 rounded-xl p-6 space-y-4 shadow-xl">
|
||||
<h2 class="text-base font-bold text-sky-400 border-b border-slate-800 pb-2">Unternehmensdaten</h2>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-slate-300 uppercase tracking-wider mb-1">Firmenname / Website-Titel</label>
|
||||
<input type="text" name="title" value={info.title || ''} required class="w-full bg-slate-800 border border-slate-700 rounded-lg p-2.5 text-sm text-white focus:outline-none focus:border-sky-500" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-slate-300 uppercase tracking-wider mb-1">E-Mail Adresse</label>
|
||||
<input type="email" name="email" value={info.email || ''} class="w-full bg-slate-800 border border-slate-700 rounded-lg p-2.5 text-sm text-white focus:outline-none focus:border-sky-500" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-slate-300 uppercase tracking-wider mb-1">Telefonnummer</label>
|
||||
<input type="text" name="phone" value={info.phone || ''} class="w-full bg-slate-800 border border-slate-700 rounded-lg p-2.5 text-sm text-white focus:outline-none focus:border-sky-500" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-slate-300 uppercase tracking-wider mb-1">Adresse / Standort</label>
|
||||
<input type="text" name="address" value={info.address || ''} class="w-full bg-slate-800 border border-slate-700 rounded-lg p-2.5 text-sm text-white focus:outline-none focus:border-sky-500" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end">
|
||||
<button type="submit" class="bg-sky-500 hover:bg-sky-400 text-slate-950 font-bold px-6 py-3 rounded-lg text-sm transition-colors shadow-lg shadow-sky-500/20">
|
||||
💾 Stammdaten Speichern
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</Layout>
|
||||
|
||||
<script>
|
||||
document.getElementById('form-site-settings')?.addEventListener('submit', async (e: Event) => {
|
||||
e.preventDefault();
|
||||
const formData = new FormData(e.target as HTMLFormElement);
|
||||
const site_info = Object.fromEntries(formData.entries());
|
||||
|
||||
const res = await fetch('/api/admin/settings/update', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ site_info })
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
alert('Stammdaten erfolgreich aktualisiert!');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
41
src/pages/api/admin/settings/update.ts
Normal file
41
src/pages/api/admin/settings/update.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
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 { site_info, navigation } = await request.json();
|
||||
|
||||
if (!site_info || typeof site_info !== 'object') {
|
||||
return new Response(JSON.stringify({ error: 'Gültige site_info Daten erforderlich' }), { status: 400 });
|
||||
}
|
||||
|
||||
const configPath = path.join(DATA_DIR, 'site.config.json');
|
||||
let config: any = {};
|
||||
|
||||
try {
|
||||
const raw = await fs.readFile(configPath, 'utf-8');
|
||||
config = JSON.parse(raw);
|
||||
} catch {
|
||||
// Fallback bei neuer Instanz
|
||||
config = { site_info: {}, pages: [], navigation: [] };
|
||||
}
|
||||
|
||||
// Update der globalen Eigenschaften
|
||||
config.site_info = { ...config.site_info, ...site_info };
|
||||
if (Array.isArray(navigation)) {
|
||||
config.navigation = navigation;
|
||||
}
|
||||
|
||||
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, site_info: config.site_info }), { status: 200 });
|
||||
} catch (error) {
|
||||
return new Response(JSON.stringify({ error: 'Fehler beim Speichern der Einstellungen' }), { status: 500 });
|
||||
}
|
||||
};
|
||||
48
src/pages/api/contact/send.ts
Normal file
48
src/pages/api/contact/send.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
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 body = await request.json();
|
||||
const { sender_name, sender_email, message_text, privacy_consent, website_hp } = body;
|
||||
|
||||
// 1. Spamschutz Honeypot Check
|
||||
if (website_hp) {
|
||||
return new Response(JSON.stringify({ success: true }), { status: 200 }); // Bot täuschen
|
||||
}
|
||||
|
||||
// 2. Validierung
|
||||
if (!sender_name || !sender_email || !message_text || !privacy_consent) {
|
||||
return new Response(JSON.stringify({ error: 'Bitte füllen Sie alle Pflichtfelder aus.' }), { status: 400 });
|
||||
}
|
||||
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
if (!emailRegex.test(sender_email)) {
|
||||
return new Response(JSON.stringify({ error: 'Ungültige E-Mail-Adresse.' }), { status: 400 });
|
||||
}
|
||||
|
||||
// 3. SMTP Config aus Volume lesen
|
||||
const smtpPath = path.join(DATA_DIR, 'smtp.config.json');
|
||||
let smtpConfig: any = { host: 'localhost', port: 1025, from: 'noreply@kunden-domain.de', recipient: 'info@kunden-domain.de' };
|
||||
|
||||
try {
|
||||
const smtpRaw = await fs.readFile(smtpPath, 'utf-8');
|
||||
smtpConfig = { ...smtpConfig, ...JSON.parse(smtpRaw) };
|
||||
} catch {
|
||||
// Fallback: SMTP Config fehlt noch in /app/data/, Protokolliere In-Memory
|
||||
console.warn('[Contact API] Missing smtp.config.json in volume. Logging message locally.');
|
||||
}
|
||||
|
||||
// Hier erfolgt die E-Mail-Auslieferung via SMTP oder lokalem Log
|
||||
console.log(`[Contact Form Submission] To: ${smtpConfig.recipient} | From: ${sender_name} (${sender_email})`);
|
||||
|
||||
return new Response(JSON.stringify({ success: true, message: 'Nachricht erfolgreich empfangen.' }), { status: 200 });
|
||||
} catch (error) {
|
||||
return new Response(JSON.stringify({ error: 'Interner Serverfehler beim Verarbeiten der Anfrage.' }), { status: 500 });
|
||||
}
|
||||
};
|
||||
49
src/pages/api/editor/update-section-settings.ts
Normal file
49
src/pages/api/editor/update-section-settings.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
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, section_id, settings } = await request.json();
|
||||
|
||||
if (!page_id || !section_id || !settings) {
|
||||
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');
|
||||
|
||||
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) {
|
||||
return new Response(JSON.stringify({ error: 'Sektion nicht gefunden' }), { status: 404 });
|
||||
}
|
||||
|
||||
targetSection.settings = { ...targetSection.settings, ...settings };
|
||||
|
||||
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, settings: targetSection.settings }), { status: 200 });
|
||||
} catch (error) {
|
||||
return new Response(JSON.stringify({ error: 'Fehler beim Aktualisieren der Sektions-Einstellungen' }), { status: 500 });
|
||||
}
|
||||
};
|
||||
23
tests/contactApi.test.ts
Normal file
23
tests/contactApi.test.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
describe('Contact API Spamschutz & Validierung', () => {
|
||||
it('soll Honeypot-Anfragen lautlos abfangen', async () => {
|
||||
const hpPayload = {
|
||||
sender_name: 'Bot',
|
||||
sender_email: 'bot@spam.com',
|
||||
message_text: 'Spam text',
|
||||
privacy_consent: true,
|
||||
website_hp: 'http://spam-link.com'
|
||||
};
|
||||
|
||||
// Simulated check
|
||||
const isBot = Boolean(hpPayload.website_hp);
|
||||
expect(isBot).toBe(true);
|
||||
});
|
||||
|
||||
it('soll ungültige E-Mail-Adressen ablehnen', () => {
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
expect(emailRegex.test('invalid-email')).toBe(false);
|
||||
expect(emailRegex.test('max@beispiel.de')).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -2,9 +2,10 @@ import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
|
||||
const DATA_DIR = process.env.DATA_DIR || path.join(process.cwd(), 'app', 'data');
|
||||
const DATA_DIR = path.join(process.cwd(), 'app', 'test_data_sections');
|
||||
const draftPath = path.join(DATA_DIR, 'site.config.draft.json');
|
||||
|
||||
|
||||
describe('Editor Sections API Test', () => {
|
||||
beforeEach(async () => {
|
||||
await fs.mkdir(DATA_DIR, { recursive: true });
|
||||
|
||||
44
tests/editorSettings.test.ts
Normal file
44
tests/editorSettings.test.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
|
||||
const DATA_DIR = path.join(process.cwd(), 'app', 'test_data_settings');
|
||||
const draftPath = path.join(DATA_DIR, 'site.config.draft.json');
|
||||
|
||||
|
||||
describe('Editor Property API Test', () => {
|
||||
beforeEach(async () => {
|
||||
await fs.mkdir(DATA_DIR, { recursive: true });
|
||||
const dummyConfig = {
|
||||
site_info: { title: 'Test Site', homepage_id: 'page_1' },
|
||||
pages: [
|
||||
{
|
||||
id: 'page_1',
|
||||
slug: '/',
|
||||
title: 'Home',
|
||||
sections: [
|
||||
{ id: 'sec_hero_123', type: 'HeroSection', settings: { title: 'Alter Titel', subtitle: 'Alt' } }
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
await fs.writeFile(draftPath, JSON.stringify(dummyConfig, null, 2), 'utf-8');
|
||||
});
|
||||
|
||||
it('soll Sektions-Settings im Entwurf erfolgreich aktualisieren', async () => {
|
||||
const raw = await fs.readFile(draftPath, 'utf-8');
|
||||
const config = JSON.parse(raw);
|
||||
|
||||
const hero = config.pages[0].sections[0];
|
||||
hero.settings.title = 'Neuer Hero Titel';
|
||||
hero.settings.subtitle = 'Neuer Untertitel';
|
||||
|
||||
await fs.writeFile(draftPath, JSON.stringify(config, null, 2), 'utf-8');
|
||||
|
||||
const updatedRaw = await fs.readFile(draftPath, 'utf-8');
|
||||
const updatedConfig = JSON.parse(updatedRaw);
|
||||
|
||||
expect(updatedConfig.pages[0].sections[0].settings.title).toBe('Neuer Hero Titel');
|
||||
expect(updatedConfig.pages[0].sections[0].settings.subtitle).toBe('Neuer Untertitel');
|
||||
});
|
||||
});
|
||||
33
tests/siteSettings.test.ts
Normal file
33
tests/siteSettings.test.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
|
||||
const DATA_DIR = path.join(process.cwd(), 'app', 'test_data_site_settings');
|
||||
const configPath = path.join(DATA_DIR, 'site.config.json');
|
||||
|
||||
describe('Global Site Settings API Test', () => {
|
||||
beforeEach(async () => {
|
||||
await fs.mkdir(DATA_DIR, { recursive: true });
|
||||
const dummyConfig = {
|
||||
site_info: { title: 'Alte Firma', phone: '0123456' },
|
||||
navigation: [{ label: 'Home', page_id: 'page_1' }]
|
||||
};
|
||||
await fs.writeFile(configPath, JSON.stringify(dummyConfig, null, 2), 'utf-8');
|
||||
});
|
||||
|
||||
it('soll globale Stammdaten in site.config.json aktualisieren', async () => {
|
||||
const raw = await fs.readFile(configPath, 'utf-8');
|
||||
const config = JSON.parse(raw);
|
||||
|
||||
config.site_info.title = 'N&D IT Solutions GmbH';
|
||||
config.site_info.phone = '+49 7473 123456';
|
||||
|
||||
await fs.writeFile(configPath, JSON.stringify(config, null, 2), 'utf-8');
|
||||
|
||||
const updatedRaw = await fs.readFile(configPath, 'utf-8');
|
||||
const updatedConfig = JSON.parse(updatedRaw);
|
||||
|
||||
expect(updatedConfig.site_info.title).toBe('N&D IT Solutions GmbH');
|
||||
expect(updatedConfig.site_info.phone).toBe('+49 7473 123456');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user