refactor(layout): switch index.astro to configurable section components driven by siteConfig
This commit is contained in:
36
src/components/sections/BentoGrid.astro
Normal file
36
src/components/sections/BentoGrid.astro
Normal file
@@ -0,0 +1,36 @@
|
||||
---
|
||||
import type { SiteConfig } from '../../utils/configLoader';
|
||||
|
||||
interface Props {
|
||||
config: SiteConfig['bento'];
|
||||
}
|
||||
|
||||
const { config } = Astro.props;
|
||||
---
|
||||
|
||||
<section id="services" class="py-20 px-4 sm:px-6 lg:px-8 max-w-7xl mx-auto relative">
|
||||
<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>
|
||||
<p class="text-slate-400 text-xs sm:text-sm">{config.sectionSubtitle}</p>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
{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="space-y-4 relative z-10">
|
||||
{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">
|
||||
{card.badge}
|
||||
</span>
|
||||
)}
|
||||
<h3 class="text-xl font-bold text-white tracking-tight">{card.title}</h3>
|
||||
<p class="text-slate-400 text-xs sm:text-sm leading-relaxed">{card.description}</p>
|
||||
</div>
|
||||
|
||||
<div class="mt-8 flex justify-end items-center opacity-40 group-hover:opacity-100 transition-opacity">
|
||||
<span class="text-xs font-mono text-sky-400">Details →</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
88
src/components/sections/ContactSection.astro
Normal file
88
src/components/sections/ContactSection.astro
Normal file
@@ -0,0 +1,88 @@
|
||||
---
|
||||
import type { SiteConfig, SmtpConfig } from '../../utils/configLoader';
|
||||
|
||||
interface Props {
|
||||
contactConfig: SiteConfig['contact'];
|
||||
smtpConfig: SmtpConfig;
|
||||
}
|
||||
|
||||
const { contactConfig, smtpConfig } = Astro.props;
|
||||
---
|
||||
|
||||
<section id="contact" class="py-20 px-4 sm:px-6 lg:px-8 max-w-4xl mx-auto relative">
|
||||
<div class="glass-panel rounded-3xl p-8 sm:p-12 relative overflow-hidden">
|
||||
<div class="text-center space-y-3 mb-10">
|
||||
<h2 class="text-3xl font-extrabold text-white tracking-tight uppercase">{contactConfig.title}</h2>
|
||||
<p class="text-slate-400 text-xs sm:text-sm">{contactConfig.subtitle}</p>
|
||||
</div>
|
||||
|
||||
{!smtpConfig.isConfigured && (
|
||||
<div class="mb-6 p-4 rounded-xl bg-amber-500/10 border border-amber-500/20 text-amber-400 text-xs font-mono text-center">
|
||||
⚡ Hinweis: SMTP noch im Platzhalter-Modus. Formular sendet Test-Events an die Server-Log.
|
||||
</div>
|
||||
)}
|
||||
|
||||
<form id="waas-contact-form" class="space-y-5">
|
||||
<div>
|
||||
<label for="name" class="block text-[10px] font-bold uppercase tracking-wider text-slate-400 mb-1.5">Name</label>
|
||||
<input type="text" id="name" name="name" required class="w-full glass-panel bg-slate-950/60 rounded-xl p-3 text-xs text-white focus:outline-none focus:border-sky-400 transition-all" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="email" class="block text-[10px] font-bold uppercase tracking-wider text-slate-400 mb-1.5">E-Mail Adresse</label>
|
||||
<input type="email" id="email" name="email" required class="w-full glass-panel bg-slate-950/60 rounded-xl p-3 text-xs text-white focus:outline-none focus:border-sky-400 transition-all" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="message" class="block text-[10px] font-bold uppercase tracking-wider text-slate-400 mb-1.5">Nachricht</label>
|
||||
<textarea id="message" name="message" rows="4" required class="w-full glass-panel bg-slate-950/60 rounded-xl p-3 text-xs text-white focus:outline-none focus:border-sky-400 transition-all resize-none"></textarea>
|
||||
</div>
|
||||
|
||||
<button type="submit" id="waas-submit-btn" class="w-full py-4 rounded-xl bg-sky-400 text-slate-950 font-bold text-xs uppercase tracking-wider hover:bg-white transition-all shadow-lg shadow-sky-400/20">
|
||||
Anfrage absenden
|
||||
</button>
|
||||
|
||||
<div id="waas-form-feedback" class="hidden text-center text-xs font-medium p-3 rounded-xl"></div>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<script is:inline>
|
||||
const form = document.getElementById('waas-contact-form');
|
||||
const btn = document.getElementById('waas-submit-btn');
|
||||
const feedback = document.getElementById('waas-form-feedback');
|
||||
|
||||
if (form) {
|
||||
form.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'Wird gesendet...';
|
||||
|
||||
try {
|
||||
const formData = new FormData(form);
|
||||
const res = await fetch('/api/contact', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
const data = await res.json();
|
||||
|
||||
feedback.classList.remove('hidden', 'bg-emerald-500/10', 'text-emerald-400', 'bg-red-500/10', 'text-red-400');
|
||||
if (data.success) {
|
||||
feedback.classList.add('bg-emerald-500/10', 'text-emerald-400');
|
||||
feedback.textContent = data.message;
|
||||
form.reset();
|
||||
} else {
|
||||
feedback.classList.add('bg-red-500/10', 'text-red-400');
|
||||
feedback.textContent = data.message || 'Fehler beim Senden.';
|
||||
}
|
||||
} catch (err) {
|
||||
feedback.classList.remove('hidden');
|
||||
feedback.classList.add('bg-red-500/10', 'text-red-400');
|
||||
feedback.textContent = 'Netzwerkfehler.';
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Anfrage absenden';
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
13
src/components/sections/FallbackSection.astro
Normal file
13
src/components/sections/FallbackSection.astro
Normal file
@@ -0,0 +1,13 @@
|
||||
---
|
||||
interface Props {
|
||||
sectionName: string;
|
||||
}
|
||||
|
||||
const { sectionName } = Astro.props;
|
||||
---
|
||||
|
||||
<section class="py-12 px-4 max-w-4xl mx-auto text-center">
|
||||
<div class="glass-panel rounded-2xl p-8 text-slate-400 text-xs font-mono">
|
||||
<p>⚙️ Section <strong>{sectionName}</strong> ist noch in Bearbeitung oder Platzhalter.</p>
|
||||
</div>
|
||||
</section>
|
||||
76
src/components/sections/HeroSection.astro
Normal file
76
src/components/sections/HeroSection.astro
Normal file
@@ -0,0 +1,76 @@
|
||||
---
|
||||
import type { SiteConfig, UIComponent } from '../../utils/configLoader';
|
||||
|
||||
interface Props {
|
||||
config: SiteConfig['hero'];
|
||||
}
|
||||
|
||||
const { config } = Astro.props;
|
||||
|
||||
const elements = config.elements || [];
|
||||
const badgeElements = elements.filter((el: UIComponent) => el.slotId === 'hero-badge-slot');
|
||||
const actionElements = elements.filter((el: UIComponent) => el.slotId === 'hero-actions-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">
|
||||
<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>
|
||||
|
||||
<div class="relative z-10 text-center max-w-4xl mx-auto space-y-8">
|
||||
|
||||
<!-- Slot 1: Badge Slot -->
|
||||
<div id="hero-badge-slot" class="inline-flex flex-wrap items-center justify-center gap-2">
|
||||
{config.badge && (
|
||||
<div class="inline-flex items-center gap-2 px-3.5 py-1.5 rounded-full border border-sky-400/20 bg-sky-400/10 text-sky-400 text-xs font-mono tracking-wide backdrop-blur-md">
|
||||
<span>{config.badge}</span>
|
||||
</div>
|
||||
)}
|
||||
{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">
|
||||
{el.content}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<h1 class="text-4xl sm:text-6xl font-extrabold tracking-tight text-white leading-tight">
|
||||
{config.title}
|
||||
</h1>
|
||||
|
||||
<p class="text-slate-400 text-xs sm:text-lg max-w-2xl mx-auto leading-relaxed font-normal">
|
||||
{config.subtitle}
|
||||
</p>
|
||||
|
||||
<!-- Slot 2: Action Buttons Slot -->
|
||||
<div id="hero-actions-slot" class="flex flex-wrap items-center justify-center gap-4 pt-4">
|
||||
{config.ctaPrimaryText && (
|
||||
<a href={config.ctaPrimaryLink} class="px-8 py-3.5 rounded-xl bg-sky-400 text-slate-950 font-bold text-xs hover:bg-white transition-all duration-300 shadow-lg shadow-sky-400/20 active:scale-95">
|
||||
{config.ctaPrimaryText}
|
||||
</a>
|
||||
)}
|
||||
{config.ctaSecondaryText && (
|
||||
<a href={config.ctaSecondaryLink} class="px-8 py-3.5 rounded-xl glass-panel text-white font-semibold text-xs glass-panel-hover active:scale-95">
|
||||
{config.ctaSecondaryText}
|
||||
</a>
|
||||
)}
|
||||
{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'}`}>
|
||||
{el.content}
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<!-- Slot 3: Custom Elements Slot -->
|
||||
{customElements.length > 0 && (
|
||||
<div id="hero-custom-slot" class="pt-6 flex flex-wrap justify-center gap-4">
|
||||
{customElements.map(el => (
|
||||
<div class="p-4 rounded-xl glass-panel text-xs text-slate-300">
|
||||
<span class="font-bold text-sky-400 block mb-1 uppercase text-[10px]">{el.type} [ID: {el.id}]</span>
|
||||
{el.content}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
</section>
|
||||
Reference in New Issue
Block a user