feat: add section content editing, animation selection and HTML5 drag & drop in live editor
All checks were successful
Production Build & Deploy / build-and-deploy (push) Successful in 1m0s

This commit is contained in:
Daniel S
2026-08-09 12:45:36 +02:00
parent b159b061ae
commit 3453746fc0
5 changed files with 184 additions and 11 deletions

View File

@@ -8,7 +8,7 @@ interface Props {
const { config } = Astro.props; const { config } = Astro.props;
--- ---
<section id="services" class="py-20 px-4 sm:px-6 lg:px-8 max-w-7xl mx-auto relative"> <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}` : ''}`}>
<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">{config.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">{config.sectionSubtitle}</p>
@@ -16,7 +16,7 @@ const { config } = Astro.props;
<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) => ( {config.cards.map((card) => (
<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'}`}> <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 && (
<span class="inline-block text-[10px] font-mono uppercase tracking-wider text-sky-400 bg-sky-400/10 px-2.5 py-1 rounded border border-sky-400/20"> <span class="inline-block text-[10px] font-mono uppercase tracking-wider text-sky-400 bg-sky-400/10 px-2.5 py-1 rounded border border-sky-400/20">

View File

@@ -13,7 +13,7 @@ const actionElements = elements.filter((el: UIComponent) => el.slotId === 'hero-
const customElements = elements.filter((el: UIComponent) => !el.slotId || el.slotId === 'hero-custom-slot'); const customElements = elements.filter((el: UIComponent) => !el.slotId || el.slotId === 'hero-custom-slot');
--- ---
<section class="relative py-24 px-4 sm:px-6 lg:px-8 max-w-7xl mx-auto overflow-hidden"> <section class={`relative py-24 px-4 sm:px-6 lg:px-8 max-w-7xl mx-auto overflow-hidden ${config.animation ? `anim-${config.animation}` : ''}`}>
<span class="aurora-glow aurora-glow-sky w-[500px] h-[500px] -top-32 -left-32"></span> <span class="aurora-glow aurora-glow-sky w-[500px] h-[500px] -top-32 -left-32"></span>
<span class="aurora-glow aurora-glow-indigo w-[400px] h-[400px] bottom-0 right-0"></span> <span class="aurora-glow aurora-glow-indigo w-[400px] h-[400px] bottom-0 right-0"></span>
@@ -27,7 +27,7 @@ const customElements = elements.filter((el: UIComponent) => !el.slotId || el.slo
</div> </div>
)} )}
{badgeElements.map(el => ( {badgeElements.map(el => (
<span class="inline-flex items-center gap-2 px-3.5 py-1.5 rounded-full border border-sky-400/30 bg-sky-400/20 text-sky-300 text-xs font-mono"> <span class={`inline-flex items-center gap-2 px-3.5 py-1.5 rounded-full border border-sky-400/30 bg-sky-400/20 text-sky-300 text-xs font-mono ${el.animation0 ? `anim-${el.animation0}` : ''}`}>
{el.content} {el.content}
</span> </span>
))} ))}
@@ -54,7 +54,7 @@ const customElements = elements.filter((el: UIComponent) => !el.slotId || el.slo
</a> </a>
)} )}
{actionElements.map(el => ( {actionElements.map(el => (
<a href={el.link || '#'} class={`px-8 py-3.5 rounded-xl font-bold text-xs transition-all active:scale-95 ${el.style === 'glass' ? 'glass-panel text-white' : 'bg-sky-400 text-slate-950 hover:bg-white'}`}> <a href={el.link || '#'} class={`px-8 py-3.5 rounded-xl font-bold text-xs transition-all active:scale-95 ${el.style === 'glass' ? 'glass-panel text-white' : 'bg-sky-400 text-slate-950 hover:bg-white'} ${el.animation0 ? `anim-${el.animation0}` : ''}`}>
{el.content} {el.content}
</a> </a>
))} ))}

View File

@@ -83,16 +83,16 @@ const { siteConfig, themeConfig } = loadWaasConfigs(Astro.request.headers.get('u
</div> </div>
</div> </div>
<!-- Sektionen Re-Order --> <!-- Sektionen Re-Order mit HTML5 Drag & Drop -->
<div class="glass-panel rounded-xl p-4 space-y-3"> <div class="glass-panel rounded-xl p-4 space-y-3">
<h3 class="text-xs font-bold text-sky-400 uppercase tracking-wider flex items-center justify-between"> <h3 class="text-xs font-bold text-sky-400 uppercase tracking-wider flex items-center justify-between">
<span>Sektionen Reihenfolge (Layout)</span> <span>Sektionen Reihenfolge (Layout)</span>
<span class="text-[10px] text-slate-500 font-mono">Drag & Drop</span> <span class="text-[10px] text-slate-400 font-mono">Drag &amp; Drop</span>
</h3> </h3>
<div id="sections-list" class="space-y-2"> <div id="sections-list" class="space-y-2">
{siteConfig.sectionsOrder.map((sec, idx) => ( {siteConfig.sectionsOrder.map((sec, idx) => (
<div data-section={sec} draggable="true" class="section-item flex items-center justify-between p-3 rounded-lg bg-slate-900/60 border border-white/5 hover:border-white/20 transition-all group cursor-grab"> <div data-section={sec} draggable="true" class="section-item flex items-center justify-between p-3 rounded-lg bg-slate-900/60 border border-white/5 hover:border-sky-400/40 transition-all group cursor-grab active:cursor-grabbing">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<span class="text-slate-500 font-mono text-xs">#{idx + 1}</span> <span class="text-slate-500 font-mono text-xs">#{idx + 1}</span>
<span class="text-xs font-semibold capitalize text-white">{sec}</span> <span class="text-xs font-semibold capitalize text-white">{sec}</span>
@@ -107,6 +107,63 @@ const { siteConfig, themeConfig } = loadWaasConfigs(Astro.request.headers.get('u
</div> </div>
</div> </div>
<!-- Sektions-Inhalte & Animationen Bearbeiten -->
<div class="glass-panel rounded-xl p-4 space-y-4">
<h3 class="text-xs font-bold text-sky-400 uppercase tracking-wider">
Hero Sektion: Inhalt &amp; Animation
</h3>
<div class="space-y-3 text-xs">
<div>
<label class="block text-[10px] font-bold text-slate-400 uppercase mb-1">Hero Title</label>
<input type="text" id="hero-title-input" value={siteConfig.hero.title} class="w-full glass-panel bg-slate-900 rounded-lg p-2 text-xs text-white focus:outline-none focus:border-sky-400" />
</div>
<div>
<label class="block text-[10px] font-bold text-slate-400 uppercase mb-1">Hero Subtitle</label>
<textarea id="hero-subtitle-input" class="w-full h-16 glass-panel bg-slate-900 rounded-lg p-2 text-xs text-white focus:outline-none focus:border-sky-400">{siteConfig.hero.subtitle}</textarea>
</div>
<div>
<label class="block text-[10px] font-bold text-slate-400 uppercase mb-1">Hero Badge Text</label>
<input type="text" id="hero-badge-input" value={siteConfig.hero.badge} class="w-full glass-panel bg-slate-900 rounded-lg p-2 text-xs text-white focus:outline-none focus:border-sky-400" />
</div>
<div>
<label class="block text-[10px] font-bold text-slate-400 uppercase mb-1">Hero Sektion Animation</label>
<select id="hero-anim-select" class="w-full glass-panel bg-slate-900 rounded-lg p-2 text-xs text-white focus:outline-none focus:border-sky-400">
<option value="" selected={!siteConfig.hero.animation}>Keine Animation</option>
<option value="spring-fade" selected={siteConfig.hero.animation === 'spring-fade'}>Spring Fade (Federleicht)</option>
<option value="slide-up" selected={siteConfig.hero.animation === 'slide-up'}>Slide Up (Eingleiten)</option>
<option value="zoom-in" selected={siteConfig.hero.animation === 'zoom-in'}>Zoom In (Heranzoomen)</option>
<option value="pulse-glow" selected={siteConfig.hero.animation === 'pulse-glow'}>Pulse Glow (Leuchten)</option>
</select>
</div>
</div>
</div>
<!-- Bento Grid: Inhalt & Animation -->
<div class="glass-panel rounded-xl p-4 space-y-4">
<h3 class="text-xs font-bold text-sky-400 uppercase tracking-wider">
Bento Grid: Inhalt &amp; Animation
</h3>
<div class="space-y-3 text-xs">
<div>
<label class="block text-[10px] font-bold text-slate-400 uppercase mb-1">Sektions-Titel</label>
<input type="text" id="bento-title-input" value={siteConfig.bento.sectionTitle} class="w-full glass-panel bg-slate-900 rounded-lg p-2 text-xs text-white focus:outline-none focus:border-sky-400" />
</div>
<div>
<label class="block text-[10px] font-bold text-slate-400 uppercase mb-1">Sektions-Untertitel</label>
<input type="text" id="bento-subtitle-input" value={siteConfig.bento.sectionSubtitle} class="w-full glass-panel bg-slate-900 rounded-lg p-2 text-xs text-white focus:outline-none focus:border-sky-400" />
</div>
<div>
<label class="block text-[10px] font-bold text-slate-400 uppercase mb-1">Bento Grid Animation</label>
<select id="bento-anim-select" class="w-full glass-panel bg-slate-900 rounded-lg p-2 text-xs text-white focus:outline-none focus:border-sky-400">
<option value="" selected={!siteConfig.bento.animation}>Keine Animation</option>
<option value="slide-up" selected={siteConfig.bento.animation === 'slide-up'}>Slide Up</option>
<option value="spring-fade" selected={siteConfig.bento.animation === 'spring-fade'}>Spring Fade</option>
<option value="zoom-in" selected={siteConfig.bento.animation === 'zoom-in'}>Zoom In</option>
</select>
</div>
</div>
</div>
<!-- Dynamische UIKomponente Hinzufuegen per Sektion --> <!-- Dynamische UIKomponente Hinzufuegen per Sektion -->
<div class="glass-panel rounded-xl p-4 space-y-4"> <div class="glass-panel rounded-xl p-4 space-y-4">
<h3 class="text-xs font-bold text-sky-400 uppercase tracking-wider"> <h3 class="text-xs font-bold text-sky-400 uppercase tracking-wider">
@@ -150,6 +207,18 @@ const { siteConfig, themeConfig } = loadWaasConfigs(Astro.request.headers.get('u
</select> </select>
</div> </div>
<div>
<label class="block text-[10px] font-bold text-slate-400 uppercase mb-1">Element Animation</label>
<select id="new-el-anim" class="w-full glass-panel bg-slate-900 rounded-lg p-2 text-xs text-white focus:outline-none focus:border-sky-400">
<option value="">Keine Animation</option>
<option value="spring-fade">Spring Fade</option>
<option value="hover-bounce">Hover Bounce</option>
<option value="pulse-glow">Pulse Glow</option>
<option value="slide-up">Slide Up</option>
<option value="zoom-in">Zoom In</option>
</select>
</div>
<div> <div>
<label class="block text-[10px] font-bold text-slate-400 uppercase mb-1">Inhalt / Label</label> <label class="block text-[10px] font-bold text-slate-400 uppercase mb-1">Inhalt / Label</label>
<input type="text" id="new-el-content" placeholder="z.B. Jetzt Anfragen" class="w-full glass-panel bg-slate-900 rounded-lg p-2 text-xs text-white focus:outline-none focus:border-sky-400" /> <input type="text" id="new-el-content" placeholder="z.B. Jetzt Anfragen" class="w-full glass-panel bg-slate-900 rounded-lg p-2 text-xs text-white focus:outline-none focus:border-sky-400" />
@@ -180,7 +249,7 @@ const { siteConfig, themeConfig } = loadWaasConfigs(Astro.request.headers.get('u
<div id="elements-container" class="space-y-2 max-h-80 overflow-y-auto pr-1 custom-scroll"> <div id="elements-container" class="space-y-2 max-h-80 overflow-y-auto pr-1 custom-scroll">
{((siteConfig.hero.elements || [])).map((el, idx) => ( {((siteConfig.hero.elements || [])).map((el, idx) => (
<div data-id={el.id} class="p-3 rounded-lg bg-slate-900/70 border border-white/10 space-y-2 text-xs group"> <div data-id={el.id} draggable="true" class="element-item p-3 rounded-lg bg-slate-900/70 border border-white/10 space-y-2 text-xs group cursor-grab active:cursor-grabbing">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<div> <div>
<span class="font-bold text-sky-400 uppercase text-[10px] block">{el.type} (Slot: {el.slotId})</span> <span class="font-bold text-sky-400 uppercase text-[10px] block">{el.type} (Slot: {el.slotId})</span>
@@ -404,11 +473,48 @@ const { siteConfig, themeConfig } = loadWaasConfigs(Astro.request.headers.get('u
renderSectionsList(); renderSectionsList();
}; };
// --- Drag & Drop for Section Items ---
let draggedSecIndex = -1;
function initSectionDragAndDrop() {
const listContainer = document.getElementById('sections-list');
if (!listContainer) return;
const items = listContainer.querySelectorAll('.section-item');
items.forEach((item, index) => {
item.addEventListener('dragstart', (e) => {
draggedSecIndex = index;
e.dataTransfer.effectAllowed = 'move';
item.classList.add('opacity-40');
});
item.addEventListener('dragend', () => {
item.classList.remove('opacity-40');
draggedSecIndex = -1;
});
item.addEventListener('dragover', (e) => {
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
});
item.addEventListener('drop', (e) => {
e.preventDefault();
if (draggedSecIndex !== -1 && draggedSecIndex !== index) {
const arr = siteConfigState.sectionsOrder;
const movedItem = arr.splice(draggedSecIndex, 1)[0];
arr.splice(index, 0, movedItem);
renderSectionsList();
}
});
});
}
function renderSectionsList() { function renderSectionsList() {
const listContainer = document.getElementById('sections-list'); const listContainer = document.getElementById('sections-list');
if (!listContainer) return; if (!listContainer) return;
listContainer.innerHTML = siteConfigState.sectionsOrder.map((sec, idx) => ( listContainer.innerHTML = siteConfigState.sectionsOrder.map((sec, idx) => (
'<div data-section="' + sec + '" draggable="true" class="section-item flex items-center justify-between p-3 rounded-lg bg-slate-900/60 border border-white/5 hover:border-white/20 transition-all group cursor-grab">' + '<div data-section="' + sec + '" draggable="true" class="section-item flex items-center justify-between p-3 rounded-lg bg-slate-900/60 border border-white/5 hover:border-sky-400/40 transition-all group cursor-grab active:cursor-grabbing">' +
'<div class="flex items-center gap-2">' + '<div class="flex items-center gap-2">' +
'<span class="text-slate-500 font-mono text-xs">#' + (idx + 1) + '</span>' + '<span class="text-slate-500 font-mono text-xs">#' + (idx + 1) + '</span>' +
'<span class="text-xs font-semibold capitalize text-white">' + sec + '</span>' + '<span class="text-xs font-semibold capitalize text-white">' + sec + '</span>' +
@@ -420,8 +526,13 @@ const { siteConfig, themeConfig } = loadWaasConfigs(Astro.request.headers.get('u
'</div>' + '</div>' +
'</div>' '</div>'
)).join(''); )).join('');
initSectionDragAndDrop();
} }
// Initial call for drag and drop setup
initSectionDragAndDrop();
function updateSectionSelectors() { function updateSectionSelectors() {
const newElSec = document.getElementById('new-el-section'); const newElSec = document.getElementById('new-el-section');
const filterSec = document.getElementById('filter-section-select'); const filterSec = document.getElementById('filter-section-select');
@@ -443,6 +554,7 @@ const { siteConfig, themeConfig } = loadWaasConfigs(Astro.request.headers.get('u
const section = document.getElementById('new-el-section').value; const section = document.getElementById('new-el-section').value;
const slotId = document.getElementById('new-el-slot').value; const slotId = document.getElementById('new-el-slot').value;
const type = document.getElementById('new-el-type').value; const type = document.getElementById('new-el-type').value;
const anim = document.getElementById('new-el-anim').value;
const content = document.getElementById('new-el-content').value || 'Neues Element'; const content = document.getElementById('new-el-content').value || 'Neues Element';
const newComp = { const newComp = {
@@ -450,6 +562,7 @@ const { siteConfig, themeConfig } = loadWaasConfigs(Astro.request.headers.get('u
slotId: section + '-' + slotId, slotId: section + '-' + slotId,
type, type,
content, content,
animation0: anim || undefined,
style: 'primary' style: 'primary'
}; };
@@ -509,7 +622,7 @@ const { siteConfig, themeConfig } = loadWaasConfigs(Astro.request.headers.get('u
if (countEl) countEl.textContent = els.length; if (countEl) countEl.textContent = els.length;
container.innerHTML = els.map((el, idx) => ( container.innerHTML = els.map((el, idx) => (
'<div data-id="' + el.id + '" class="p-3 rounded-lg bg-slate-900/70 border border-white/10 space-y-2 text-xs group">' + '<div data-id="' + el.id + '" draggable="true" class="element-item p-3 rounded-lg bg-slate-900/70 border border-white/10 space-y-2 text-xs group cursor-grab active:cursor-grabbing">' +
'<div class="flex items-center justify-between">' + '<div class="flex items-center justify-between">' +
'<div>' + '<div>' +
'<span class="font-bold text-sky-400 uppercase text-[10px] block">' + el.type + ' (Slot: ' + el.slotId + ')</span>' + '<span class="font-bold text-sky-400 uppercase text-[10px] block">' + el.type + ' (Slot: ' + el.slotId + ')</span>' +
@@ -544,6 +657,28 @@ const { siteConfig, themeConfig } = loadWaasConfigs(Astro.request.headers.get('u
saveBtn.disabled = true; saveBtn.disabled = true;
saveBtn.textContent = 'Wird gespeichert...'; saveBtn.textContent = 'Wird gespeichert...';
// Collect Hero Section Inputs & Animation
const heroTitle = document.getElementById('hero-title-input')?.value;
const heroSubtitle = document.getElementById('hero-subtitle-input')?.value;
const heroBadge = document.getElementById('hero-badge-input')?.value;
const heroAnim = document.getElementById('hero-anim-select')?.value;
if (!siteConfigState.hero) siteConfigState.hero = {};
if (heroTitle !== undefined) siteConfigState.hero.title = heroTitle;
if (heroSubtitle !== undefined) siteConfigState.hero.subtitle = heroSubtitle;
if (heroBadge !== undefined) siteConfigState.hero.badge = heroBadge;
siteConfigState.hero.animation = heroAnim || undefined;
// Collect Bento Section Inputs & Animation
const bentoTitle = document.getElementById('bento-title-input')?.value;
const bentoSubtitle = document.getElementById('bento-subtitle-input')?.value;
const bentoAnim = document.getElementById('bento-anim-select')?.value;
if (!siteConfigState.bento) siteConfigState.bento = {};
if (bentoTitle !== undefined) siteConfigState.bento.sectionTitle = bentoTitle;
if (bentoSubtitle !== undefined) siteConfigState.bento.sectionSubtitle = bentoSubtitle;
siteConfigState.bento.animation = bentoAnim || undefined;
// Collect Header Inputs // Collect Header Inputs
const brandName = document.getElementById('header-brand-name')?.value; const brandName = document.getElementById('header-brand-name')?.value;
const brandHighlight = document.getElementById('header-brand-highlight')?.value; const brandHighlight = document.getElementById('header-brand-highlight')?.value;

View File

@@ -46,3 +46,37 @@ body {
background: radial-gradient(circle, var(--theme-accent) 0%, transparent 70%); background: radial-gradient(circle, var(--theme-accent) 0%, transparent 70%);
} }
/* Preset Animations */
.anim-spring-fade {
animation: springFade 0.8s cubic-bezier(0.175, 0.885, 0.32, 1.275) forwards;
}
.anim-hover-bounce:hover {
transform: translateY(-6px);
transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.anim-pulse-glow {
animation: pulseGlow 3s infinite ease-in-out;
}
.anim-slide-up {
animation: slideUp 0.6s ease-out forwards;
}
.anim-zoom-in {
animation: zoomIn 0.5s cubic-bezier(0.165, 0.84, 0.44, 1) forwards;
}
@keyframes springFade {
0% { opacity: 0; transform: translateY(20px) scale(0.95); }
100% { opacity: 1; transform: translateY(0) scale(1); }
}
@keyframes pulseGlow {
0%, 100% { box-shadow: 0 0 15px -3px var(--theme-accent); }
50% { box-shadow: 0 0 35px 5px var(--theme-accent); }
}
@keyframes slideUp {
0% { opacity: 0; transform: translateY(30px); }
100% { opacity: 1; transform: translateY(0); }
}
@keyframes zoomIn {
0% { opacity: 0; transform: scale(0.85); }
100% { opacity: 1; transform: scale(1); }
}

View File

@@ -18,6 +18,7 @@ export interface SectionConfig {
title?: string; title?: string;
subtitle?: string; subtitle?: string;
badge?: string; badge?: string;
animation?: string;
elements?: UIComponent[]; elements?: UIComponent[];
} }
@@ -45,11 +46,13 @@ export interface SiteConfig {
ctaPrimaryLink: string; ctaPrimaryLink: string;
ctaSecondaryText: string; ctaSecondaryText: string;
ctaSecondaryLink: string; ctaSecondaryLink: string;
animation?: string;
elements?: UIComponent[]; elements?: UIComponent[];
}; };
bento: { bento: {
sectionTitle: string; sectionTitle: string;
sectionSubtitle: string; sectionSubtitle: string;
animation?: string;
cards: Array<{ cards: Array<{
id: string; id: string;
title: string; title: string;
@@ -57,6 +60,7 @@ export interface SiteConfig {
colSpan?: number; colSpan?: number;
badge?: string; badge?: string;
icon?: string; icon?: string;
animation?: string;
}>; }>;
}; };
contact: { contact: {