feat: enable full section editing, background styling, animations and live file persistence

This commit is contained in:
Daniel S
2026-08-09 23:50:33 +02:00
parent 831a0d53f0
commit fc2017b210
9 changed files with 41 additions and 31 deletions

View File

@@ -1,4 +1,5 @@
{ {
"updated_at": "2026-08-09T21:48:17.969Z",
"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, "is_demo_mode": false,
@@ -131,23 +132,7 @@
"slug": "/", "slug": "/",
"title": "Startseite (Hauptseite)", "title": "Startseite (Hauptseite)",
"is_published": true, "is_published": true,
"sections": [ "sections": []
{
"id": "hero",
"type": "HeroSection",
"settings": {}
},
{
"id": "bento",
"type": "BentoGrid",
"settings": {}
},
{
"id": "contact",
"type": "ContactSection",
"settings": {}
}
]
}, },
{ {
"id": "page_templates", "id": "page_templates",

View File

@@ -5,18 +5,22 @@ interface Props {
config: SiteConfig['bento']; config: SiteConfig['bento'];
} }
const { config } = Astro.props; const props = Astro.props;
const config = props.config || props.settings || props;
const sectionTitle = config.sectionTitle || config.title || 'Unsere Leistungen';
const sectionSubtitle = config.sectionSubtitle || config.subtitle || '';
const cards = config.cards || config.items || [];
--- ---
<section id="services" class={`py-20 px-4 sm:px-6 lg:px-8 max-w-7xl mx-auto relative ${config.animation ? `anim-${config.animation}` : ''}`} style={(config as any).bg_color ? `background-color: ${(config as any).bg_color};` : ''}> <section id="services" class={`py-20 px-4 sm:px-6 lg:px-8 max-w-7xl mx-auto relative ${config.animation ? `anim-${config.animation}` : ''}`} style={(config as any).bg_color ? `background-color: ${(config as any).bg_color};` : ''}>
<div class="text-center max-w-3xl mx-auto mb-16 space-y-3"> <div class="text-center max-w-3xl mx-auto mb-16 space-y-3">
<h2 class="text-3xl font-extrabold text-white tracking-tight uppercase">{config.sectionTitle}</h2> <h2 class="text-3xl font-extrabold text-white tracking-tight uppercase">{sectionTitle}</h2>
<p class="text-slate-400 text-xs sm:text-sm">{config.sectionSubtitle}</p> <p class="text-slate-400 text-xs sm:text-sm">{sectionSubtitle}</p>
</div> </div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-6"> <div class="grid grid-cols-1 md:grid-cols-3 gap-6">
{config.cards.map((card) => ( {cards.map((card: any) => (
<div class={`relative glass-panel glass-panel-hover rounded-2xl p-8 flex flex-col justify-between overflow-hidden group ${card.colSpan === 2 ? 'md:col-span-2' : 'md:col-span-1'} ${card.animation ? `anim-${card.animation}` : ''}`}> <div class={`relative glass-panel glass-panel-hover rounded-2xl p-8 flex flex-col justify-between overflow-hidden group ${card.colSpan === 2 ? 'md:col-span-2' : 'md:col-span-1'} ${card.animation ? `anim-${card.animation}` : ''}`}>
<div class="space-y-4 relative z-10"> <div class="space-y-4 relative z-10">
{card.badge && ( {card.badge && (

View File

@@ -6,15 +6,19 @@ interface Props {
smtpConfig: SmtpConfig; smtpConfig: SmtpConfig;
} }
const { contactConfig, smtpConfig } = Astro.props; const props = Astro.props;
const contactConfig = props.contactConfig || props.settings || props;
const smtpConfig = props.smtpConfig || { isConfigured: false };
const title = contactConfig.title || 'Kontaktieren Sie uns';
const subtitle = contactConfig.subtitle || 'Wir freuen uns auf Ihre Nachricht.';
--- ---
<section id="contact" class="py-20 px-4 sm:px-6 lg:px-8 max-w-4xl mx-auto relative" style={(contactConfig as any).bg_color ? `background-color: ${(contactConfig as any).bg_color};` : ''}> <section id="contact" class="py-20 px-4 sm:px-6 lg:px-8 max-w-4xl mx-auto relative" style={(contactConfig as any).bg_color ? `background-color: ${(contactConfig as any).bg_color};` : ''}>
<div class="glass-panel rounded-3xl p-8 sm:p-12 relative overflow-hidden"> <div class="glass-panel rounded-3xl p-8 sm:p-12 relative overflow-hidden">
<div class="text-center space-y-3 mb-10"> <div class="text-center space-y-3 mb-10">
<h2 class="text-3xl font-extrabold text-white tracking-tight uppercase">{contactConfig.title}</h2> <h2 class="text-3xl font-extrabold text-white tracking-tight uppercase">{title}</h2>
<p class="text-slate-400 text-xs sm:text-sm">{contactConfig.subtitle}</p> <p class="text-slate-400 text-xs sm:text-sm">{subtitle}</p>
</div> </div>
{!smtpConfig.isConfigured && ( {!smtpConfig.isConfigured && (

View File

@@ -7,11 +7,13 @@ interface Props {
contextData?: Record<string, any>; contextData?: Record<string, any>;
} }
const { config, contextData = {} } = Astro.props; const props = Astro.props;
const config = props.config || props.settings || props;
const contextData = props.contextData || {};
const heroTitle = resolveDynamicBinding(config.title, contextData); const heroTitle = resolveDynamicBinding(config.title || config.heroTitle || 'Willkommen', contextData);
const heroSubtitle = resolveDynamicBinding(config.subtitle, contextData); const heroSubtitle = resolveDynamicBinding(config.subtitle || config.heroSubtitle || '', contextData);
const heroBadge = resolveDynamicBinding(config.badge, contextData); const heroBadge = resolveDynamicBinding(config.badge || config.heroBadge || '', contextData);
const elements = config.elements || []; const elements = config.elements || [];
const badgeElements = elements.filter((el: UIComponent) => el.slotId === 'hero-badge-slot'); const badgeElements = elements.filter((el: UIComponent) => el.slotId === 'hero-badge-slot');

View File

@@ -285,9 +285,10 @@ const previewUrl = `${targetPage?.slug || '/'}?preview=draft`;
if (fieldsContainer) { if (fieldsContainer) {
fieldsContainer.innerHTML = ''; fieldsContainer.innerHTML = '';
const allKeys = new Set(Object.keys(currentSettings)); const allKeys = new Set(Object.keys(currentSettings));
if (!allKeys.has('bg_color')) { if (!allKeys.has('title')) allKeys.add('title');
allKeys.add('bg_color'); if (!allKeys.has('subtitle')) allKeys.add('subtitle');
} if (!allKeys.has('bg_color')) allKeys.add('bg_color');
if (!allKeys.has('animation')) allKeys.add('animation');
allKeys.forEach((key) => { allKeys.forEach((key) => {
const value = currentSettings[key] || ''; const value = currentSettings[key] || '';
@@ -312,6 +313,16 @@ const previewUrl = `${targetPage?.slug || '/'}?preview=draft`;
/> />
</div> </div>
`; `;
} else if (key === 'animation') {
fieldWrapper.innerHTML = `
<label class="block text-xs font-semibold text-slate-400 mb-1 uppercase tracking-wider">✨ Animation / Effekt</label>
<select name="animation" 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">
<option value="" ${!value ? 'selected' : ''}>Keine Animation</option>
<option value="fade-in" ${value === 'fade-in' ? 'selected' : ''}>Fade In (Einblenden)</option>
<option value="slide-up" ${value === 'slide-up' ? 'selected' : ''}>Slide Up (Von unten)</option>
<option value="zoom-in" ${value === 'zoom-in' ? 'selected' : ''}>Zoom In (Heranzoomen)</option>
</select>
`;
} else { } else {
fieldWrapper.innerHTML = ` fieldWrapper.innerHTML = `
<label class="block text-xs font-semibold text-slate-400 mb-1 uppercase tracking-wider">${key}</label> <label class="block text-xs font-semibold text-slate-400 mb-1 uppercase tracking-wider">${key}</label>

View File

@@ -73,6 +73,7 @@ export const POST: APIRoute = async ({ request }) => {
targetPage.sections.push(newSection); targetPage.sections.push(newSection);
await fs.mkdir(DATA_DIR, { recursive: true }); await fs.mkdir(DATA_DIR, { recursive: true });
await fs.writeFile(livePath, JSON.stringify(config, null, 2), 'utf-8');
await fs.writeFile(draftPath, JSON.stringify(config, null, 2), 'utf-8'); await fs.writeFile(draftPath, JSON.stringify(config, null, 2), 'utf-8');
return new Response(JSON.stringify({ success: true, section: newSection }), { status: 201 }); return new Response(JSON.stringify({ success: true, section: newSection }), { status: 201 });

View File

@@ -35,6 +35,7 @@ export const POST: APIRoute = async ({ request }) => {
targetPage.sections = targetPage.sections.filter((sec: any) => sec.id !== section_id); targetPage.sections = targetPage.sections.filter((sec: any) => sec.id !== section_id);
await fs.mkdir(DATA_DIR, { recursive: true }); await fs.mkdir(DATA_DIR, { recursive: true });
await fs.writeFile(livePath, JSON.stringify(config, null, 2), 'utf-8');
await fs.writeFile(draftPath, JSON.stringify(config, null, 2), 'utf-8'); await fs.writeFile(draftPath, JSON.stringify(config, null, 2), 'utf-8');
return new Response(JSON.stringify({ success: true, message: 'Sektion gelöscht' }), { status: 200 }); return new Response(JSON.stringify({ success: true, message: 'Sektion gelöscht' }), { status: 200 });

View File

@@ -40,6 +40,7 @@ export const POST: APIRoute = async ({ request }) => {
targetSection.settings = { ...targetSection.settings, ...settings }; targetSection.settings = { ...targetSection.settings, ...settings };
await fs.mkdir(DATA_DIR, { recursive: true }); await fs.mkdir(DATA_DIR, { recursive: true });
await fs.writeFile(livePath, JSON.stringify(config, null, 2), 'utf-8');
await fs.writeFile(draftPath, JSON.stringify(config, null, 2), 'utf-8'); await fs.writeFile(draftPath, JSON.stringify(config, null, 2), 'utf-8');
return new Response(JSON.stringify({ success: true, settings: targetSection.settings }), { status: 200 }); return new Response(JSON.stringify({ success: true, settings: targetSection.settings }), { status: 200 });

View File

@@ -37,6 +37,7 @@ export const POST: APIRoute = async ({ request }) => {
// 3. Speichere ausschließlich in site.config.draft.json // 3. Speichere ausschließlich in site.config.draft.json
await fs.mkdir(DATA_DIR, { recursive: true }); await fs.mkdir(DATA_DIR, { recursive: true });
await fs.writeFile(livePath, JSON.stringify(config, null, 2), 'utf-8');
await fs.writeFile(draftPath, JSON.stringify(config, null, 2), 'utf-8'); await fs.writeFile(draftPath, JSON.stringify(config, null, 2), 'utf-8');
return new Response( return new Response(