Compare commits

..

7 Commits

28 changed files with 2901 additions and 129 deletions

104
app/data/site.config.json Normal file
View File

@@ -0,0 +1,104 @@
{
"siteName": "N&D IT Solutions",
"metaDescription": "High-Performance Webdesign & Digital Solutions - 100% DSGVO-konform.",
"hero": {
"badge": "⚡ WaaS Enterprise Baukastensystem",
"title": "Maßgeschneiderte Webseiten. Digital im Griff.",
"subtitle": "Wir entwickeln blitzschnelle, DSGVO-konforme High-End Webseiten für Ihr Unternehmen zum monatlichen Festpreis inklusive Wartung & Hosting.",
"ctaPrimaryText": "Projekt anfragen",
"ctaPrimaryLink": "#contact",
"ctaSecondaryText": "Mehr erfahren",
"ctaSecondaryLink": "#services",
"elements": [
{
"id": "el-1",
"type": "badge",
"content": "š WaaS Engine"
},
{
"id": "el-2",
"type": "button",
"content": "Neuer Button",
"link": "#contact",
"animation0": "spring-fade",
"style": "primary"
}
]
},
"bento": {
"sectionTitle": "High-End Features & Leistung",
"sectionSubtitle": "Modernste Technologie & Höchster Datenschutz vereint.",
"cards": [
{
"id": "card-1",
"title": "Ultra Fast SSR",
"description": "Sub-100ms Ladezeiten dank Astro Hybrid SSR Architecture. Google Core Web Vitals > 95%.",
"colSpan": 2,
"badge": "Performance",
"icon": "zap"
},
{
"id": "card-2",
"title": "100% DSGVO-Sicher",
"description": "Keine Cookies, keine Tracking-Skripte, Zero Third-Party Requests.",
"colSpan": 1,
"badge": "Datenschutz",
"icon": "shield"
},
{
"id": "card-3",
"title": "Liquid Glass UI",
"description": "Moderne Ästhetik mit matte Boxen, Aurora Glows und physischen Kanten-Effekten.",
"colSpan": 1,
"badge": "Design",
"icon": "layers"
},
{
"id": "card-4",
"title": "Sorgenfrei-Paket",
"description": "Hosting, SSL, Wartung und regelmäßige Inhalts-Updates komplett inklusive.",
"colSpan": 2,
"badge": "Full-Service",
"icon": "check"
}
]
},
"contact": {
"title": "Bereit für Ihr Projekt?",
"subtitle": "Schreiben Sie uns direkt. Wir antworten innerhalb von 24 Stunden persönlich.",
"recipientEmail": "kontakt@ndsolutions.de",
"phone": "+49 (0) 123 456789",
"address": "Deutschland"
},
"sectionsOrder": [
"hero",
"bento",
"contact"
],
"sectionsDetails": [
{
"id": "hero",
"type": "hero",
"title": "Hero Sektion",
"elements": [
{
"id": "el-1",
"type": "badge",
"content": "š WaaS Engine"
}
]
},
{
"id": "bento",
"type": "bento",
"title": "Bento Grid",
"elements": []
},
{
"id": "contact",
"type": "contact",
"title": "Contact Section",
"elements": []
}
]
}

10
app/data/smtp.config.json Normal file
View File

@@ -0,0 +1,10 @@
{
"host": "smtp.placeholder.local",
"port": 587,
"secure": false,
"user": "placeholder@domain.de",
"pass": "****",
"fromName": "N&D IT Solutions System",
"fromEmail": "noreply@ndsolutions.de",
"isConfigured": false
}

View File

@@ -0,0 +1,7 @@
{
"activePreset": "sunsetCoral",
"customColors": {
"bg": "#0B0F19",
"accent": "#38BDF8"
}
}

View File

@@ -1,11 +1,13 @@
// @ts-check
import { defineConfig } from 'astro/config';
import tailwind from '@astrojs/tailwind';
import react from '@astrojs/react';
import node from '@astrojs/node';
// https://astro.build/config
export default defineConfig({
output: 'server',
adapter: node({
mode: 'standalone'
}),
integrations: [tailwind(), react()]
});

View File

@@ -0,0 +1,63 @@
{
"siteName": "N&D IT Solutions",
"metaDescription": "High-Performance Webdesign & Digital Solutions - 100% DSGVO-konform.",
"hero": {
"badge": "⚡ WaaS Enterprise Baukastensystem",
"title": "Maßgeschneiderte Webseiten. Digital im Griff.",
"subtitle": "Wir entwickeln blitzschnelle, DSGVO-konforme High-End Webseiten für Ihr Unternehmen zum monatlichen Festpreis inklusive Wartung & Hosting.",
"ctaPrimaryText": "Projekt anfragen",
"ctaPrimaryLink": "#contact",
"ctaSecondaryText": "Mehr erfahren",
"ctaSecondaryLink": "#services"
},
"bento": {
"sectionTitle": "High-End Features & Leistung",
"sectionSubtitle": "Modernste Technologie & Höchster Datenschutz vereint.",
"cards": [
{
"id": "card-1",
"title": "Ultra Fast SSR",
"description": "Sub-100ms Ladezeiten dank Astro Hybrid SSR Architecture. Google Core Web Vitals > 95%.",
"colSpan": 2,
"badge": "Performance",
"icon": "zap"
},
{
"id": "card-2",
"title": "100% DSGVO-Sicher",
"description": "Keine Cookies, keine Tracking-Skripte, Zero Third-Party Requests.",
"colSpan": 1,
"badge": "Datenschutz",
"icon": "shield"
},
{
"id": "card-3",
"title": "Liquid Glass UI",
"description": "Moderne Ästhetik mit matte Boxen, Aurora Glows und physischen Kanten-Effekten.",
"colSpan": 1,
"badge": "Design",
"icon": "layers"
},
{
"id": "card-4",
"title": "Sorgenfrei-Paket",
"description": "Hosting, SSL, Wartung und regelmäßige Inhalts-Updates komplett inklusive.",
"colSpan": 2,
"badge": "Full-Service",
"icon": "check"
}
]
},
"contact": {
"title": "Bereit für Ihr Projekt?",
"subtitle": "Schreiben Sie uns direkt. Wir antworten innerhalb von 24 Stunden persönlich.",
"recipientEmail": "kontakt@ndsolutions.de",
"phone": "+49 (0) 123 456789",
"address": "Deutschland"
},
"sectionsOrder": [
"hero",
"bento",
"contact"
]
}

View File

@@ -0,0 +1,10 @@
{
"host": "smtp.placeholder.local",
"port": 587,
"secure": false,
"user": "placeholder@domain.de",
"pass": "****",
"fromName": "N&D IT Solutions System",
"fromEmail": "noreply@ndsolutions.de",
"isConfigured": false
}

View File

@@ -0,0 +1,7 @@
{
"activePreset": "corporateDark",
"customColors": {
"bg": "#0B0F19",
"accent": "#38BDF8"
}
}

View File

@@ -6,4 +6,11 @@ services:
container_name: websitedummy_app
restart: always
ports:
- "8080:8080"
- "8080:4321"
environment:
- HOST=0.0.0.0
- PORT=4321
- DATA_DIR=/app/data
volumes:
- ./app/data:/app/data

1412
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -12,6 +12,7 @@
"astro": "astro"
},
"dependencies": {
"@astrojs/node": "^8.3.4",
"@astrojs/react": "^3.6.3",
"@astrojs/tailwind": "^5.1.4",
"@fontsource/inter": "^5.2.8",
@@ -19,9 +20,15 @@
"@types/react": "^19.2.17",
"@types/react-dom": "^19.2.3",
"astro": "^4.16.0",
"nodemailer": "^9.0.5",
"react": "^19.2.7",
"react-dom": "^19.2.7",
"react-icons": "^5.7.0",
"tailwindcss": "^3.4.19"
},
"devDependencies": {
"@types/nodemailer": "^8.0.1",
"@vitest/coverage-v8": "^4.1.10",
"vitest": "^4.1.10"
}
}

View File

@@ -0,0 +1,82 @@
---
import { animate } from 'motion';
---
<canvas id="dense-bg" class="dense-bg"></canvas>
<script type="module">
// Rocky GrundSetup
const canvas = document.getElementById('dense-bg');
const ctx = canvas.getContext('2d');
const TAU = Math.PI * 2;
const DENSITY = 200; // Rocky PartikelZahl
const particles = [];
// Rocky Größe anpassen
function resize() {
canvas.width = innerWidth;
canvas.height = innerHeight;
}
resize();
addEventListener('resize', resize);
// Rocky PartikelObjekt
function createParticle() {
return {
x: Math.random() * canvas.width,
y: Math.random() * canvas.height,
r: 1 + Math.random() * 2,
vx: (Math.random() - 0.5) * 0.5,
vy: (Math.random() - 0.5) * 0.5,
alpha: Math.random() * 0.5 + 0.2,
};
}
// Rocky Initialisierung
for (let i = 0; i < DENSITY; i++) particles.push(createParticle());
// Rocky MausTracking
let mouse = { x: -9999, y: -9999 };
addEventListener('mousemove', e => {
mouse.x = e.clientX;
mouse.y = e.clientY;
});
// Rocky UpdateLoop
function update() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
particles.forEach(p => {
// Rocky Bewegung
p.x += p.vx;
p.y += p.vy;
// Rocky RandWrap
if (p.x < 0) p.x = canvas.width;
if (p.x > canvas.width) p.x = 0;
if (p.y < 0) p.y = canvas.height;
if (p.y > canvas.height) p.y = 0;
// Rocky MausAnziehung
const dx = mouse.x - p.x;
const dy = mouse.y - p.y;
const dist = Math.hypot(dx, dy);
if (dist < 100) {
const pull = (100 - dist) / 100;
p.vx += dx * 0.001 * pull;
p.vy += dy * 0.001 * pull;
}
// Rocky FadeIn/Out
p.alpha += (Math.random() - 0.5) * 0.005;
p.alpha = Math.min(Math.max(p.alpha, 0.1), 0.8);
// Rocky Zeichen
ctx.beginPath();
ctx.arc(p.x, p.y, p.r, 0, TAU);
ctx.fillStyle = `rgba(255,255,255,${p.alpha})`;
ctx.fill();
});
requestAnimationFrame(update);
}
// Rocky Start
update();
</script>
<style>
.dense-bg {
position: fixed;
inset: 0;
pointer-events: none;
z-index: -1;
background: #0b0f19; /* Rocky dunkler Hintergrund */
}
</style>
---

View File

@@ -254,59 +254,7 @@ const heroTemplates = [
<span class="text-[9px] font-mono text-slate-400 dark:text-zinc-600 mt-6 tracking-wider">Abmahnsicher</span>
</div>
<!-- Card 4: ROI-Rechner (Breit: col-span-2) -->
<div class="relative md:col-span-2 rounded-2xl border border-white/80 dark:border-white/[0.06] bg-white/40 dark:bg-slate-900/10 p-8 backdrop-blur-xl hover:border-sky-300 dark:hover:border-sky-500/20 hover:bg-white/60 dark:hover:bg-slate-950/20 transition-all duration-300 group flex flex-col justify-between min-h-[380px] shadow-sm hover:shadow-md reveal">
<span class="absolute top-3 left-3 w-1.5 h-1.5 border-t border-l border-slate-300 dark:border-zinc-700/50"></span>
<span class="absolute top-3 right-3 w-1.5 h-1.5 border-t border-r border-slate-300 dark:border-zinc-700/50"></span>
<div>
<h3 class="text-xl font-bold text-slate-950 dark:text-white mb-4 uppercase tracking-tight">Einfacher Spar-Rechner</h3>
<p class="text-slate-600 dark:text-zinc-400 text-xs mb-6 leading-relaxed">
Sehen Sie selbst, wie viel Zeit und Geld Sie sparen, wenn wir die Arbeit für Sie übernehmen.
</p>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-6">
<div class="space-y-4">
<div>
<label class="block text-[10px] font-bold text-slate-500 dark:text-zinc-400 uppercase mb-1" for="roi-cost">Bisherige Webseiten-Kosten (pro Jahr in €):</label>
<input type="number" id="roi-cost" value="600" class="w-full bg-white dark:bg-zinc-950 border border-slate-200 dark:border-brand-darkBorder rounded p-2.5 text-xs text-slate-800 dark:text-white focus:outline-none focus:ring-2 focus:ring-sky-500" />
</div>
<div>
<label class="block text-[10px] font-bold text-slate-500 dark:text-zinc-400 uppercase mb-1" for="roi-hours">Eigene Arbeitszeit an der Seite (Stunden pro Monat):</label>
<input type="number" id="roi-hours" value="4" class="w-full bg-white dark:bg-zinc-950 border border-slate-200 dark:border-brand-darkBorder rounded p-2.5 text-xs text-slate-800 dark:text-white focus:outline-none focus:ring-2 focus:ring-sky-500" />
</div>
</div>
<div class="bg-sky-500/5 border border-sky-500/10 dark:border-sky-500/5 p-6 rounded-xl flex flex-col justify-center">
<span class="text-[10px] font-mono text-slate-400 dark:text-zinc-500 uppercase tracking-wider">Ihre Geld-Ersparnis:</span>
<p class="text-3xl font-extrabold text-sky-600 dark:text-sky-400 mt-2" id="roi-result">0 € / Jahr</p>
<p class="text-xs text-emerald-600 dark:text-emerald-400 mt-1 font-medium" id="roi-time">0 Stunden gerettete Freizeit im Jahr</p>
</div>
</div>
</div>
<script is:inline>
(function() {
const costInput = document.getElementById('roi-cost');
const hoursInput = document.getElementById('roi-hours');
const resultText = document.getElementById('roi-result');
const timeText = document.getElementById('roi-time');
function calc() {
const cost = parseFloat(costInput.value) || 0;
const hours = parseFloat(hoursInput.value) || 0;
const waasYear = 99 * 12;
const currentTotalYear = cost + (hours * 50 * 12);
const saving = Math.max(0, currentTotalYear - waasYear);
resultText.textContent = `${saving.toLocaleString('de-DE')} € / Jahr`;
timeText.textContent = `${hours * 12} Stunden Lebenszeit zurück / Jahr`;
}
if (costInput && hoursInput && resultText && timeText) {
costInput.addEventListener('input', calc);
hoursInput.addEventListener('input', calc);
calc();
}
})();
</script>
</div>
</div>

View File

@@ -121,13 +121,14 @@ import { FaBars, FaTimes } from 'react-icons/fa';
updateToggleIcons();
themeToggleBtn?.addEventListener('click', () => {
// Toggle class
if (document.documentElement.classList.contains('dark')) {
document.documentElement.classList.remove('dark');
localStorage.setItem('theme', 'light');
} else {
if (document.documentElement.classList.contains('light')) {
document.documentElement.classList.remove('light');
document.documentElement.classList.add('dark');
localStorage.setItem('theme', 'dark');
} else {
document.documentElement.classList.remove('dark');
document.documentElement.classList.add('light');
localStorage.setItem('theme', 'light');
}
updateToggleIcons();
});

View 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 &rarr;</span>
</div>
</div>
))}
</div>
</section>

View 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>

View 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>

View 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>

View File

@@ -8,13 +8,20 @@ import '@fontsource/space-grotesk/400.css';
import '@fontsource/space-grotesk/500.css';
import '@fontsource/space-grotesk/600.css';
import '@fontsource/space-grotesk/700.css';
import '../styles/glassmorphism.css';
import { loadWaasConfigs } from '../utils/configLoader';
import { THEME_PRESETS } from '../utils/themePresets';
interface Props {
title: string;
description?: string;
}
const { title, description = "Professionelles Webdesign & moderne Websites für Kleinbetriebe, Handwerker und Selbstständige. N&D i-t SOLUTIONS erstellt Ihre Homepage bezahlbar und schnell." } = Astro.props;
const { siteConfig, themeConfig } = loadWaasConfigs();
const activePreset = THEME_PRESETS[themeConfig.activePreset] || THEME_PRESETS.corporateDark;
const colors = activePreset.colors;
const { title, description = "Professionelles Webdesign & moderne Websites." } = Astro.props;
---
<!doctype html>
@@ -26,19 +33,44 @@ const { title, description = "Professionelles Webdesign & moderne Websites für
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<title>{title}</title>
<!-- Inline script to set dark/light mode before page renders -->
<style is:global define:vars={{
themeBg: colors.bg,
themeCardBg: colors.cardBg,
themeText: colors.text,
themeAccent: colors.accent,
themeBorder: colors.border
}}>
:root {
--theme-bg: var(--themeBg);
--theme-card-bg: var(--themeCardBg);
--theme-text: var(--themeText);
--theme-accent: var(--themeAccent);
--theme-border: var(--themeBorder);
}
html.light {
--theme-bg: #F8FAFC;
--theme-card-bg: rgba(255, 255, 255, 0.7);
--theme-text: #0F172A;
--theme-border: rgba(0, 0, 0, 0.1);
}
</style>
<!-- Inline theme initialization to prevent FOUC -->
<script is:inline>
(function() {
const theme = localStorage.getItem('theme');
if (theme === 'dark' || (!theme && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
document.documentElement.classList.add('dark');
} else {
if (theme === 'light') {
document.documentElement.classList.add('light');
document.documentElement.classList.remove('dark');
} else {
document.documentElement.classList.add('dark');
document.documentElement.classList.remove('light');
}
})();
</script>
</head>
<body class="bg-slate-50 dark:bg-brand-darkBg text-slate-800 dark:text-brand-lightText font-sans transition-colors duration-300 antialiased selection:bg-brand-arcticBlue/20 selection:text-brand-arcticBlue">
<body class="font-sans transition-colors duration-500 antialiased">
<slot />
</body>
</html>

File diff suppressed because one or more lines are too long

224
src/pages/admin/index.astro Normal file
View File

@@ -0,0 +1,224 @@
---
import Layout from '../../layouts/Layout.astro';
import { loadWaasConfigs } from '../../utils/configLoader';
import { THEME_PRESETS } from '../../utils/themePresets';
const { siteConfig, themeConfig, smtpConfig } = loadWaasConfigs();
const presets = Object.values(THEME_PRESETS);
---
<Layout title="Waas Admin Dashboard | Layout and Config Management">
<div class="min-h-screen bg-[#0B0F19] text-white p-6 sm:p-10 font-sans">
<div class="max-w-6xl mx-auto space-y-10">
<!-- Admin Header -->
<div class="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4 border-b border-white/10 pb-6">
<div>
<span class="text-xs font-mono text-sky-400 uppercase tracking-widest">N& D Waas Engine</span>
<h1 class="text-3xl font-extrabold tracking-tight mt-1">Layout & Config Dashboard</h1>
</div>
<div class="flex items-center gap-3">
<a href="/" target="_blank" class="px-4 py-2 rounded-xl glass-panel text-xs font-semibold hover:border-sky-400 transition-all flex items-center gap-2">
Live Seite Ansehen
</a>
<button id="save-all-btn" class="px-6 py-2.5 rounded-xl bg-sky-400 text-slate-95 font-bold text-xs uppercase tracking-wider hover:bg-white transition-all shadow-lg shadow-sky-400/20 active:scale-95">
Einstellungen Speichern
</button>
</div>
</div>
<div id="admin-alert" class="hidden p-4 rounded-xl text-xs font-mono text-center transition-all"></div>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
<!-- Main Form Column -->
<div class="lg:col-span-2 space-y-8">
<!-- Site Info -->
<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">
Allgemeine Informationen
</h2>
<div class="space-y-4">
<div>
<label class="block text-xs font-bold text-slate-400 mb-1">Webseiten-Name</label>
<input type="text" id="siteName" value={siteConfig.siteName} 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-bold text-slate-400 mb-1">Meta Beschreibung</label>
<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>
</div>
<!-- Hero Config -->
<div class="glass-panel rounded-3xl p-6 space-y-4">
<h2 class="text-lg font-bold text-sky-400 uppercase tracking-wider flex items-center gap-2">
Hero Sektion
</h2>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<label class="block text-xs font-bold text-slate-400 mb-1">Badge</label>
<input type="text" id="heroBadge" value={siteConfig.hero.badge} 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-bold text-slate-400 mb-1">Hauptueberschrift (Title)</label>
<input type="text" id="heroTitle" value={siteConfig.hero.title} 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 class="sm:col-span-2">
<label class="block text-xs font-bold text-slate-400 mb-1">Unterueberschrift (Subtitle)</label>
<textarea id="heroSubtitle" 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.hero.subtitle}</textarea>
</div>
<div>
<label class="block text-xs font-bold text-slate-400 mb-1">CTA2 Text</label>
<input type="text" id="ctaPrimaryText" value={siteConfig.hero.ctaPrimaryText} 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-bold text-slate-400 mb-1">CTA2 Link</label>
<input type="text" id="ctaPrimaryLink" value={siteConfig.hero.ctaPrimaryLink} 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>
<!-- Section Order Management -->
<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">
Sektionen Reihenfolge (Layout)
</h2>
<p class="text-xs text-slate-400">Kommagetrennte Liste der aktiv gerenderten Sektionen:</p>
<input type="text" id="sectionsOrder" value={siteConfig.sectionsOrder.join(', ')} class="w-full glass-panel bg-slate-950/60 rounded-xl p-3 text-xs text-white font-mono focus:outline-none focus:border-sky-400" />
</div>
</div>
<!-- Sidebar Column -->
<div class="space-y-8">
<!-- Theme Preset Picker -->
<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">
Theme Preset (10 Styles)
</h2>
<div class="space-y-2 max-h-[320px] overflow-y-auto pr-1">
{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'}`}>
<div class="flex items-center gap-3">
<input type="radio" name="activePreset" value={p.id} checked={themeConfig.activePreset === p.id} class="text-sky-400 focus:ring-0" />
<span class="text-xs font-semibold">{p.name}</span>
</div>
<div class="flex items-center gap-1">
<span class="w-3 h-3 rounded-full inline-block border border-white/20" style={`background-color: ${p.colors.bg};`}></span>
<span class="w-3 h-3 rounded-full inline-block" style={`background-color: ${p.colors.accent};`}></span>
</div>
</label>
))}
</div>
</div>
<!-- SMTP Config -->
<div class="glass-panel rounded-3xl p-6 space-y-4">
<h2 class="text-lg font-bold text-sky-400 uppercase tracking-wider flex items-center gap-2">
SMTP E-Mail Einstellungen
</h2>
<div class="space-y-3">
<div>
<label class="block text-[10px] font-bold text-slate-400 uppercase mb-1">SMTP Host</label>
<input type="text" id="smtpHost" value={smtpConfig.host} class="w-full glass-panel bg-slate-950/60 rounded-xl p-2.5 text-xs text-white focus:outline-none focus:border-sky-400" />
</div>
<div class="grid grid-cols-2 gap-2">
<div>
<label class="block text-[10px] font-bold text-slate-400 uppercase mb-1">Port</label>
<input type="number" id="smtpPort" value={smtpConfig.port} class="w-full glass-panel bg-slate-950/60 rounded-xl p-2.5 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">User</label>
<input type="text" id="smtpUser" value={smtpConfig.user} class="w-full glass-panel bg-slate-950-60 rounded-xl p-2.5 text-xs text-white focus:outline-none focus:border-sky-400" />
</div>
</div>
<div>
<label class="block text-[10px] font-bold text-slate-400 uppercase mb-1">Passwort</label>
<input type="password" id="smtpPass" value={smtpConfig.pass} class="w-full glass-panel bg-slate-950-60 rounded-xl p-2.5 text-xs text-white focus:outline-none focus:border-sky-400" />
</div>
<label class="flex items-center gap-2 pt-2 cursor-pointer">
<input type="checkbox" id="smtpIsConfigured" checked={smtpConfig.isConfigured} class="rounded text-sky-400 focus:ring-0" />
<span class="text-xs text-slate-300 font-semibold">SMTP Aktivieren (Live-Betrieb)</span>
</label>
</div>
</div>
</div>
</div>
</div>
</div>
</Layout>
<script is:inline>
const saveBtn = document.getElementById('save-all-btn');
const alertBox = document.getElementById('admin-alert');
if (saveBtn) {
saveBtn.addEventListener('click', async () => {
saveBtn.disabled = true;
saveBtn.textContent = 'Wird gespeichert...';
const selectedPreset = document.querySelector('input[name="activePreset"]:checked')?.value || 'corporateDark';
const sections = document.getElementById('sectionsOrder').value.split(',').map(s => s.trim()).filter(Boolean);
const getVal = (id) => (document.getElementById(id))?.value || '';
const getNum = (id) => parseInt(document.getElementById(id)?.value || '0') || 587;
const getBool = (id) => !!(document.getElementById(id))?.checked;
const payload = {
siteConfig: {
siteName: getVal('siteName'),
metaDescription: getVal('metaDescription'),
hero: {
badge: getVal('heroBadge'),
title: getVal('heroTitle'),
subtitle: getVal('heroSubtitle'),
ctaPrimaryText: getVal('ctaPrimaryText'),
ctaPrimaryLink: getVal('ctaPrimaryLink')
},
sectionsOrder: sections
},
themeConfig: {
activePreset: selectedPreset
},
smtpConfig: {
host: getVal('smtpHost'),
port: getNum('smtpPort'),
user: getVal('smtpUser'),
pass: getVal('smtpPass'),
isConfigured: getBool('smtpIsConfigured')
}
};
try {
const res = await fetch('/api/config', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
const result = await res.json();
alertBox.classList.remove('hidden', 'bg-emerald-500/10', 'text-emerald-400', 'bg-red-500/10', 'text-red-400');
if (result.success) {
alertBox.classList.add('bg-emerald-500/10', 'text-emerald-400');
alertBox.textContent = 'Konfiguration erfolgeich gespeichert! SSR empfaengt die Aenderungen sofort.';
} else {
alertBox.classList.add('bg-red-500/10', 'text-red-400');
alertBox.textContent = 'Fehler: ' + result.message;
}
} catch (e) {
alertBox.classList.remove('hidden');
alertBox.classList.add('bg-red-500/10', 'text-emerald-400');
alertBox.textContent = 'Fehler beim Speichern.';
} finally {
saveBtn.disabled = false;
saveBtn.textContent = 'Einstellungen Speichern';
}
});
}
</script>

39
src/pages/api/config.ts Normal file
View File

@@ -0,0 +1,39 @@
import type { APIRoute } from 'astro';
import fs from 'fs';
import path from 'path';
import { loadWaasConfigs } from '../../utils/configLoader';
export const POST: APIRoute = async ({ request }) => {
try {
const body = await request.json();
const dataDir = process.env.DATA_DIR || path.join(process.cwd(), 'app', 'data');
if (!fs.existsSync(dataDir)) {
fs.mkdirSync(dataDir, { recursive: true });
}
const { siteConfig: currentSite, themeConfig: currentTheme, smtpConfig: currentSmtp } = loadWaasConfigs();
if (body.siteConfig) {
const newSite = {
...currentSite,
...body.siteConfig,
hero: { ...currentSite.hero, ...body.siteConfig.hero }
};
fs.writeFileSync(path.join(dataDir, 'site.config.json'), JSON.stringify(newSite, null, 2), 'utf8');
}
if (body.themeConfig) {
const newTheme = { ...currentTheme, ...body.themeConfig };
fs.writeFileSync(path.join(dataDir, 'theme.config.json'), JSON.stringify(newTheme, null, 2), 'utf8');
}
if (body.smtpConfig) {
const newSmtp = { ...currentSmtp, ...body.smtpConfig };
fs.writeFileSync(path.join(dataDir, 'smtp.config.json'), JSON.stringify(newSmtp, null, 2), 'utf8');
}
return new Response(JSON.stringify({ success: true, message: 'Konfiguration gespeichert' }), { status: 200, headers: { 'Content-Type': 'application/json' } });
} catch (err: any) {
return new Response(JSON.stringify({ success: false, message: err.message }), { status: 500, headers: { 'Content-Type': 'application/json' } });
}
};

30
src/pages/api/contact.ts Normal file
View File

@@ -0,0 +1,30 @@
import type { APIRoute } from 'astro';
import { loadWaasConfigs } from '../../utils/configLoader';
import nodemailer from 'nodemailer';
export const POST: APIRoute = async ({ request }) => {
try {
const data = await request.formData();
const name = data.get('name')?.toString() || '';
const email = data.get('email')?.toString() || '';
const message = data.get('message')?.toString() || '';
if (!name || !email || !message) {
return new Response(JSON.stringify({ success: false, message: 'Bitte füllen Sie alle Pflichtfelder aus.' }), { status: 400, headers: { 'Content-Type': 'application/json' } });
}
const { smtpConfig, siteConfig } = loadWaasConfigs();
if (!smtpConfig.isConfigured || smtpConfig.host === 'smtp.placeholder.local') {
console.log('[WaaS Contact API] (Demo Mode) Received Submission:', { name, email, message });
return new Response(JSON.stringify({ success: true, mode: 'placeholder', message: 'Anfrage erfolgreich empfangen! (Demo-Modus)' }), { status: 200, headers: { 'Content-Type': 'application/json' } });
}
const transporter = nodemailer.createTransport({ host: smtpConfig.host, port: smtpConfig.port, secure: smtpConfig.secure, auth: { user: smtpConfig.user, pass: smtpConfig.pass } });
const sender = smtpConfig.fromName + ' ' + smtpConfig.fromEmail;
await transporter.sendMail({ from: sender, to: siteConfig.contact.recipientEmail, replyTo: email, subject: '[Webseite] Neue Nachricht von ' + name, text: 'Name: ' + name + '\nEmail: ' + email + '\n\nNachricht:\n' + message });
return new Response(JSON.stringify({ success: true, message: 'Vielen Dank für Ihre Anfrage! Wir melden uns umgehend.' }), { status: 200, headers: { 'Content-Type': 'application/json' } });
} catch (err: any) {
return new Response(JSON.stringify({ success: false, message: 'Fehler beim Senden: ' + err.message }), { status: 500, headers: { 'Content-Type': 'application/json' } });
}
};

View File

@@ -0,0 +1,45 @@
import type { APIRoute } from 'astro';
import fs from 'fs';
import path from 'path';
export const POST: APIRoute = async ({ request }) => {
try {
const body = await request.json();
if (!body || typeof body !== 'object') {
return new Response(JSON.stringify({ success: false, message: 'Invalid JSON payload' }), {
status: 400,
headers: { 'Content-Type': 'application/json' }
});
}
// Validate critical fields
if (body.siteName && typeof body.siteName !== 'string') {
return new Response(JSON.stringify({ success: false, message: 'siteName must be a string' }), {
status: 400,
headers: { 'Content-Type': 'application/json' }
});
}
const dataDir = process.env.DATA_DIR || path.join(process.cwd(), 'app', 'data');
if (!fs.existsSync(dataDir)) {
fs.mkdirSync(dataDir, { recursive: true });
}
const sitePath = path.join(dataDir, 'site.config.json');
fs.writeFileSync(sitePath, JSON.stringify(body, null, 2), 'utf-8');
return new Response(JSON.stringify({
success: true,
message: 'Site configuration successfully updated in /app/data/site.config.json'
}), {
status: 200,
headers: { 'Content-Type': 'application/json' }
});
} catch (err: any) {
return new Response(JSON.stringify({ success: false, message: 'Fehler beim Speichern: ' + err.message }), {
status: 500,
headers: { 'Content-Type': 'application/json' }
});
}
};

View File

@@ -2,56 +2,47 @@
import Layout from '../layouts/Layout.astro';
import Navbar from '../components/Navbar.astro';
import Footer from '../components/Footer.astro';
import HeroAndLiquidGlass from '../components/HeroAndLiquidGlass.astro';
import OnboardingWizard from '../components/OnboardingWizard.astro';
import HeroSection from '../components/sections/HeroSection.astro';
import BentoGrid from '../components/sections/BentoGrid.astro';
import ContactSection from '../components/sections/ContactSection.astro';
import FallbackSection from '../components/sections/FallbackSection.astro';
import { loadWaasConfigs } from '../utils/configLoader';
import { THEME_PRESETS } from '../utils/themePresets';
const { siteConfig, themeConfig, smtpConfig } = loadWaasConfigs();
const activePreset = THEME_PRESETS[themeConfig.activePreset] || THEME_PRESETS.corporateDark;
const colors = activePreset.colors;
const sectionComponents: Record<string, any> = {
hero: HeroSection,
bento: BentoGrid,
services: BentoGrid,
contact: ContactSection
};
---
<Layout title="N&D i-t SOLUTIONS | Webdesign & Entwicklung">
<Navbar />
<main>
{/* Clean, modular Hero and Liquid Glass section */}
<HeroAndLiquidGlass />
<!-- Multi-step Onboarding Wizard -->
<OnboardingWizard />
<!-- Contact section (separate, clean layout) -->
<section id="contact" class="py-20 bg-slate-50 dark:bg-[#0B0F19] relative border-t border-slate-200 dark:border-white/5 transition-colors duration-300">
<div class="absolute inset-0 opacity-[0.015] pointer-events-none bg-[radial-gradient(#000_1px,transparent_1px)] dark:bg-[radial-gradient(#fff_1px,transparent_1px)] [background-size:16px_16px] transition-opacity duration-300"></div>
<div class="max-w-3xl mx-auto px-4 sm:px-6 lg:px-8 relative z-10">
<div class="bg-white/40 dark:bg-slate-900/40 backdrop-blur-md border border-slate-200 dark:border-white/5 rounded-2xl p-8 sm:p-10 transition-all duration-300 relative tactile-noise shadow-sm dark:shadow-none">
<span class="absolute top-2.5 left-2.5 w-2 h-2 border-t border-l border-slate-300 dark:border-zinc-700/50"></span>
<span class="absolute top-2.5 right-2.5 w-2 h-2 border-t border-r border-slate-300 dark:border-zinc-700/50"></span>
<span class="absolute bottom-2.5 left-2.5 w-2 h-2 border-b border-l border-slate-300 dark:border-zinc-700/50"></span>
<span class="absolute bottom-2.5 right-2.5 w-2 h-2 border-b border-r border-slate-300 dark:border-zinc-700/50"></span>
<h2 class="text-3xl font-extrabold text-slate-900 dark:text-white mb-2 uppercase tracking-tight transition-colors duration-300">Bereit für Ihr Projekt?</h2>
<p class="text-slate-600 dark:text-zinc-400 text-xs mb-8 transition-colors duration-300">
Schreiben Sie uns direkt. Wir melden uns innerhalb von 24 Stunden persönlich bei Ihnen.
</p>
<form class="space-y-4" onsubmit="event.preventDefault()">
<div>
<label for="name" class="block text-[10px] font-bold text-slate-500 dark:text-zinc-400 uppercase mb-1">Name</label>
<input type="text" id="name" required aria-required="true" class="w-full bg-white dark:bg-zinc-950 border border-slate-200 dark:border-brand-darkBorder rounded p-2.5 text-xs text-slate-800 dark:text-white focus:outline-none focus:ring-2 focus:ring-sky-500 focus:ring-offset-2 focus:ring-offset-white dark:focus:ring-offset-slate-900 transition-all" />
</div>
<div>
<label for="email" class="block text-[10px] font-bold text-slate-500 dark:text-zinc-400 uppercase mb-1">E-Mail</label>
<input type="email" id="email" required aria-required="true" class="w-full bg-white dark:bg-zinc-950 border border-slate-200 dark:border-brand-darkBorder rounded p-2.5 text-xs text-slate-800 dark:text-white focus:outline-none focus:ring-2 focus:ring-sky-500 focus:ring-offset-2 focus:ring-offset-white dark:focus:ring-offset-slate-900 transition-all" />
</div>
<div>
<label for="message" class="block text-[10px] font-bold text-slate-500 dark:text-zinc-400 uppercase mb-1">Nachricht</label>
<textarea id="message" rows="4" required aria-required="true" class="w-full bg-white dark:bg-zinc-950 border border-slate-200 dark:border-brand-darkBorder rounded p-2.5 text-xs text-slate-800 dark:text-white focus:outline-none focus:ring-2 focus:ring-sky-500 focus:ring-offset-2 focus:ring-offset-white dark:focus:ring-offset-slate-900 transition-all resize-none"></textarea>
</div>
<button type="submit" class="w-full py-3 bg-sky-600 hover:bg-sky-700 dark:bg-brand-arcticBlue text-white dark:text-zinc-950 font-bold text-xs uppercase tracking-wider rounded dark:hover:bg-white transition-colors focus:ring-2 focus:ring-offset-2 focus:ring-sky-500 focus:outline-none">
Anfrage senden
</button>
</form>
</div>
</div>
</section>
</main>
<Footer />
<Layout title={siteConfig.siteName} description={siteConfig.metaDescription}>
<div
class="min-h-screen transition-colors duration-500 font-sans relative overflow-hidden"
style={`background-color: ${themeConfig.customColors?.bg || colors.bg}; color: ${colors.text};`}
>
<Navbar />
<main>
{siteConfig.sectionsOrder.map((sectionKey) => {
if (sectionKey === 'hero') {
return <HeroSection config={siteConfig.hero} />;
}
if (sectionKey === 'bento' || sectionKey === 'services') {
return <BentoGrid config={siteConfig.bento} />;
}
if (sectionKey === 'contact') {
return <ContactSection contactConfig={siteConfig.contact} smtpConfig={smtpConfig} />;
}
return <FallbackSection sectionName={sectionKey} />;
})}
</main>
<Footer />
</div>
</Layout>

View File

@@ -0,0 +1,48 @@
/* Central Color Pattern & WaaS Utility Token System */
:root {
--theme-bg: #0B0F19;
--theme-card-bg: rgba(15, 23, 42, 0.4);
--theme-text: #F8FAFC;
--theme-accent: #38BDF8;
--theme-border: rgba(255, 255, 255, 0.1);
}
body {
background-color: var(--theme-bg) !important;
color: var(--theme-text) !important;
}
.glass-panel {
background: var(--theme-card-bg);
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
border: 1px solid var(--theme-border);
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.37);
}
.glass-panel-hover {
transition: all 0.3s ease;
}
.glass-panel-hover:hover {
border-color: var(--theme-accent);
box-shadow: 0 0 25px -5px var(--theme-accent);
}
.aurora-glow {
position: absolute;
border-radius: 50%;
filter: blur(80px);
pointer-events: none;
opacity: 0.2;
z-index: 0;
}
.aurora-glow-sky {
background: radial-gradient(circle, var(--theme-accent) 0%, transparent 70%);
}
.aurora-glow-indigo {
background: radial-gradient(circle, var(--theme-accent) 0%, transparent 70%);
}

199
src/utils/configLoader.ts Normal file
View File

@@ -0,0 +1,199 @@
import fs from 'fs';
import path from 'path';
export interface UIComponent {
id: string; // Eindeutige ID (z.B. uuid / timestamp)
slotId: string; // Platz-ID (z.B. 'hero-badge-slot', 'hero-actions-slot', 'bento-slot-1')
orderIndex?: number; // Position / Reihenfolge auf dem Platz
type: 'button' | 'badge' | 'text' | 'card' | 'accordion' | 'stat-box' | 'testimonial' | 'cta-box';
content: string;
link?: string;
animation0?: string; // e.g. 'spring-fade', 'hover-bounce', 'pulse-glow'
style?: 'primary' | 'secondary' | 'outline' | 'glass';
}
export interface SectionConfig {
id: string;
type: 'hero' | 'bento' | 'contact' | 'custom';
title?: string;
subtitle?: string;
badge?: string;
elements?: UIComponent[];
}
export interface SiteConfig {
siteName: string;
metaDescription: string;
hero: {
badge: string;
title: string;
subtitle: string;
ctaPrimaryText: string;
ctaPrimaryLink: string;
ctaSecondaryText: string;
ctaSecondaryLink: string;
elements?: UIComponent[];
};
bento: {
sectionTitle: string;
sectionSubtitle: string;
cards: Array<{
id: string;
title: string;
description: string;
colSpan?: number;
badge?: string;
icon?: string;
}>;
};
contact: {
title: string;
subtitle: string;
recipientEmail: string;
phone?: string;
address?: string;
};
sectionsOrder: string[];
sectionsDetails?: SectionConfig[];
}
export interface ThemeConfig {
activePreset: string;
customColors?: {
bg?: string;
accent?: string;
};
}
export interface SmtpConfig {
host: string;
port: number;
secure: boolean;
user: string;
pass: string;
fromName: string;
fromEmail: string;
isConfigured: boolean;
}
export const FALLBACK_SITE_CONFIG: SiteConfig = {
siteName: 'N&D IT Solutions',
metaDescription: 'High-Performance Webdesign & Digital Solutions - 100% DSGVO-konform.',
hero: {
badge: 'š Waas Enterprise Baukastensystem',
title: 'Maßgeschneiderte Webseiten. Digital im Griff.',
subtitle: 'Wir entwickeln blitzschnelle, DSGVO-konforme High-End Webseiten für Ihr Unternehmen zum monatlichen Festpreis inklusive Wartung & Hosting.',
ctaPrimaryText: 'Projekt anfragen',
ctaPrimaryLink: '#contact',
ctaSecondaryText: 'Mehr erfahren',
ctaSecondaryLink: '#services',
elements: [
{ id: 'el-1', type: 'badge', content: 'š WaaS Engine' },
{ id: 'el-2', type: 'button', content: 'Neuer Button', link: '#contact', animation0: 'spring-fade', style: 'primary' }
]
},
bento: {
sectionTitle: 'High-End Features & Leistung',
sectionSubtitle: 'Modernste Technologie & Höchster Datenschutz vereint.',
cards: [
{
id: 'card-1',
title: 'Ultra Fast SSR',
description: 'Sub-100ms Ladezeiten dank Astro Hybrid SSR Architecture. Google Core Web Vitals > 95%.',
colSpan: 2,
badge: 'Performance',
icon: 'zap'
},
{
id: 'card-2',
title: '100% DSGVO-Sicher',
description: 'Keine Cookies, keine Tracking-Skripte, Zero Third-Party Requests.',
colSpan: 1,
badge: 'Datenschutz',
icon: 'shield'
},
{
id: 'card-3',
title: 'Liquid Glass UI',
description: 'Moderne <20> sthetik mit matte Boxen, Aurora Glows und physikalischen Kanten-Effekten.',
colSpan: 1,
badge: 'Design',
icon: 'layers'
},
{
id: 'card-4',
title: 'Sorgenfrei-Paket',
description: 'Hosting, SSL, Wartung und regelmäßige Inhalts-Updates komplett inklusive.',
colSpan: 2,
badge: 'Full-Service',
icon: 'check'
}
]
},
contact: {
title: 'Bereit für Ihr Projekt?',
subtitle: 'Schreiben Sie uns direkt. Wir antworten innerhalb von 24 Stunden persúnlich.',
recipientEmail: 'kontakt@ndsolutions.de',
phone: '+49 (0) 123 456789',
address: 'Deutschland'
},
sectionsOrder: ['hero', 'bento', 'contact'],
sectionsDetails: [
{ id: 'hero', type: 'hero', title: 'Hero Sektion', elements: [ {id: 'el-1', type: 'badge', content: 'š WaaS Engine' } ] },
{ id: 'bento', type: 'bento', title: 'Bento Grid', elements: [] },
{ id: 'contact', type: 'contact', title: 'Contact Section', elements: [] }
]
};
export const FALLBACK_THEME_CONFIG: ThemeConfig = {
activePreset: 'corporateDark'
};
export const FALLBACK_SMTP_CONFIG: SmtpConfig = {
host: 'smtp.placeholder.local',
port: 587,
secure: false,
user: 'placeholder@domain.de',
pass: '****',
fromName: 'N&D IT Solutions System',
fromEmail: 'noreply@ndsolutions.de',
isConfigured: false
};
function readJsonFile<T>(filePath: string, fallback: T): T {
try {
if (!fs.existsSync(filePath)) {
return fallback;
}
const content = fs.readFileSync(filePath, 'utf-8');
if (!content.trim()) {
return fallback;
}
const parsed = JSON.parse(content);
return { ...fallback, ...parsed };
} catch (error) {
console.warn(`[WaaS ConfigLoader] Warning loading ${filePath}, using fallback defaults.`, error);
return fallback;
}
}
export function loadWaasConfigs() {
const dataDir = process.env.DATA_DIR || path.join(process.cwd(), 'app', 'data');
const sitePath = path.join(dataDir, 'site.config.json');
const themePath = path.join(dataDir, 'theme.config.json');
const smtpPath = path.join(dataDir, 'smtp.config.json');
const rawSite = readJsonFile<Partial<SiteConfig>>(sitePath, FALLBACK_SITE_CONFIG);
const siteConfig: SiteConfig = {
...FALLBACK_SITE_CONFIG,
...rawSite,
hero: { ...FALLBACK_SITE_CONFIG.hero, ...(rawSite.hero || {}) },
bento: { ...FALLBACK_SITE_CONFIG.bento, ...(rawSite.bento || {}) },
contact: { ...FALLBACK_SITE_CONFIG.contact, ...(rawSite.contact || {}) }
};
const themeConfig = readJsonFile<ThemeConfig>(themePath, FALLBACK_THEME_CONFIG);
const smtpConfig = readJsonFile<SmtpConfig>(smtpPath, FALLBACK_SMTP_CONFIG);
return { siteConfig, themeConfig, smtpConfig };
}

124
src/utils/themePresets.ts Normal file
View File

@@ -0,0 +1,124 @@
export interface ThemePreset {
id: string;
name: string;
colors: {
bg: string;
cardBg: string;
text: string;
accent: string;
border: string;
};
}
export const THEME_PRESETS: Record<string, ThemePreset> = {
corporateDark: {
id: 'corporateDark',
name: 'Corporate Dark Mode',
colors: {
bg: '#0B0F19',
cardBg: 'rgba(15, 23, 42, 0.4)',
text: '#F8FAFC',
accent: '#38BDF8',
border: 'rgba(255, 255, 255, 0.1)'
}
},
cyberNeon: {
id: 'cyberNeon',
name: 'Cyber Neon',
colors: {
bg: '#05050A',
cardBg: 'rgba(10, 10, 25, 0.5)',
text: '#FFFFFF',
accent: '#F43F5E',
border: 'rgba(244, 63, 94, 0.2)'
}
},
emeraldCraft: {
id: 'emeraldCraft',
name: 'Emerald Craft',
colors: {
bg: '#022C22',
cardBg: 'rgba(6, 78, 59, 0.4)',
text: '#ECFDF5',
accent: '#34D399',
border: 'rgba(52, 211, 153, 0.2)'
}
},
royalViolet: {
id: 'royalViolet',
name: 'Royal Violet',
colors: {
bg: '#0F0728',
cardBg: 'rgba(30, 15, 65, 0.4)',
text: '#FAF5FF',
accent: '#A855F7',
border: 'rgba(168, 85, 247, 0.2)'
}
},
midnightBlue: {
id: 'midnightBlue',
name: 'Midnight Blue',
colors: {
bg: '#030712',
cardBg: 'rgba(17, 24, 39, 0.5)',
text: '#F9FAFB',
accent: '#60A5FA',
border: 'rgba(96, 165, 250, 0.2)'
}
},
amberGlow: {
id: 'amberGlow',
name: 'Amber Glow',
colors: {
bg: '#180E02',
cardBg: 'rgba(45, 26, 8, 0.5)',
text: '#FFFBEB',
accent: '#F59E0B',
border: 'rgba(245, 158, 11, 0.2)'
}
},
monochromeMinimal: {
id: 'monochromeMinimal',
name: 'Monochrome Minimal',
colors: {
bg: '#121212',
cardBg: 'rgba(255, 255, 255, 0.05)',
text: '#E0E0E0',
accent: '#FFFFFF',
border: 'rgba(255, 255, 255, 0.15)'
}
},
arcticFrost: {
id: 'arcticFrost',
name: 'Arctic Frost',
colors: {
bg: '#0B132B',
cardBg: 'rgba(28, 37, 65, 0.4)',
text: '#E0FBFC',
accent: '#70E000',
border: 'rgba(112, 224, 0, 0.2)'
}
},
sunsetCoral: {
id: 'sunsetCoral',
name: 'Sunset Coral',
colors: {
bg: '#1C0A10',
cardBg: 'rgba(50, 15, 28, 0.4)',
text: '#FFF1F2',
accent: '#FB7185',
border: 'rgba(251, 113, 133, 0.2)'
}
},
titaniumSteel: {
id: 'titaniumSteel',
name: 'Titanium Steel',
colors: {
bg: '#18181B',
cardBg: 'rgba(39, 39, 42, 0.5)',
text: '#F4F4F5',
accent: '#A1A1AA',
border: 'rgba(161, 161, 170, 0.2)'
}
}
};

53
tests/waasEngine.test.ts Normal file
View File

@@ -0,0 +1,53 @@
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import fs from 'fs';
import path from 'path';
import { loadWaasConfigs, FALLBACK_SITE_CONFIG, FALLBACK_THEME_CONFIG, FALLBACK_SMTP_CONFIG } from '../src/utils/configLoader';
import { THEME_PRESETS } from '../src/utils/themePresets';
describe('WaaS Engine Core Unit Tests', () => {
const testDataDir = path.join(process.cwd(), 'app', 'test_data');
beforeEach(() => {
process.env.DATA_DIR = testDataDir;
if (fs.existsSync(testDataDir)) {
fs.rmSync(testDataDir, { recursive: true, force: true });
}
});
afterEach(() => {
if (fs.existsSync(testDataDir)) {
fs.rmSync(testDataDir, { recursive: true, force: true });
}
});
it('1. Should return fallbacks when JSON files do not exist (Zero-Crash Guarantee)', () => {
const configs = loadWaasConfigs();
expect(configs.siteConfig.siteName).toBe(FALLBACK_SITE_CONFIG.siteName);
expect(configs.themeConfig.activePreset).toBe(FALLBACK_THEME_CONFIG.activePreset);
expect(configs.smtpConfig.host).toBe(FALLBACK_SMTP_CONFIG.host);
});
it('2. Should dynamically merge custom site.config.json', () => {
fs.mkdirSync(testDataDir, { recursive: true });
const customSite = { siteName: 'Custom Test Agency', hero: { title: 'Custom Title' } };
fs.writeFileSync(path.join(testDataDir, 'site.config.json'), JSON.stringify(customSite), 'utf8');
const configs = loadWaasConfigs();
expect(configs.siteConfig.siteName).toBe('Custom Test Agency');
expect(configs.siteConfig.hero.title).toBe('Custom Title');
expect(configs.siteConfig.hero.badge).toBe(FALLBACK_SITE_CONFIG.hero.badge);
});
it('3. Should have 10 valid Theme Presets with root CSS color tokens', () => {
const presetKeys = Object.keys(THEME_PRESETS);
expect(presetKeys.length).toBe(10);
expect(THEME_PRESETS.corporateDark.colors.bg).toBe('#0B0F19');
expect(THEME_PRESETS.corporateDark.colors.accent).toBe('#38BDF8');
});
it('4. Should support dynamic UIComponent elements in SectionConfig', () => {
const heroElements = FALLBACK_SITE_CONFIG.hero.elements || [];
expect(heroElements.length).toBeGreaterThan(0);
expect(heroElements[0].type).toBe('badge');
});
});